SearchIndex::fieldData()   F
last analyzed

Complexity

Conditions 31
Paths 470

Size

Total Lines 153
Code Lines 97

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 31
eloc 97
nc 470
nop 3
dl 0
loc 153
rs 0.7361
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\FullTextSearch\Search\Indexes;
4
5
use Exception;
6
use Psr\Log\LoggerInterface;
7
use SilverStripe\Core\ClassInfo;
8
use SilverStripe\Core\Config\Config;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\FullTextSearch\Search\SearchIntrospection;
11
use SilverStripe\FullTextSearch\Search\Variants\SearchVariant;
12
use SilverStripe\FullTextSearch\Utils\MultipleArrayIterator;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\FieldType\DBField;
15
use SilverStripe\ORM\FieldType\DBString;
16
use SilverStripe\ORM\Queries\SQLSelect;
17
use SilverStripe\View\ViewableData;
18
use SilverStripe\ORM\SS_List;
19
20
/**
21
 * SearchIndex is the base index class. Each connector will provide a subclass of this that
22
 * provides search engine specific behavior.
23
 *
24
 * This class is responsible for:
25
 *
26
 * - Taking index calls adding classes and fields, and resolving those to value sources and types
27
 *
28
 * - Determining which records in this index need updating when a DataObject is changed
29
 *
30
 * - Providing utilities to the connector indexes
31
 *
32
 * The connector indexes are responsible for
33
 *
34
 * - Mapping types to index configuration
35
 *
36
 * - Adding and removing items to index
37
 *
38
 * - Parsing and converting SearchQueries into a form the engine will understand, and executing those queries
39
 *
40
 * The user indexes are responsible for
41
 *
42
 * - Specifying which classes and fields this index contains
43
 *
44
 * - Specifying update rules that are not extractable from metadata (because the values come from functions for instance)
45
 *
46
 */
