Completed
Push — master ( 8deccf...0fd7bd )
by
unknown
03:42 queued 02:08
created

SearchUpdater::bind_manipulation_capture()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 29
rs 8.439
cc 5
eloc 15
nc 3
nop 0
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
Unused Code introduced by
The property $flush_on_shutdown is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
39
40
    public static $registered = false;
41
    /** @var SearchUpdateProcessor */
42
    public static $processor = null;
43
44
    /**
45
     * Called by the SearchManiplateCapture database adapter with every manipulation made against the database.
46
     *
47
     * Check every index to see what objects need re-inserting into what indexes to keep the index fresh,
48
     * but doesn't actually do it yet.
49
     *
50
     * TODO: This is pretty sensitive to the format of manipulation that DataObject::write produces. Specifically,
51
     * it expects the actual class of the object to be present as a table, regardless of if any fields changed in that table
52
     * (so a class => array( 'fields' => array() ) item), in order to find the actual class for a set of table manipulations
53
     */
54
    public static function handle_manipulation($manipulation)
55
    {
56
        // First, extract any state that is in the manipulation itself
57
        foreach ($manipulation as $table => $details) {
58
            if (!isset($manipulation[$table]['class'])) {
59
                $manipulation[$table]['class'] = DataObject::getSchema()->tableClass($table);
60
            }
61
            $manipulation[$table]['state'] = array();
62
        }
63
64
        SearchVariant::call('extractManipulationState', $manipulation);
65
66
        // Then combine the manipulation back into object field sets
67
68
        $writes = array();
69
70
        foreach ($manipulation as $table => $details) {
71
            if (!isset($details['id'])) {
72
                continue;
73
            }
74
75
            $id = $details['id'];
76
            $state = $details['state'];
77
            $class = $details['class'];
78
            $command = $details['command'];
79
            $fields = isset($details['fields']) ? $details['fields'] : array();
80
81
            $base = DataObject::getSchema()->baseDataClass($class);
82
            $key = "$id:$base:".serialize($state);
83
84
            $statefulids = array(array('id' => $id, 'state' => $state));
85
86
            // Is this the first table for this particular object? Then add an item to $writes
87
            if (!isset($writes[$key])) {
88
                $writes[$key] = array(
89
                    'base' => $base,
90
                    'class' => $class,
91
                    'id' => $id,
92
                    'statefulids' => $statefulids,
93
                    'command' => $command,
94
                    'fields' => array()
95
                );
96
            } // Otherwise update the class label if it's more specific than the currently recorded one
97
            elseif (is_subclass_of($class, $writes[$key]['class'])) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $writes[$key]['class'] can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
98
                $writes[$key]['class'] = $class;
99
            }
100
101
            // Update the fields
102
            foreach ($fields as $field => $value) {
103
                $writes[$key]['fields']["$class:$field"] = $value;
104
            }
105
        }
106
107
        // Trim non-delete records without fields
108
        foreach (array_keys($writes) as $key) {
109
            if ($writes[$key]['command'] !== 'delete' && empty($writes[$key]['fields'])) {
110
                unset($writes[$key]);
111
            }
112
        }
113
114
        // Then extract any state that is needed for the writes
115
116
        SearchVariant::call('extractManipulationWriteState', $writes);
117
118
        // Submit all of these writes to the search processor
119
120
        static::process_writes($writes);
121
    }
122
123
    /**
124
     * Send updates to the current search processor for execution
125
     *
126
     * @param array $writes
127
     */
128
    public static function process_writes($writes)
129
    {
130
        foreach ($writes as $write) {
131
            // For every index
132
            foreach (FullTextSearch::get_indexes() as $index => $instance) {
133
                // If that index as a field from this class
134
                if (SearchIntrospection::is_subclass_of($write['class'], $instance->dependancyList)) {
135
                    // Get the dirty IDs
136
                    $dirtyids = $instance->getDirtyIDs($write['class'], $write['id'], $write['statefulids'], $write['fields']);
137
138
                    // Then add then then to the global list to deal with later
139
                    foreach ($dirtyids as $dirtyclass => $ids) {
140
                        if ($ids) {
141
                            if (!self::$processor) {
142
                                self::$processor = Injector::inst()->create(SearchUpdateImmediateProcessor::class);
143
                            }
144
                            self::$processor->addDirtyIDs($dirtyclass, $ids, $index);
145
                        }
146
                    }
147
                }
148
            }
149
        }
150
151
        // If we do have some work to do register the shutdown function to actually do the work
152
        if (self::$processor && !self::$registered && self::config()->get('flush_on_shutdown')) {
153
            register_shutdown_function(array(SearchUpdater::class, "flush_dirty_indexes"));
154
            self::$registered = true;
155
        }
156
    }
157
158
    /**
159
     * Throw away the recorded dirty IDs without doing anything with them.
160
     */
161
    public static function clear_dirty_indexes()
162
    {
163
        self::$processor = null;
164
    }
165
166
    /**
167
     * Do something with the recorded dirty IDs, where that "something" depends on the value of self::$update_method,
168
     * either immediately update the indexes, queue a messsage to update the indexes at some point in the future, or
169
     * just throw the dirty IDs away.
170
     */
171
    public static function flush_dirty_indexes()
172
    {
173
        if (!self::$processor) {
174
            return;
175
        }
176
        self::$processor->triggerProcessing();
177
        self::$processor = null;
178
    }
179
}
180