Passed
Push — master ( b4e3f0...d6a119 )
by Robbie
04:16 queued 02:31
created

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