47
abstract class SearchIndex extends ViewableData
48
{
49
    /**
50
     * Allows this index to hide a parent index. Specifies the name of a parent index to disable
51
     *
52
     * @var string
53
     * @config
54
     */
55
    private static $hide_ancestor;
56
57
    /**
58
     * Used to separate class name and relation name in the sources array
59
     * this string must not be present in class name
60
     * @var string
61
     * @config
62
     */
63
    private static $class_delimiter = '_|_';
64
65
    /**
66
     * This is used to clean the source name from suffix
67
     * suffixes are needed to support multiple relations with the same name on different page types
68
     * @param string $source
69
     * @return string
70
     */
71
    protected function getSourceName($source)
72
    {
73
        $source = explode(self::config()->get('class_delimiter'), $source);
74
75
        return $source[0];
76
    }
77
78
    public function __construct()
79
    {
80
        parent::__construct();
81
        $this->init();
82
83
        foreach ($this->getClasses() as $class => $options) {
84
            SearchVariant::with($class, $options['include_children'])->call('alterDefinition', $class, $this);
85
        }
86
87
        $this->buildDependancyList();
88
    }
89
90
    public function __toString()
91
    {
92
        return 'Search Index ' . get_class($this);
93
    }
94
95
    /**
96
     * Examines the classes this index is built on to try and find defined fields in the class hierarchy
97
     * for those classes.
98
     * Looks for db and viewable-data fields, although can't necessarily find type for viewable-data fields.
99
     * If multiple classes have a relation with the same name all of these will be included in the search index
100
     * Note that only classes that have the relations uninherited (defined in them) will be listed
101
     * this is because inherited relations do not need to be processed by index explicitly
102
     */
103
    public function fieldData($field, $forceType = null, $extraOptions = [])
104
    {
105
        $fullfield = str_replace(".", "_", $field);
106
        $sources = $this->getClasses();
107
108
        foreach ($sources as $source => $options) {
109
            $sources[$source]['base'] = DataObject::getSchema()->baseDataClass($source);
110
            $sources[$source]['lookup_chain'] = [];
111
        }
112
113
        $found = [];
114
115
        if (strpos($field, '.') !== false) {
116
            $lookups = explode(".", $field);
117
            $field = array_pop($lookups);
118
119
            foreach ($lookups as $lookup) {
120
                $next = [];
121
122
                foreach ($sources as $source => $baseOptions) {
123
                    $source = $this->getSourceName($source);
124
125
                    foreach (SearchIntrospection::hierarchy($source, $baseOptions['include_children']) as $dataclass) {
126
                        $class = null;
127
                        $options = $baseOptions;
128
                        $singleton = singleton($dataclass);
129
                        $schema = DataObject::getSchema();
130
                        $className = $singleton->getClassName();
131
132
                        if ($hasOne = $schema->hasOneComponent($className, $lookup)) {
133
                            // we only want to include base class for relation, omit classes that inherited the relation
134
                            $relationList = Config::inst()->get($dataclass, 'has_one', Config::UNINHERITED);
135
                            $relationList = (!is_null($relationList)) ? $relationList : [];
136
                            if (!array_key_exists($lookup, $relationList)) {
137
                                continue;
138
                            }
139
140
                            $class = $hasOne;
141
                            $options['lookup_chain'][] = array(
142
                                'call' => 'method', 'method' => $lookup,
143
                                'through' => 'has_one', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => "{$lookup}ID"
144
                            );
145
                        } elseif ($hasMany = $schema->hasManyComponent($className, $lookup)) {
146
                            // we only want to include base class for relation, omit classes that inherited the relation
147
                            $relationList = Config::inst()->get($dataclass, 'has_many', Config::UNINHERITED);
148
                            $relationList = (!is_null($relationList)) ? $relationList : [];
149
                            if (!array_key_exists($lookup, $relationList)) {
150
                                continue;
151
                            }
152
153
                            $class = $hasMany;
154
                            $options['multi_valued'] = true;
155
                            $options['lookup_chain'][] = array(
156
                                'call' => 'method', 'method' => $lookup,
157
                                'through' => 'has_many', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => $schema->getRemoteJoinField($className, $lookup, 'has_many')
158
                            );
159
                        } elseif ($manyMany = $schema->manyManyComponent($className, $lookup)) {
160
                            // we only want to include base class for relation, omit classes that inherited the relation
161
                            $relationList = Config::inst()->get($dataclass, 'many_many', Config::UNINHERITED);
162
                            $relationList = (!is_null($relationList)) ? $relationList : [];
163
                            if (!array_key_exists($lookup, $relationList)) {
164
                                continue;
165
                            }
166
167
                            $class = $manyMany['childClass'];
168
                            $options['multi_valued'] = true;
169
                            $options['lookup_chain'][] = array(
170
                                'call' => 'method',
171
                                'method' => $lookup,
172
                                'through' => 'many_many',
173
                                'class' => $dataclass,
174
                                'otherclass' => $class,
175
                                'details' => $manyMany,
176
                            );
177
                        }
178
179
                        if (is_string($class) && $class) {
180
                            if (!isset($options['origin'])) {
181
                                $options['origin'] = $dataclass;
182
                            }
183
184
                            // we add suffix here to prevent the relation to be overwritten by other instances
185
                            // all sources lookups must clean the source name before reading it via getSourceName()
186
                            $next[$class . self::config()->get('class_delimiter') . $dataclass] = $options;
187
                        }
188
                    }
189
                }
190
191
                if (!$next) {
192
                    return $next;
193
                } // Early out to avoid excessive empty looping
194
                $sources = $next;
195
            }
196
        }
197
198
        foreach ($sources as $class => $options) {
199
            $class = $this->getSourceName($class);
200
            $dataclasses = SearchIntrospection::hierarchy($class, $options['include_children']);
201
202
            while (count($dataclasses)) {
203
                $dataclass = array_shift($dataclasses);
204
                $type = null;
205
                $fieldoptions = $options;
206
207
                $fields = DataObject::getSchema()->databaseFields($class);
208
209
                if (isset($fields[$field])) {
210
                    $type = $fields[$field];
211
                    $fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field);
212
                } else {
213
                    $singleton = singleton($dataclass);
214
215
                    if ($singleton->hasMethod("get$field") || $singleton->hasField($field)) {
216
                        $type = $singleton->castingClass($field);
217
                        if (!$type) {
218
                            $type = 'String';
219
                        }
220
221
                        if ($singleton->hasMethod("get$field")) {
222
                            $fieldoptions['lookup_chain'][] = array('call' => 'method', 'method' => "get$field");
223
                        } else {
224
                            $fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field);
225
                        }
226
                    }
227
                }
228
229
                if ($type) {
230
                    // Don't search through child classes of a class we matched on. TODO: Should we?
231
                    $dataclasses = array_diff($dataclasses, array_values(ClassInfo::subclassesFor($dataclass)));
232
                    // Trim arguments off the type string
233
                    if (preg_match('/^(\w+)\(/', $type, $match)) {
234
                        $type = $match[1];
235
                    }
236
                    // Get the origin
237
                    $origin = isset($fieldoptions['origin']) ? $fieldoptions['origin'] : $dataclass;
238
239
                    $found["{$origin}_{$fullfield}"] = array(
240
                        'name' => "{$origin}_{$fullfield}",
241
                        'field' => $field,
242
                        'fullfield' => $fullfield,
243
                        'base' => $fieldoptions['base'],
244
                        'origin' => $origin,
245
                        'class' => $dataclass,
246
                        'lookup_chain' => $fieldoptions['lookup_chain'],
247
                        'type' => $forceType ? $forceType : $type,
248
                        'multi_valued' => isset($fieldoptions['multi_valued']) ? true : false,
249
                        'extra_options' => $extraOptions
250
                    );
251
                }
252
            }
253
        }
