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-11-30 02:43:17 +01:00
|
|
|
use SilverStripe\ORM\Connect\Database;
|
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;
|
2020-02-10 05:05:16 +01:00
|
|
|
use SilverStripe\FullTextSearch\Search\Processors\SearchUpdateProcessor;
|
|
|
|
|
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
|
|
|
/**
|
2017-12-03 23:56:18 +01:00
|
|
|
* This class is responsible for capturing changes to DataObjects and triggering index updates of the resulting dirty
|
|
|
|
* index items.
|
2011-05-02 06:33:05 +02:00
|
|
|
*
|
2017-12-03 23:53:37 +01:00
|
|
|
* Attached automatically by Injector configuration that overloads your flavour of Database class. The
|
|
|
|
* SearchManipulateCapture_[type] classes overload the manipulate method - basically we need to capture a
|
|
|
|
* manipulation _after_ all the augmentManipulation code (for instance Version's) is run
|
2011-05-02 06:33:05 +02:00
|
|
|
*
|
|
|
|
* Pretty closely tied to the field structure of SearchIndex.
|
|
|
|
*/
|
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;
|
|
|
|
|
2018-03-20 04:32:32 +01:00
|
|
|
/**
|
|
|
|
* Whether the updater is enabled. Set to false for local development if you don't have a Solr server.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private static $enabled = true;
|
|
|
|
|
2015-11-21 07:19:20 +01:00
|
|
|
public static $registered = false;
|
|
|
|
/** @var SearchUpdateProcessor */
|
|
|
|
public static $processor = null;
|
|
|
|
|
|
|
|
/**
|
2018-03-14 21:45:14 +01:00
|
|
|
* Called by the ProxyDBExtension database connector with every manipulation made against the database.
|
2015-11-21 07:19:20 +01:00
|
|
|
*
|
|
|
|
* 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)
|
|
|
|
{
|
2018-03-20 04:32:32 +01:00
|
|
|
if (!static::config()->get('enabled')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-11-21 07:19:20 +01:00
|
|
|
// 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);
|
2018-06-27 06:50:54 +02:00
|
|
|
$key = "$id:$base:" . serialize($state);
|
2015-11-21 07:19:20 +01:00
|
|
|
|
|
|
|
$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()
|
|
|
|
);
|
2018-06-27 06:54:08 +02:00
|
|
|
} elseif (is_subclass_of($class, $writes[$key]['class'])) {
|
|
|
|
// Otherwise update the class label if it's more specific than the currently recorded one
|
2015-11-21 07:19:20 +01:00
|
|
|
$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) {
|
2020-02-10 05:05:16 +01:00
|
|
|
self::$processor = Injector::inst()->create(SearchUpdateProcessor::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
|
|
|
}
|