2011-05-02 06:33:05 +02:00
|
|
|
<?php
|
2017-04-21 04:14:30 +02:00
|
|
|
|
2017-04-21 03:26:24 +02:00
|
|
|
namespace SilverStripe\FullTextSearch\Search\Updaters;
|
2017-04-21 04:14:30 +02:00
|
|
|
|
2017-11-14 05:05:30 +01:00
|
|
|
use SilverStripe\Core\Config\Configurable;
|
2017-04-26 14:24:46 +02:00
|
|
|
use SilverStripe\Core\Injector\Injector;
|
2017-04-26 13:13:26 +02:00
|
|
|
use SilverStripe\Dev\SapphireTest;
|
2017-04-26 14:24:46 +02:00
|
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
use SilverStripe\ORM\DB;
|
2017-04-21 06:34:04 +02:00
|
|
|
use SilverStripe\FullTextSearch\Search\FullTextSearch;
|
|
|
|
use SilverStripe\FullTextSearch\Search\SearchIntrospection;
|
2017-04-22 20:21:06 +02:00
|
|
|
use SilverStripe\FullTextSearch\Search\Variants\SearchVariant;
|
2017-04-21 06:34:04 +02:00
|
|
|
use SilverStripe\FullTextSearch\Search\Processors\SearchUpdateImmediateProcessor;
|
2017-04-22 11:22:54 +02:00
|
|
|
use ReflectionClass;
|
2017-04-26 10:58:14 +02:00
|
|
|
|
2011-05-02 06:33:05 +02:00
|
|
|
/**
|
|
|
|
* This class is responsible for capturing changes to DataObjects and triggering index updates of the resulting dirty index
|
|
|
|
* items.
|
|
|
|
*
|
|
|
|
* Attached automatically by _config calling SearchUpdater#bind_manipulation_capture. Overloads the current database connector's
|
|
|
|
* manipulate method - basically we need to capture a manipulation _after_ all the augmentManipulation code (for instance Version's)
|
|
|
|
* is run
|
|
|
|
*
|
|
|
|
* Pretty closely tied to the field structure of SearchIndex.
|
|
|
|
*
|
2013-07-25 04:27:09 +02:00
|
|
|
* TODO: The way we bind in is awful hacky.
|
2011-05-02 06:33:05 +02:00
|
|
|
*/
|
2017-04-21 01:37:01 +02:00
|
|
|
|
2017-11-14 04:08:28 +01:00
|
|
|
class SearchUpdater
|
2015-11-21 07:19:20 +01:00
|
|
|
{
|
2017-11-14 05:05:30 +01:00
|
|
|
use Configurable;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether to register the shutdown function to flush. Can be disabled for example in unit testing.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private static $flush_on_shutdown = true;
|
|
|
|
|
2015-11-21 07:19:20 +01:00
|
|
|
/**
|
|
|
|
* Replace the database object with a subclass that captures all manipulations and passes them to us
|
|
|
|
*/
|
|
|
|
public static function bind_manipulation_capture()
|
|
|
|
{
|
|
|
|
global $databaseConfig;
|
|
|
|
|
2017-04-21 06:34:04 +02:00
|
|
|
$current = DB::get_conn();
|
2017-04-26 14:24:46 +02:00
|
|
|
|
2017-04-21 06:34:04 +02:00
|
|
|
if (!$current || !$current->getSelectedDatabase() || @$current->isManipulationCapture) {
|
2015-11-21 07:19:20 +01:00
|
|
|
return;
|
|
|
|
} // If not yet set, or its already captured, just return
|
|
|
|
|
2017-04-22 11:22:54 +02:00
|
|
|
$type = (new ReflectionClass($current))->getShortName();
|
|
|
|
$dbClass = 'SilverStripe\FullTextSearch\Captures\SearchManipulateCapture_' . $type;
|
|
|
|
|
|
|
|
// Check if Capture class exists.
|
|
|
|
if (!class_exists($dbClass)) {
|
|
|
|
return;
|
|
|
|
}
|
2015-11-21 07:19:20 +01:00
|
|
|
|
|
|
|
/** @var SS_Database $captured */
|
|
|
|
$captured = new $dbClass($databaseConfig);
|
|
|
|
|
2017-04-22 11:22:54 +02:00
|
|
|
$captured->setConnector($current->getConnector());
|
|
|
|
$captured->setQueryBuilder($current->getQueryBuilder());
|
|
|
|
$captured->setSchemaManager($current->getSchemaManager());
|
2015-11-21 07:19:20 +01:00
|
|
|
|
|
|
|
// The connection might have had it's name changed (like if we're currently in a test)
|
2017-04-21 06:34:04 +02:00
|
|
|
$captured->selectDatabase($current->getSelectedDatabase());
|
|
|
|
DB::set_conn($captured);
|
2015-11-21 07:19:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public static $registered = false;
|
|
|
|
/** @var SearchUpdateProcessor */
|
|
|
|
public 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
|
|
|
|
*/
|
|
|
|
public static function handle_manipulation($manipulation)
|
|
|
|
{
|
|
|
|
// First, extract any state that is in the manipulation itself
|
|
|
|
foreach ($manipulation as $table => $details) {
|
2017-04-21 06:34:04 +02:00
|
|
|
if (!isset($manipulation[$table]['class'])) {
|
|
|
|
$manipulation[$table]['class'] = DataObject::getSchema()->tableClass($table);
|
|
|
|
}
|
2015-11-21 07:19:20 +01:00
|
|
|
$manipulation[$table]['state'] = array();
|
|
|
|
}
|
|
|
|
|
|
|
|
SearchVariant::call('extractManipulationState', $manipulation);
|
|
|
|
|
|
|
|
// Then combine the manipulation back into object field sets
|
|
|
|
|
|
|
|
$writes = array();
|
|
|
|
|
|
|
|
foreach ($manipulation as $table => $details) {
|
2016-04-15 05:46:19 +02:00
|
|
|
if (!isset($details['id'])) {
|
2015-11-21 07:19:20 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$id = $details['id'];
|
|
|
|
$state = $details['state'];
|
|
|
|
$class = $details['class'];
|
2017-03-08 23:40:13 +01:00
|
|
|
$command = $details['command'];
|
2016-04-15 05:46:19 +02:00
|
|
|
$fields = isset($details['fields']) ? $details['fields'] : array();
|
2015-11-21 07:19:20 +01:00
|
|
|
|
2017-04-21 06:34:04 +02:00
|
|
|
$base = DataObject::getSchema()->baseDataClass($class);
|
2015-11-21 07:19:20 +01:00
|
|
|
$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,
|
2017-03-08 23:40:13 +01:00
|
|
|
'command' => $command,
|
2015-11-21 07:19:20 +01:00
|
|
|
'fields' => array()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// Otherwise update the class label if it's more specific than the currently recorded one
|
|
|
|
elseif (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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-08 23:40:13 +01:00
|
|
|
// Trim non-delete records without fields
|
|
|
|
foreach (array_keys($writes) as $key) {
|
|
|
|
if ($writes[$key]['command'] !== 'delete' && empty($writes[$key]['fields'])) {
|
2016-04-15 05:46:19 +02:00
|
|
|
unset($writes[$key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-21 07:19:20 +01:00
|
|
|
// 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
|
2016-04-15 05:46:19 +02:00
|
|
|
*
|
2015-11-21 07:19:20 +01:00
|
|
|
* @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) {
|
2017-04-21 06:34:04 +02:00
|
|
|
self::$processor = Injector::inst()->create(SearchUpdateImmediateProcessor::class);
|
2015-11-21 07:19:20 +01:00
|
|
|
}
|
|
|
|
self::$processor->addDirtyIDs($dirtyclass, $ids, $index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we do have some work to do register the shutdown function to actually do the work
|
2017-11-14 05:05:30 +01:00
|
|
|
if (self::$processor && !self::$registered && self::config()->get('flush_on_shutdown')) {
|
2017-04-21 06:34:04 +02:00
|
|
|
register_shutdown_function(array(SearchUpdater::class, "flush_dirty_indexes"));
|
2015-11-21 07:19:20 +01:00
|
|
|
self::$registered = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Throw away the recorded dirty IDs without doing anything with them.
|
|
|
|
*/
|
|
|
|
public 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.
|
|
|
|
*/
|
|
|
|
public static function flush_dirty_indexes()
|
|
|
|
{
|
|
|
|
if (!self::$processor) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
self::$processor->triggerProcessing();
|
|
|
|
self::$processor = null;
|
|
|
|
}
|
2011-05-02 06:33:05 +02:00
|
|
|
}
|