254
255
        return $found;
256
    }
257
258
    /** Public, but should only be altered by variants */
259
260
    protected $classes = array();
261
262
    protected $fulltextFields = array();
263
264
    public $filterFields = array();
265
266
    protected $sortFields = array();
267
268
    protected $excludedVariantStates = array();
269
270
    /**
271
     * Add a DataObject subclass whose instances should be included in this index
272
     *
273
     * Can only be called when addFulltextField, addFilterField, addSortField and addAllFulltextFields have not
274
     * yet been called for this index instance
275
     *
276
     * @throws Exception
277
     * @param string $class - The class to include
278
     * @param array $options - TODO: Remove
279
     */
280
    public function addClass($class, $options = array())
281
    {
282
        if ($this->fulltextFields || $this->filterFields || $this->sortFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sortFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->fulltextFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->filterFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
283
            throw new Exception('Can\'t add class to Index after fields have already been added');
284
        }
285
286
        $options = array_merge(array(
287
            'include_children' => true
288
        ), $options);
289
290
        $this->classes[$class] = $options;
291
    }
292
293
    /**
294
     * Get the classes added by addClass
295
     */
296
    public function getClasses()
297
    {
298
        return $this->classes;
299
    }
300
301
    /**
302
     * Add a field that should be fulltext searchable
303
     * @param string $field - The field to add
304
     * @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
305
     * @param string $extraOptions - Dependent on search implementation
306
     */
307
    public function addFulltextField($field, $forceType = null, $extraOptions = array())
308
    {
309
        $this->fulltextFields = array_merge($this->fulltextFields, $this->fieldData($field, $forceType, $extraOptions));
310
    }
311
312
    public function getFulltextFields()
313
    {
314
        return $this->fulltextFields;
315
    }
316
317
    /**
318
     * Add a field that should be filterable
319
     * @param string $field - The field to add
320
     * @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
321
     * @param string $extraOptions - Dependent on search implementation
322
     */
