Passed
Push — sheepy/introspection ( 000d40...b6b869 )
by Marco
02:43
created

DocumentFactory::addField()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 7
Bugs 1 Features 0
Metric Value
eloc 12
c 7
b 1
f 0
dl 0
loc 25
ccs 13
cts 13
cp 1
rs 9.2222
cc 6
nc 5
nop 3
crap 6
1
<?php
2
3
4
namespace Firesphere\SolrSearch\Factories;
5
6
use Exception;
7
use Firesphere\SolrSearch\Extensions\DataObjectExtension;
8
use Firesphere\SolrSearch\Helpers\SearchIntrospection;
9
use Firesphere\SolrSearch\Helpers\Statics;
10
use Firesphere\SolrSearch\Indexes\BaseIndex;
11
use Firesphere\SolrSearch\Services\SolrCoreService;
12
use Psr\Log\LoggerInterface;
13
use SilverStripe\Core\ClassInfo;
14
use SilverStripe\Core\Config\Configurable;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\ORM\ArrayList;
17
use SilverStripe\ORM\DataList;
18
use SilverStripe\ORM\DataObject;
19
use SilverStripe\ORM\FieldType\DBDate;
20
use SilverStripe\ORM\SS_List;
21
use Solarium\QueryType\Update\Query\Document\Document;
22
use Solarium\QueryType\Update\Query\Query;
23
24
class DocumentFactory
25
{
26
    use Configurable;
27
28
    /**
29
     * Numeral types in Solr
30
     * @var array
31
     */
32
    protected static $numerals = [
33
        'tint',
34
        'tfloat',
35
        'tdouble'
36
    ];
37
    /**
38
     * @var SearchIntrospection
39
     */
40
    protected $introspection;
41
    /**
42
     * @var null|ArrayList|DataList
43
     */
44
    protected $items;
45
    /**
46
     * @var string
47
     */
48
    protected $class;
49
    /**
50
     * @var bool
51
     */
52
    protected $debug = false;
53
54
    /**
55
     * @var null|LoggerInterface
56
     */
57
    protected $logger;
58
59
    /**
60
     * DocumentFactory constructor, sets up introspection
61
     */
62 4
    public function __construct()
63
    {
64 4
        $this->introspection = Injector::inst()->get(SearchIntrospection::class);
65 4
    }
66
67
    /**
68
     * Note, it can only take one type of class at a time!
69
     * So make sure you properly loop and set $class
70
     * @param $fields
71
     * @param BaseIndex $index
72
     * @param Query $update
73
     * @return array
74
     * @throws Exception
75
     */
76 14
    public function buildItems($fields, $index, $update): array
77
    {
78 14
        $class = $this->getClass();
79 14
        $this->getIntrospection()->setIndex($index);
80 14
        $boostFields = $index->getBoostedFields();
81 14
        $docs = [];
82 14
        $debugString = sprintf('Adding %s to %s%s', $class, $index->getIndexName(), PHP_EOL);
83 14
        $debugString .= '[';
84 14
        foreach ($this->getItems() as $item) {
85 14
            $debugString .= "$item->ID, ";
86
            /** @var Document $doc */
87 14
            $doc = $update->createDocument();
88 14
            $this->addDefaultFields($doc, $item);
89
90 14
            $this->buildField($fields, $doc, $item, $boostFields);
91 14
            $item->destroy();
92
93 14
            $docs[] = $doc;
94
        }
95
96 14
        if ($this->debug) {
97 11
            $this->getLogger()->info(rtrim($debugString, ', ') . ']' . PHP_EOL);
98
        }
99
100 14
        unset($this->items);
101
102 14
        return $docs;
103
    }
104
105
    /**
106
     * @return string
107
     */
108 14
    public function getClass(): string
109
    {
110 14
        return $this->class;
111
    }
112
113
    /**
114
     * @param string $class
115
     * @return DocumentFactory
116
     */
117 14
    public function setClass(string $class): DocumentFactory
118
    {
119 14
        $this->class = $class;
120
121 14
        return $this;
122
    }
123
124
    /**
125
     * @return SearchIntrospection
126
     */
127 15
    public function getIntrospection(): SearchIntrospection
128
    {
129 15
        return $this->introspection;
130
    }
131
132
    /**
133
     * @return ArrayList|DataList|null
134
     */
135 14
    public function getItems()
136
    {
137 14
        return $this->items;
138
    }
139
140
    /**
141
     * @param ArrayList|DataList|null $items
142
     * @return DocumentFactory
143
     */
144 14
    public function setItems($items): DocumentFactory
145
    {
146 14
        $this->items = $items;
147
148 14
        return $this;
149
    }
150
151
    /**
152
     * @param Document $doc
153
     * @param DataObject|DataObjectExtension $item
154
     */
155 14
    protected function addDefaultFields(Document $doc, DataObject $item)
156
    {
157 14
        $doc->setKey(SolrCoreService::ID_FIELD, $item->ClassName . '-' . $item->ID);
158 14
        $doc->addField(SolrCoreService::CLASS_ID_FIELD, $item->ID);
159 14
        $doc->addField('ClassName', $item->ClassName);
160 14
        $doc->addField('ClassHierarchy', ClassInfo::ancestry($item));
161 14
        $doc->addField('ViewStatus', $item->getViewStatus());
162 14
    }
163
164
    /**
165
     * @param $fields
166
     * @param Document $doc
167
     * @param DataObject $item
168
     * @param array $boostFields
169
     * @throws Exception
170
     */
171 14
    protected function buildField($fields, Document $doc, DataObject $item, array $boostFields): void
172
    {
173 14
        foreach ($fields as $field) {
174 14
            $fieldData = $this->getIntrospection()->getFieldIntrospection($field);
175 14
            foreach ($fieldData as $dataField => $options) {
176
                // Only one field per class, so let's take the fieldData. This will override previous additions
177 14
                $this->addField($doc, $item, $fieldData[$dataField]);
178 14
                if (array_key_exists($field, $boostFields)) {
179 14
                    $doc->setFieldBoost($dataField, $boostFields[$field]);
180
                }
181
            }
182
        }
183 14
    }
184
185
    /**
186
     * @param Document $doc
187
     * @param $object
188
     * @param $field
189
     */
190 14
    protected function addField($doc, $object, $field): void
191
    {
192 14
        if (!$this->classIs($object, $field['origin'])) {
193 10
            return;
194
        }
195
196 14
        $valuesForField = $this->getValueForField($object, $field);
197
198 14
        $typeMap = Statics::getTypeMap();
199 14
        $type = $typeMap[$field['type']] ?? $typeMap['*'];
200
201 14
        while ($value = array_shift($valuesForField)) {
202 14
            if (!$this->isValidValue($value, $type)) {
203 14
                continue;
204
            }
205
206
            /* Solr requires dates in the form 1995-12-31T23:59:59Z */
207 14
            if ($type === 'tdate' || $value instanceof DBDate) {
208 13
                $value = gmdate('Y-m-d\TH:i:s\Z', strtotime($value));
209
            }
210
211
212 14
            $name = $this->sanitiseName($field['name']);
213
214 14
            $doc->addField($name, $value);
215
        }
216 14
    }
217
218
    /**
219
     * Determine if the given object is one of the given type
220
     * @param string|array $class
221
     * @param array|string $base Class or list of base classes
222
     * @return bool
223
     */
224 14
    protected function classIs($class, $base): bool
225
    {
226 14
        $base = is_array($base) ? $base : [$base];
227
228 14
        foreach ($base as $nextBase) {
229 14
            if ($this->classEquals($class, $nextBase)) {
230 14
                return true;
231
            }
232
        }
233
234 10
        return false;
235
    }
236
237
    /**
238
     * Check if a base class is an instance of the expected base group
239
     * @param $class
240
     * @param $base
241
     * @return bool
242
     */
243 14
    protected function classEquals($class, $base): bool
244
    {
245 14
        return $class === $base || ($class instanceof $base);
246
    }
247
248
    /**
249
     * Given an object and a field definition get the current value of that field on that object
250
     *
251
     * @param DataObject|array $objects - The object to get the value from
252
     * @param array $field - The field definition to use
253
     * @return array Technically, it's always an array
254
     */
255 14
    protected function getValueForField($objects, $field)
256
    {
257
        // Make sure we always have an array to iterate
258 14
        $objects = is_array($objects) ? $objects : [$objects];
259
260 14
        while ($step = array_shift($field['lookup_chain'])) {
261
            // If we're looking up this step on an array or SS_List, do the step on every item, merge result
262 14
            $next = [];
263
264 14
            foreach ($objects as $item) {
265 14
                $item = $this->getItemForStep($step, $item);
266
267 14
                if (is_array($item)) {
268 14
                    $next = array_merge($next, $item);
269
                } else {
270
                    $next[] = $item;
271
                }
272
                // Destroy the item(s) to clear out memory
273 14
                unset($item);
274
            }
275
276
            // When all objects have been processed, put them in to objects
277
            // This ensures the next step is an array of the correct objects to index
278 14
            $objects = $next;
279 14
            unset($next);
280
        }
281
282 14
        return $objects;
283
    }
284
285
    /**
286
     * Find the item for the current ste
287
     * This can be a DataList or ArrayList, or a string
288
     * @param $step
289
     * @param $item
290
     * @return array
291
     */
292 14
    protected function getItemForStep($step, $item)
293
    {
294 14
        if ($step['call'] === 'method') {
295
            $method = $step['method'];
296
            $item = $item->$method();
297
        } else {
298 14
            $property = $step['property'];
299 14
            $item = $item->$property;
300
        }
301
302 14
        if ($item instanceof SS_List) {
303
            $item = $item->toArray();
304
        }
305
306 14
        return is_array($item) ? $item : [$item];
307
    }
308
309
    /**
310
     * Check if a given value is valid for the type
311
     * @param string $value
312
     * @param string $type
313
     * @return bool
314
     */
315 14
    protected function isValidValue($value, $type)
316
    {
317
        // Value must be set and a string type value
318 14
        if (!$value || !is_string($value)) {
319 14
            return false;
320
        }
321
322
        // And be a number if numeric type
323 14
        return !(!is_numeric($value) && in_array($type, static::$numerals, true));
324
    }
325
326
    /**
327
     * @param string $field
328
     * @return string
329
     */
330 15
    public function sanitiseName($field)
331
    {
332 15
        $name = explode('\\', $field);
333
334 15
        return end($name);
335
    }
336
337 12
    public function getLogger()
338
    {
339 12
        if (!$this->logger) {
340 3
            $this->logger = Injector::inst()->get(LoggerInterface::class);
341
        }
342
343 12
        return $this->logger;
344
    }
345
346
    /**
347
     * @param bool $debug
348
     * @return DocumentFactory
349
     */
350 13
    public function setDebug(bool $debug): DocumentFactory
351
    {
352 13
        $this->debug = $debug;
353
354 13
        return $this;
355
    }
356
}
357