Passed
Pull Request — master (#77)
by Marco
05:19 queued 03:13
created

DocumentFactory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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