323
    public function addFilterField($field, $forceType = null, $extraOptions = array())
324
    {
325
        $this->filterFields = array_merge($this->filterFields, $this->fieldData($field, $forceType, $extraOptions));
326
    }
327
328
    public function getFilterFields()
329
    {
330
        return $this->filterFields;
331
    }
332
333
    /**
334
     * Add a field that should be sortable
335
     * @param string $field - The field to add
336
     * @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
337
     * @param string $extraOptions - Dependent on search implementation
338
     */
339
    public function addSortField($field, $forceType = null, $extraOptions = array())
340
    {
341
        $this->sortFields = array_merge($this->sortFields, $this->fieldData($field, $forceType, $extraOptions));
342
    }
343
344
    public function getSortFields()
345
    {
346
        return $this->sortFields;
347
    }
348
349
    /**
350
     * Add all database-backed text fields as fulltext searchable fields.
351
     *
352
     * For every class included in the index, examines those classes and all subclasses looking for "Text" database
353
     * fields (Varchar, Text, HTMLText, etc) and adds them all as fulltext searchable fields.
354
     */
355
    public function addAllFulltextFields($includeSubclasses = true)
356
    {
357
        foreach ($this->getClasses() as $class => $options) {
358
            $classHierarchy = SearchIntrospection::hierarchy($class, $includeSubclasses, true);
359
360
            foreach ($classHierarchy as $dataClass) {
361
                $fields = DataObject::getSchema()->databaseFields($dataClass);
362
363
                foreach ($fields as $field => $type) {
364
                    list($type, $args) = ClassInfo::parse_class_spec($type);
365
366
                    /** @var DBField $object */
367
                    $object = Injector::inst()->get($type, false, ['Name' => 'test']);
368
                    if ($object instanceof DBString) {
369
                        $this->addFulltextField($field);
370
                    }
371
                }
372
            }
373
        }
374
    }
375
376
    /**
377
     * Returns an interator that will let you interate through all added fields, regardless of whether they
378
     * were added as fulltext, filter or sort fields.
379
     *
380
     * @return MultipleArrayIterator
381
     */
382
    public function getFieldsIterator()
383
    {
384
        return new MultipleArrayIterator($this->fulltextFields, $this->filterFields, $this->sortFields);
385
    }
386
387
    public function excludeVariantState($state)
388
    {
389
        $this->excludedVariantStates[] = $state;
390
    }
391
392
    /** Returns true if some variant state should be ignored */
393
    public function variantStateExcluded($state)
394
    {
395
        foreach ($this->excludedVariantStates as $excludedstate) {
396
            $matches = true;
397
398
            foreach ($excludedstate as $variant => $variantstate) {
399
                if (!isset($state[$variant]) || $state[$variant] != $variantstate) {
400
                    $matches = false;
401
                    break;
402
                }
403
            }
404
405
            if ($matches) {
406
                return true;
407
            }
408
        }
409
    }
410
411
    public $dependancyList = array();
412
413
    public function buildDependancyList()
414
    {
415
        $this->dependancyList = array_keys($this->getClasses());
416
417
        foreach ($this->getFieldsIterator() as $name => $field) {
418
            if (!isset($field['class'])) {
419
                continue;
420
            }
421
            SearchIntrospection::add_unique_by_ancestor($this->dependancyList, $field['class']);
422
        }
423
    }
424
425
    public $derivedFields = null;
426
427
    /**
428
     * Returns an array where each member is all the fields and the classes that are at the end of some
429
     * specific lookup chain from one of the base classes
430
     */
431
    public function getDerivedFields()
