isManipulationCapture) return; // If not yet set, or its already captured, just return $type = get_class($current); $file = TEMP_FOLDER."/.cache.SMC.$type"; if (!is_file($file)) { file_put_contents($file, "selectDatabase($current->currentDatabase()); DB::setConn($captured); } static $registered = false; /** @var SearchUpdateProcessor */ static $processor = null; /** * Called by the SearchManiplateCapture database adapter with every manipulation made against the database. * * Check every index to see what objects need re-inserting into what indexes to keep the index fresh, * but doesn't actually do it yet. * * TODO: This is pretty sensitive to the format of manipulation that DataObject::write produces. Specifically, * it expects the actual class of the object to be present as a table, regardless of if any fields changed in that table * (so a class => array( 'fields' => array() ) item), in order to find the actual class for a set of table manipulations */ static function handle_manipulation($manipulation) { // First, extract any state that is in the manipulation itself foreach ($manipulation as $table => $details) { $manipulation[$table]['class'] = $table; $manipulation[$table]['state'] = array(); } SearchVariant::call('extractManipulationState', $manipulation); // Then combine the manipulation back into object field sets $writes = array(); foreach ($manipulation as $table => $details) { if (!isset($details['id']) || !isset($details['fields'])) continue; $id = $details['id']; $state = $details['state']; $class = $details['class']; $fields = $details['fields']; $base = ClassInfo::baseDataClass($class); $key = "$id:$base:".serialize($state); $statefulids = array(array('id' => $id, 'state' => $state)); // Is this the first table for this particular object? Then add an item to $writes if (!isset($writes[$key])) { $writes[$key] = array( 'base' => $base, 'class' => $class, 'id' => $id, 'statefulids' => $statefulids, 'fields' => array() ); } // Otherwise update the class label if it's more specific than the currently recorded one else if (is_subclass_of($class, $writes[$key]['class'])) { $writes[$key]['class'] = $class; } // Update the fields foreach ($fields as $field => $value) { $writes[$key]['fields']["$class:$field"] = $value; } } // Then extract any state that is needed for the writes SearchVariant::call('extractManipulationWriteState', $writes); // Submit all of these writes to the search processor static::process_writes($writes); } /** * Send updates to the current search processor for execution * * @param array $writes */ public static function process_writes($writes) { foreach ($writes as $write) { // For every index foreach (FullTextSearch::get_indexes() as $index => $instance) { // If that index as a field from this class if (SearchIntrospection::is_subclass_of($write['class'], $instance->dependancyList)) { // Get the dirty IDs $dirtyids = $instance->getDirtyIDs($write['class'], $write['id'], $write['statefulids'], $write['fields']); // Then add then then to the global list to deal with later foreach ($dirtyids as $dirtyclass => $ids) { if ($ids) { if (!self::$processor) { self::$processor = Injector::inst()->create('SearchUpdateProcessor'); } self::$processor->addDirtyIDs($dirtyclass, $ids, $index); } } } } } // If we do have some work to do register the shutdown function to actually do the work // Don't do it if we're testing - there's no database connection outside the test methods, so we'd // just get errors $runningTests = class_exists('SapphireTest', false) && SapphireTest::is_running_test(); if (self::$processor && !self::$registered && !$runningTests) { register_shutdown_function(array("SearchUpdater", "flush_dirty_indexes")); self::$registered = true; } } /** * Throw away the recorded dirty IDs without doing anything with them. */ static function clear_dirty_indexes() { self::$processor = null; } /** * Do something with the recorded dirty IDs, where that "something" depends on the value of self::$update_method, * either immediately update the indexes, queue a messsage to update the indexes at some point in the future, or * just throw the dirty IDs away. */ static function flush_dirty_indexes() { if (!self::$processor) return; self::$processor->triggerProcessing(); self::$processor = null; } } class SearchUpdater_BindManipulationCaptureFilter implements RequestFilter { public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model) { SearchUpdater::bind_manipulation_capture(); } public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model) { /* NOP */ } } /** * Delete operations do not use database manipulations. * * If a delete has been requested, force a write on objects that should be * indexed. This causes the object to be marked for deletion from the index. */ class SearchUpdater_ObjectHandler extends DataExtension { public function onAfterDelete() { // Calling delete() on empty objects does nothing if (!$this->owner->ID) return; // Force SearchUpdater to mark this record as dirty $manipulation = array( $this->owner->ClassName => array( 'fields' => array(), 'id' => $this->owner->ID, 'command' => 'update' ) ); $this->owner->extend('augmentWrite', $manipulation); SearchUpdater::handle_manipulation($manipulation); } /** * Forces this object to trigger a re-index in the current state */ public function triggerReindex() { if (!$this->owner->ID) { return; } $id = $this->owner->ID; $class = $this->owner->ClassName; $state = SearchVariant::current_state(); $base = ClassInfo::baseDataClass($class); $key = "$id:$base:".serialize($state); $statefulids = array(array( 'id' => $id, 'state' => $state )); $writes = array( $key => array( 'base' => $base, 'class' => $class, 'id' => $id, 'statefulids' => $statefulids, 'fields' => array() ) ); SearchUpdater::process_writes($writes); } }