IMMEDIATE and DEFERRED => DISABLED */ static $update_method = SearchUpdater::AUTO; // How many items can be dirty before we defer updates static $auto_threshold = 6; // The indexing message queue static $reindex_queue = "search_indexing"; static function set_reindexing_queue($queue) { self::$reindex_queue = $queue; } /** * Replace the database object with a subclass that captures all manipulations and passes them to us */ static function bind_manipulation_capture() { global $databaseConfig; $current = DB::getConn(); if (!$current || @$current->isManipulationCapture) return; // If not yet set, or its already captured, just return $type = $databaseConfig['type']; $file = TEMP_FOLDER."/.cache.SMC.$type"; if (!is_file($file)) { file_put_contents($file, "selectDatabase($current->currentDatabase()); DB::setConn($captured); } static $dirty = array(); static $dirtycount = 0; static function add_dirty_ids($class, $statefulids, $index) { $base = ClassInfo::baseDataClass($class); $forclass = isset(self::$dirty[$base]) ? self::$dirty[$base] : array(); foreach ($statefulids as $statefulid) { $id = $statefulid['id']; $state = $statefulid['state']; $statekey = serialize($state); if (!isset($forclass[$statekey])) { $forclass[$statekey] = array('state' => $state, 'ids' => array($id => array($index))); self::$dirtycount += 1; } else if (!isset($forclass[$statekey]['ids'][$id])) { $forclass[$statekey]['ids'][$id] = array($index); self::$dirtycount += 1; } else if (array_search($index, $forclass[$statekey]['ids'][$id]) === false) { $forclass[$statekey]['ids'][$id][] = $index; // dirty count stays the same } } self::$dirty[$base] = $forclass; } static $registered = false; /** * 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); // Then for each write, figure out what objects need updating 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) self::add_dirty_ids($dirtyclass, $ids, $index); } } } } // Finally, 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 if (self::$dirty && !self::$registered && !(class_exists('SapphireTest',false) && SapphireTest::is_running_test())) { 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::$dirty = array(); self::$dirtycount = 0; } /** * 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::$dirty) return; $method = self::$update_method; if (class_exists("MessageQueue")) { if ($method == self::AUTO) $method = self::$dirtycount < self::$auto_threshold ? self::IMMEDIATE : self::DEFERRED; } else { if ($method == self::AUTO) $method = self::IMMEDIATE; elseif ($method == self::DEFERRED) $method = self::DISABLED; } switch ($method) { case self::IMMEDIATE: self::process_dirty_indexes(self::$dirty); break; case self::DEFERRED: MessageQueue::send( self::$reindex_queue, new MethodInvocationMessage("SearchUpdater", "process_dirty_indexes", self::$dirty) ); break; case self::DISABLED: // NOP break; } self::clear_dirty_indexes(); } /** * Internal function. Process the passed list of dirty ids. Split from flush_dirty_indexes so it can be called both * directly and via messagequeue message. */ static function process_dirty_indexes($dirty) { $indexes = FullTextSearch::get_indexes(); $dirtyindexes = array(); $originalState = SearchVariant::current_state(); foreach ($dirty as $base => $statefulids) { if (!$statefulids) continue; foreach ($statefulids as $statefulid) { $state = $statefulid['state']; $ids = $statefulid['ids']; SearchVariant::activate_state($state); $objs = DataObject::get($base, '"'.$base.'"."ID" IN ('.implode(',', array_keys($ids)).')'); if ($objs) foreach ($objs as $obj) { foreach ($ids[$obj->ID] as $index) { $indexes[$index]->add($obj); $dirtyindexes[$index] = $index; } unset($ids[$obj->ID]); } foreach ($ids as $id => $fromindexes) { foreach ($fromindexes as $index) { $indexes[$index]->delete($base, $id, $state); $dirtyindexes[$index] = $index; } } } } foreach ($dirtyindexes as $index) { $indexes[$index]->commit(); } SearchVariant::activate_state($originalState); } } 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 */ } }