432
    {
433
        if ($this->derivedFields === null) {
434
            $this->derivedFields = array();
435
436
            foreach ($this->getFieldsIterator() as $name => $field) {
437
                if (count($field['lookup_chain']) < 2) {
438
                    continue;
439
                }
440
441
                $key = sha1($field['base'] . serialize($field['lookup_chain']));
442
                $fieldname = "{$field['class']}:{$field['field']}";
443
444
                if (isset($this->derivedFields[$key])) {
445
                    $this->derivedFields[$key]['fields'][$fieldname] = $fieldname;
446
                    SearchIntrospection::add_unique_by_ancestor($this->derivedFields['classes'], $field['class']);
447
                } else {
448
                    $chain = array_reverse($field['lookup_chain']);
449
                    array_shift($chain);
450
451
                    $this->derivedFields[$key] = array(
452
                        'base' => $field['base'],
453
                        'fields' => array($fieldname => $fieldname),
454
                        'classes' => array($field['class']),
455
                        'chain' => $chain
456
                    );
457
                }
458
            }
459
        }
460
461
        return $this->derivedFields;
462
    }
463
464
    /**
465
     * Get the "document ID" (a database & variant unique id) given some "Base" class, DataObject ID and state array
466
     *
467
     * @param string $base - The base class of the object
468
     * @param integer $id - The ID of the object
469
     * @param array $state - The variant state of the object
470
     * @return string - The document ID as a string
471
     */
472
    public function getDocumentIDForState($base, $id, $state)
473
    {
474
        ksort($state);
475
        $parts = array('id' => $id, 'base' => $base, 'state' => json_encode($state));
476
        return implode('-', array_values($parts));
477
    }
478
479
    /**
480
     * Get the "document ID" (a database & variant unique id) given some "Base" class and DataObject
481
     *
482
     * @param DataObject $object - The object
483
     * @param string $base - The base class of the object
484
     * @param boolean $includesubs - TODO: Probably going away
485
     * @return string - The document ID as a string
486
     */
487
    public function getDocumentID($object, $base, $includesubs)
488
    {
489
        return $this->getDocumentIDForState($base, $object->ID, SearchVariant::current_state($base, $includesubs));
490
    }
491
492
    /**
493
     * Given an object and a field definition (as returned by fieldData) get the current value of that field on that object
494
     *
495
     * @param DataObject $object - The object to get the value from
496
     * @param array $field - The field definition to use
497
     * @return mixed - The value of the field, or null if we couldn't look it up for some reason
498
     */
499
    protected function _getFieldValue($object, $field)
500
    {
501
        $errorHandler = function ($no, $str) {
502
            throw new Exception('HTML Parse Error: ' . $str);
503
        };
504
        set_error_handler($errorHandler, E_ALL);
505
506
        try {
507
            foreach ($field['lookup_chain'] as $step) {
508
                // Just fail if we've fallen off the end of the chain
509
                if (!$object) {
510
                    return null;
511
                }
512
513
                // If we're looking up this step on an array or SS_List, do the step on every item, merge result
514
                if (is_array($object) || $object instanceof SS_List) {
515
                    $next = array();
516
517
                    foreach ($object as $item) {
518
                        if ($step['call'] == 'method') {
519
                            $method = $step['method'];
520
                            $item = $item->$method();
521
                        } else {
522
                            $property = $step['property'];
523
                            $item = $item->$property;
524
                        }
525
526
                        if ($item instanceof SS_List) {
527
                            $next = array_merge($next, $item->toArray());
528
                        } elseif (is_array($item)) {
529
                            $next = array_merge($next, $item);
530
                        } else {
531
                            $next[] = $item;
532
                        }
533
                    }
534
535
                    $object = $next;
536
                } else {
537
                    // Otherwise, just call
538
                    if ($step['call'] == 'method') {
539
                        $method = $step['method'];
540
                        $object = $object->$method();
541
                    } elseif ($step['call'] == 'variant') {
542
                        $variants = SearchVariant::variants();
543
                        $variant = $variants[$step['variant']];
544
                        $method = $step['method'];
545
                        $object = $variant->$method($object);
546
                    } else {
547
                        $property = $step['property'];
548
                        $object = $object->$property;
549
                    }
550
                }
551
            }
552
        } catch (Exception $e) {
553
            static::warn($e);
554
            $object = null;
555
        }
556
557
        restore_error_handler();
558
        return $object;
559
    }
