Issues (186)

src/Search/Updaters/SearchUpdater.php (2 issues)

1
<?php
2
3
namespace SilverStripe\FullTextSearch\Search\Updaters;
4
5
use SilverStripe\Core\Config\Configurable;
6
use SilverStripe\Core\Injector\Injector;
7
use SilverStripe\Dev\SapphireTest;
8
use SilverStripe\ORM\Connect\Database;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\DB;
11
use SilverStripe\FullTextSearch\Search\FullTextSearch;
12
use SilverStripe\FullTextSearch\Search\SearchIntrospection;
13
use SilverStripe\FullTextSearch\Search\Variants\SearchVariant;
14
use SilverStripe\FullTextSearch\Search\Processors\SearchUpdateProcessor;
15
16
use ReflectionClass;
17
18
/**
19
 * This class is responsible for capturing changes to DataObjects and triggering index updates of the resulting dirty
20
 * index items.
21
 *
22
 * Attached automatically by Injector configuration that overloads your flavour of Database class. The
23
 * SearchManipulateCapture_[type] classes overload the manipulate method - basically we need to capture a
24
 * manipulation _after_ all the augmentManipulation code (for instance Version's) is run
25
 *
26
 * Pretty closely tied to the field structure of SearchIndex.
27
 */
28
29
class SearchUpdater
30
{
31
    use Configurable;
32
33
    /**
34
     * Whether to register the shutdown function to flush. Can be disabled for example in unit testing.
35
     *
36
     * @config
37
     * @var bool
38
     */
39
    private static $flush_on_shutdown = true;
0 ignored issues
show
The private property $flush_on_shutdown is not used, and could be removed.
Loading history...
40
41
    /**
42
     * Whether the updater is enabled. Set to false for local development if you don't have a Solr server.
43
     *
44
     * @config
45
     * @var bool
46
     */
47
    private static $enabled = true;
0 ignored issues
show
The private property $enabled is not used, and could be removed.
Loading history...
48
49
    public static $registered = false;
50
    /** @var SearchUpdateProcessor */
51
    public static $processor = null;
52
53
    /**
54
     * Called by the ProxyDBExtension database connector with every manipulation made against the database.
55
     *
56
     * Check every index to see what objects need re-inserting into what indexes to keep the index fresh,
57
     * but doesn't actually do it yet.
58
     *
59
     * TODO: This is pretty sensitive to the format of manipulation that DataObject::write produces. Specifically,
60
     * it expects the actual class of the object to be present as a table, regardless of if any fields changed in that table
61
     * (so a class => array( 'fields' => array() ) item), in order to find the actual class for a set of table manipulations
62
     */
63
    public static function handle_manipulation($manipulation)
64
    {
65
        if (!static::config()->get('enabled')) {
66
            return;
67
        }
68
69
        // First, extract any state that is in the manipulation itself
70
        foreach ($manipulation as $table => $details) {
71
            if (!isset($manipulation[$table]['class'])) {
72
                $manipulation[$table]['class'] = DataObject::getSchema()->tableClass($table);
73
            }
74
            $manipulation[$table]['state'] = array();
75
        }
76
77
        SearchVariant::call('extractManipulationState', $manipulation);
78
79
        // Then combine the manipulation back into object field sets
80
81
        $writes = array();
82
83
        foreach ($manipulation as $table => $details) {
84
            if (!isset($details['id'])) {
85
                continue;
86
            }
87
88
            $id = $details['id'];
89
            $state = $details['state'];
90
            $class = $details['class'];
91
            $command = $details['command'];
92
            $fields = isset($details['fields']) ? $details['fields'] : array();
93
94
            $base = DataObject::getSchema()->baseDataClass($class);
95
            $key = "$id:$base:" . serialize($state);
96
97
            $statefulids = array(array('id' => $id, 'state' => $state));
98
99
            // Is this the first table for this particular object? Then add an item to $writes
100
            if (!isset($writes[$key])) {
101
                $writes[$key] = array(
102
                    'base' => $base,
103
                    'class' => $class,
104
                    'id' => $id,
105
                    'statefulids' => $statefulids,
106
                    'command' => $command,
107
                    'fields' => array()
108
                );
109
            } elseif (is_subclass_of($class, $writes[$key]['class'])) {
110
                // Otherwise update the class label if it's more specific than the currently recorded one
111
                $writes[$key]['class'] = $class;
112
            }
113
114
            // Update the fields
115
            foreach ($fields as $field => $value) {
116
                $writes[$key]['fields']["$class:$field"] = $value;
117
            }
118
        }
119
120
        // Trim non-delete records without fields
121
        foreach (array_keys($writes) as $key) {
122
            if ($writes[$key]['command'] !== 'delete' && empty($writes[$key]['fields'])) {
123
                unset($writes[$key]);
124
            }
125
        }
126
127
        // Then extract any state that is needed for the writes
128
129
        SearchVariant::call('extractManipulationWriteState', $writes);
130
131
        // Submit all of these writes to the search processor
132
133
        static::process_writes($writes);
134
    }
135
136
    /**
137
     * Send updates to the current search processor for execution
138
     *
139
     * @param array $writes
140
     */
141
    public static function process_writes($writes)
142
    {
143
        foreach ($writes as $write) {
144
            // For every index
145
            foreach (FullTextSearch::get_indexes() as $index => $instance) {
146
                // If that index as a field from this class
147
                if (SearchIntrospection::is_subclass_of($write['class'], $instance->dependancyList)) {
148
                    // Get the dirty IDs
149
                    $dirtyids = $instance->getDirtyIDs($write['class'], $write['id'], $write['statefulids'], $write['fields']);
150
151
                    // Then add then then to the global list to deal with later
152
                    foreach ($dirtyids as $dirtyclass => $ids) {
153
                        if ($ids) {
154
                            if (!self::$processor) {
155
                                self::$processor = Injector::inst()->create(SearchUpdateProcessor::class);
156
                            }
157
                            self::$processor->addDirtyIDs($dirtyclass, $ids, $index);
158
                        }
159
                    }
160
                }
161
            }
162
        }
163
164
        // If we do have some work to do register the shutdown function to actually do the work
165
        if (self::$processor && !self::$registered && self::config()->get('flush_on_shutdown')) {
166
            register_shutdown_function(array(SearchUpdater::class, "flush_dirty_indexes"));
167
            self::$registered = true;
168
        }
169
    }
170
171
    /**
172
     * Throw away the recorded dirty IDs without doing anything with them.
173
     */
174
    public static function clear_dirty_indexes()
175
    {
176
        self::$processor = null;
177
    }
178
179
    /**
180
     * Do something with the recorded dirty IDs, where that "something" depends on the value of self::$update_method,
181
     * either immediately update the indexes, queue a messsage to update the indexes at some point in the future, or
182
     * just throw the dirty IDs away.
183
     */
184
    public static function flush_dirty_indexes()
185
    {
186
        if (!self::$processor) {
187
            return;
188
        }
189
        self::$processor->triggerProcessing();
190
        self::$processor = null;
191
    }
192
}
193