560
561
    /**
562
     * Log non-fatal errors
563
     *
564
     * @param Exception $e
565
     */
566
    public static function warn($e)
567
    {
568
        Injector::inst()->get(LoggerInterface::class)->info($e);
569
    }
570
571
    /**
572
     * Given a class, object id, set of stateful ids and a list of changed fields (in a special format),
573
     * return what statefulids need updating in this index
574
     *
575
     * Internal function used by SearchUpdater.
576
     *
577
     * @param  string $class
578
     * @param  int $id
579
     * @param  array $statefulids
580
     * @param  array $fields
581
     * @return array
582
     */
583
    public function getDirtyIDs($class, $id, $statefulids, $fields)
584
    {
585
        $dirty = array();
586
587
        // First, if this object is directly contained in the index, add it
588
        foreach ($this->classes as $searchclass => $options) {
589
            if ($searchclass == $class || ($options['include_children'] && is_subclass_of($class, $searchclass))) {
590
                $base = DataObject::getSchema()->baseDataClass($searchclass);
591
                $dirty[$base] = array();
592
                foreach ($statefulids as $statefulid) {
593
                    $key = serialize($statefulid);
594
                    $dirty[$base][$key] = $statefulid;
595
                }
596
            }
597
        }
598
599
        $current = SearchVariant::current_state();
600
601
602
        // Then, for every derived field
603
        foreach ($this->getDerivedFields() as $derivation) {
604
            // If the this object is a subclass of any of the classes we want a field from
605
            if (!SearchIntrospection::is_subclass_of($class, $derivation['classes'])) {
606
                continue;
607
            }
608
            if (!array_intersect_key($fields, $derivation['fields'])) {
609
                continue;
610
            }
611
612
            foreach (SearchVariant::reindex_states($class, false) as $state) {
613
                SearchVariant::activate_state($state);
614
615
                $ids = array($id);
616
617
                foreach ($derivation['chain'] as $step) {
618
                    // Use TableName for queries
619
                    $tableName = DataObject::getSchema()->tableName($step['class']);
0 ignored issues
show
Unused Code introduced by
The assignment to $tableName is dead and can be removed.
Loading history...
620
621
                    if ($step['through'] == 'has_one') {
622
                        $ids = DataObject::get($step['class'])
623
                            ->filter($step['foreignkey'], $ids)
624
                            ->column('ID');
625
                    } elseif ($step['through'] == 'has_many') {
626
                        // foreignkey identifies a has_one column on the model linked via the has_many relation
627
                        $ids = DataObject::get($step['otherclass'])
628
                            ->filter('ID', $ids)
629
                            ->column($step['foreignkey']);
630
                    }
631
632
                    if (empty($ids)) {
633
                        break;
634
                    }
635
                }
636
637
                SearchVariant::activate_state($current);
638
639
                if ($ids) {
640
                    $base = $derivation['base'];
641
                    if (!isset($dirty[$base])) {
642
                        $dirty[$base] = array();
643
                    }
644
645
                    foreach ($ids as $rid) {
646
                        $statefulid = array('id' => $rid, 'state' => $state);
647
                        $key = serialize($statefulid);
648
                        $dirty[$base][$key] = $statefulid;
649
                    }
650
                }
651
            }
652
        }
653
654
        return $dirty;
655
    }
656
657
    /** !! These should be implemented by the full text search engine */
658
659
    abstract public function add($object);
660
    abstract public function delete($base, $id, $state);
661
662
    abstract public function commit();
663
664
    /** !! These should be implemented by the specific index */
665
666
    /**
667
     * Called during construction, this is the method that builds the structure.
668
     * Used instead of overriding __construct as we have specific execution order - code that has
669
     * to be run before _and/or_ after this.
670
     */
671
    abstract public function init();
672
}
673