Passed
Push — master ( 4af294...16bfce )
by Nicolaas
04:04
created

SearchApi::doReplacement()   C

Complexity

Conditions 12
Paths 3

Size

Total Lines 47
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 12
eloc 32
c 4
b 0
f 0
nc 3
nop 3
dl 0
loc 47
rs 6.9666

How to fix   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 Sunnysideup\SiteWideSearch\Api;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Core\Extensible;
9
use SilverStripe\Core\Injector\Injectable;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\ORM\ArrayList;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\ORM\DB;
14
use SilverStripe\ORM\FieldType\DBDatetime;
15
use SilverStripe\ORM\FieldType\DBField;
16
use SilverStripe\Security\LoginAttempt;
17
use SilverStripe\Security\MemberPassword;
18
use SilverStripe\Security\RememberLoginHash;
19
use SilverStripe\SessionManager\Models\LoginSession;
20
use SilverStripe\Versioned\ChangeSet;
21
use SilverStripe\Versioned\ChangeSetItem;
22
use SilverStripe\Versioned\Versioned;
23
use SilverStripe\View\ArrayData;
24
use Sunnysideup\SiteWideSearch\Helpers\FindClassesAndFields;
25
use Sunnysideup\SiteWideSearch\Helpers\FindEditableObjects;
26
27
class SearchApi
28
{
29
    use Extensible;
30
    use Configurable;
31
    use Injectable;
32
33
    protected $debug = false;
34
35
    protected $showReplacements = false;
36
37
    protected $isQuickSearch = false;
38
39
    protected $searchWholePhrase = false;
40
41
    protected $baseClass = DataObject::class;
42
43
    protected $quickSearchType = 'limited';
44
45
    protected $excludedClasses = [];
46
47
    protected $excludedClassesWithSubClassess = [];
48
49
    protected $includedClasses = [];
50
51
    protected $includedClassesWithSubClassess = [];
52
53
    protected $excludedFields = [];
54
55
    protected $includedFields = [];
56
57
    protected $includedClassFieldCombos = [];
58
59
    protected $defaultLists = [];
60
61
    protected $sortOverride;
62
63
    protected $words = [];
64
65
    protected $replace = '';
66
67
    private $objects = [];
68
69
    private static $limit_of_count_per_data_object = 999;
70
71
    private static $hours_back_for_recent = 48;
72
73
    private static $limit_per_class_for_recent = 5;
74
75
    private static $default_exclude_classes = [
76
        MemberPassword::class,
77
        LoginAttempt::class,
78
        ChangeSet::class,
79
        ChangeSetItem::class,
80
        RememberLoginHash::class,
81
        LoginSession::class,
82
        'SilverStripe\\UserForms\\Model\\Submission\\SubmittedFormField'
83
    ];
84
85
    private static $default_exclude_fields = [
86
        'ClassName',
87
        'LastEdited',
88
        'Created',
89
        'ID',
90
        'CanViewType',
91
        'CanEditType',
92
    ];
93
94
    private static $default_include_classes = [];
95
96
    private static $default_include_fields = [];
97
98
    private static $default_include_class_field_combos = [];
99
100
    private static $default_lists = [];
101
102
    public function setDebug(bool $b): SearchApi
103
    {
104
        $this->debug = $b;
105
106
        return $this;
107
    }
108
109
    public function setShowReplacements(bool $b): SearchApi
110
    {
111
        $this->showReplacements = $b;
112
113
        return $this;
114
    }
115
116
    protected function getCache()
117
    {
118
        return FindClassesAndFields::inst($this->baseClass);
119
    }
120
121
    public function setQuickSearchType(string $nameOrType): SearchApi
122
    {
123
        if ($nameOrType === 'all') {
124
            $this->isQuickSearch = false;
125
            $this->quickSearchType = '';
126
        } elseif ($nameOrType === 'limited') {
127
            $this->isQuickSearch = true;
128
            $this->quickSearchType = '';
129
        } elseif (class_exists($nameOrType)) {
130
            $this->quickSearchType = $nameOrType;
131
            $object = Injector::inst()->get($nameOrType);
132
            $this->setIncludedClasses($object->getClassesToSearch());
133
            $this->setIncludedFields($object->getFieldsToSearch());
134
            $this->setIncludedClassFieldCombos($object->getIncludedClassFieldCombos());
135
            $this->setDefaultLists($object->getDefaultLists());
136
            $this->setSortOverride($object->getSortOverride());
137
        } else {
138
            user_error('QuickSearchType must be either "all" or "limited" or a defined quick search class. Provided was: ' . $nameOrType);
139
        }
140
141
        return $this;
142
    }
143
144
    public function setIsQuickSearch(bool $b): SearchApi
145
    {
146
        $this->isQuickSearch = $b;
147
148
        return $this;
149
    }
150
151
    public function setSearchWholePhrase(bool $b): SearchApi
152
    {
153
        $this->searchWholePhrase = $b;
154
155
        return $this;
156
    }
157
158
    public function setBaseClass(string $class): SearchApi
159
    {
160
        if (class_exists($class)) {
161
            $this->baseClass = $class;
162
        }
163
164
        return $this;
165
    }
166
167
    public function setExcludedClasses(array $a): SearchApi
168
    {
169
        $this->excludedClasses = $a;
170
171
        return $this;
172
    }
173
174
    public function setIncludedClasses(array $a): SearchApi
175
    {
176
        $this->includedClasses = $a;
177
        return $this;
178
    }
179
180
    public function setExcludedFields(array $a): SearchApi
181
    {
182
        $this->excludedFields = $a;
183
184
        return $this;
185
    }
186
187
    public function setIncludedFields(array $a): SearchApi
188
    {
189
        $this->includedFields = $a;
190
191
        return $this;
192
    }
193
194
    public function setIncludedClassFieldCombos(array $a): SearchApi
195
    {
196
        $this->includedClassFieldCombos = $a;
197
198
        return $this;
199
    }
200
201
    public function setDefaultLists(array $a): SearchApi
202
    {
203
        $this->defaultLists = $a;
204
205
        return $this;
206
    }
207
208
    public function setSortOverride(?array $a = null): SearchApi
209
    {
210
        $this->sortOverride = $a;
211
212
        return $this;
213
    }
214
215
    public function setWordsAsString(string $s): SearchApi
216
    {
217
        $s = $this->securityCheckInput($s);
218
        $this->words = explode(' ', $s);
219
220
        return $this;
221
    }
222
223
    // public function __construct()
224
    // {
225
    //     Environment::increaseTimeLimitTo(300);
226
    //     Environment::setMemoryLimitMax(-1);
227
    //     Environment::increaseMemoryLimitTo(-1);
228
    // }
229
230
    protected string $cacheHasBeenBuilt = '';
231
232
    public function buildCache(?string $word = ''): SearchApi
233
    {
234
        if ($this->cacheHasBeenBuilt !== $word) {
235
            $this->getLinksInner($word);
236
            $this->cacheHasBeenBuilt = $word;
237
        }
238
        return $this;
239
    }
240
241
    public function getLinks(?string $word = '', ?string $type = ''): ArrayList
242
    {
243
        return $this->getLinksInner($word, $type);
244
    }
245
246
    protected function getLinksInner(?string $word = '', ?string $type = ''): ArrayList
247
    {
248
        $this->initCache();
249
250
        //always do first ...
251
        $matches = $this->getMatches($word, $type);
252
253
        $list = $this->turnMatchesIntoList($matches);
254
255
        $this->saveCache();
256
        return $list;
257
    }
258
259
260
261
    public function doReplacementURL(string $word, string $replace, ?bool $isURL = false): int
0 ignored issues
show
Unused Code introduced by
The parameter $isURL is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

261
    public function doReplacementURL(string $word, string $replace, /** @scrutinizer ignore-unused */ ?bool $isURL = false): int

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
262
    {
263
        return $this->doReplacement($word, $replace, 'url');
264
    }
265
266
    public function doReplacement(string $word, string $replace, ?string $type = ''): int
267
    {
268
        $count = 0;
269
        if ($word !== '' && $word !== '0') {
270
            $this->buildCache($word);
271
            $replace = $this->securityCheckInput($replace);
272
            if (strpos('://', $word) !== false) {
273
                $type = 'url';
274
            }
275
            foreach ($this->objects as $item) {
276
                if ($item->canEdit()) {
277
                    $className = $item->ClassName;
278
                    $fields = $this->getAllValidFields($className);
279
                    foreach ($fields as $field) {
280
                        if (! $this->includeFieldTest($className, $field)) {
281
                            continue;
282
                        }
283
                        if ($type === 'url') {
284
                            $escapedFrom = preg_quote($word, '/');
285
                            // It replaces exact matches of $escapedFrom (with optional trailing slash) in $item->{$field} only if followed by space, quote, ?, #, or end of string, preserving the slash if present.
286
                            $new = preg_replace_callback(
287
                                '/\b' . $escapedFrom . '(\/?)(?=[\s"\']|\?|#|$)/',
288
                                fn($matches) => $replace . ($matches[1] ?? ''),
289
                                $item->{$field}
290
                            );
291
                        } else {
292
                            $new = str_replace($word, $replace, $item->{$field});
293
                        }
294
                        if ($new === $item->{$field}) {
295
                            continue;
296
                        }
297
                        ++$count;
298
                        $item->{$field} = $new;
299
                        $this->writeAndPublishIfAppropriate($item);
300
                        if ($this->showReplacements) {
301
                            DB::alteration_message('.... .... ' . $item->ClassName . $item->ID . ' replace ' . $word . ' with ' . $replace . ' (' . $type . ') in field ' . $field, 'changed');
302
                        }
303
                    }
304
                } else {
305
                    if ($this->showReplacements) {
306
                        DB::alteration_message('.... .... ' . $item->ClassName . $item->ID . ' cannot be edited, so no replacement done', 'deleted');
307
                    }
308
                }
309
            }
310
        }
311
312
        return $count;
313
    }
314
315
    protected function saveCache(): self
316
    {
317
        $this->getCache()->saveCache();
318
319
        return $this;
320
    }
321
322
    protected function initCache(): self
323
    {
324
        $this->getCache()->initCache();
325
326
        return $this;
327
    }
328
329
    protected function writeAndPublishIfAppropriate($item)
330
    {
331
        if ($item->hasExtension(Versioned::class)) {
332
            $myStage = Versioned::get_stage();
333
            Versioned::set_stage(Versioned::DRAFT);
334
            // is it on live and is live the same as draft
335
            $canBePublished = $item->isPublished() && ! $item->isModifiedOnDraft();
336
            $item->writeToStage(Versioned::DRAFT);
337
            if ($canBePublished) {
338
                $item->publishSingle();
339
            }
340
            Versioned::set_stage($myStage);
341
        } else {
342
            $item->write();
343
        }
344
    }
345
346
    protected function getMatches(?string $word = '', ?string $type = ''): array
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

346
    protected function getMatches(?string $word = '', /** @scrutinizer ignore-unused */ ?string $type = ''): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
347
    {
348
        $startInner = 0;
349
        $startOuter = 0;
350
        if ($this->debug) {
351
            $startOuter = microtime(true);
352
        }
353
        $this->workOutInclusionsAndExclusions();
354
355
        // important to do this first
356
        if ($word) {
357
            $this->setWordsAsString($word);
358
        }
359
        $this->workOutWordsForSearching();
360
        if ($this->debug) {
361
            DB::alteration_message('Words searched for ' . print_r($this->words, 1));
0 ignored issues
show
Bug introduced by
Are you sure print_r($this->words, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

361
            DB::alteration_message('Words searched for ' . /** @scrutinizer ignore-type */ print_r($this->words, 1));
Loading history...
362
        }
363
364
        $array = [];
365
366
        if (count($this->words) > 0) {
367
            foreach ($this->getAllDataObjects() as $className) {
368
                if (! $this->includeClassTest($className)) {
369
                    continue;
370
                }
371
372
                $array[$className] = [];
373
                $fields = $this->getAllValidFields($className);
374
                $filterAny = [];
375
                foreach ($fields as $field) {
376
                    if (! $this->includeFieldTest($className, $field)) {
377
                        continue;
378
                    }
379
                    $filterAny[$field . ':PartialMatch'] = $this->words;
380
                    if ($this->debug) {
381
                        DB::alteration_message(' ... ... Searching in ' . $className . '.' . $field);
382
                    }
383
                }
384
                if ([] !== $filterAny) {
385
                    if ($this->debug) {
386
                        $startInner = microtime(true);
387
                        DB::alteration_message(' ... Filter: ' . implode(', ', array_keys($filterAny)));
388
                    }
389
                    $defaultList = $this->getDefaultList($className);
390
                    if ($defaultList === []) {
391
                        $array[$className] = $className::get();
392
                    }
393
                    $array[$className] = $array[$className]->filter(['ClassName' => $className]);
394
                    $array[$className] = $array[$className]
395
                        ->filterAny($filterAny)
396
                        ->limit($this->Config()->get('limit_of_count_per_data_object'))
397
                        ->columnUnique('ID');
398
                    if ($this->debug) {
399
                        $elaps = microtime(true) - $startInner;
400
                        DB::alteration_message('search for ' . $className . ' taken : ' . $elaps);
401
                    }
402
                }
403
404
                if ($this->debug) {
405
                    DB::alteration_message(' ... No fields in ' . $className);
406
                }
407
            }
408
        } else {
409
            $array = $this->getDefaultResults();
410
        }
411
412
        if ($this->debug) {
413
            $elaps = microtime(true) - $startOuter;
414
            DB::alteration_message('seconds taken find results: ' . $elaps);
415
        }
416
417
        return $array;
418
    }
419
420
    protected function getDefaultResults(): array
421
    {
422
        $back = $this->config()->get('hours_back_for_recent') ?: 24;
423
        $limit = $this->Config()->get('limit_per_class_for_recent') ?: 5;
424
        $threshold = strtotime('-' . $back . ' hours', DBDatetime::now()->getTimestamp());
425
        if (! $threshold) {
426
            $threshold = time() - 86400;
427
        }
428
429
        $array = [];
430
        $classNames = $this->getAllDataObjects();
431
        foreach ($classNames as $className) {
432
            if ($this->includeClassTest($className)) {
433
                $array[$className] = $className::get()
434
                    ->filter('LastEdited:GreaterThan', date('Y-m-d H:i:s', $threshold))
435
                    ->sort(['LastEdited' => 'DESC'])
436
                    ->limit($limit)
437
                    ->column('ID');
438
            }
439
        }
440
441
        return $array;
442
    }
443
444
    /**
445
     * weeds out doubles
446
     */
447
    protected function turnArrayIntoObjects(array $matches, ?int $limit = 0): array
448
    {
449
        $start = 0;
450
        $fullListCheck = [];
451
452
        if (empty($this->objects)) {
453
            if ($limit === null || $limit === 0) {
454
                $limit = (int) $this->Config()->get('limit_of_count_per_data_object');
455
            }
456
457
            $this->objects = [];
458
            if ($this->debug) {
459
                DB::alteration_message('number of classes: ' . count($matches));
460
            }
461
462
            foreach ($matches as $className => $ids) {
463
                if ($this->debug) {
464
                    $start = microtime(true);
465
                    DB::alteration_message(' ... number of matches for : ' . $className . ': ' . count($ids));
466
                }
467
468
                if (count($ids) > 0) {
469
                    $className = (string) $className;
470
                    $items = $className::get()
471
                        ->filter(['ID' => $ids, 'ClassName' => $className])
472
                        ->limit($limit);
473
                    foreach ($items as $item) {
474
                        if (isset($fullListCheck[$item->ClassName][$item->ID])) {
475
                            continue;
476
                        }
477
                        if ($item->canView() || 1 === 1) {
478
                            $fullListCheck[$item->ClassName][$item->ID] = true;
479
                            $this->objects[] = $item;
480
                        } else {
481
                            $fullListCheck[$item->ClassName][$item->ID] = false;
482
                        }
483
                    }
484
                }
485
486
                if ($this->debug) {
487
                    $elaps = microtime(true) - $start;
488
                    DB::alteration_message('seconds taken to find objects in: ' . $className . ': ' . $elaps);
489
                }
490
            }
491
        }
492
493
        return $this->objects;
494
    }
495
496
    protected function turnMatchesIntoList(array $matches): ArrayList
497
    {
498
        // helper
499
        //return values
500
        $list = ArrayList::create();
501
        $finder = Injector::inst()->get(FindEditableObjects::class);
502
        $finder->initCache(md5(serialize($this->excludedClassesWithSubClassess)))
503
            ->setExcludedClasses($this->excludedClassesWithSubClassess);
504
505
        $items = $this->turnArrayIntoObjects($matches);
506
        foreach ($items as $item) {
507
            if ($item->canView()) {
508
                $link = $finder->getLink($item);
509
                $cmsEditLink = trim($item->canEdit() ? $finder->getCMSEditLink($item) : '');
510
                $list->push(
511
                    ArrayData::create(
512
                        [
513
                            'HasLink' => (bool) $link,
514
                            'HasCMSEditLink' => (bool) $cmsEditLink,
515
                            'Link' => $link,
516
                            'CMSEditLink' => $cmsEditLink,
517
                            'ID' => $item->ID,
518
                            'LastEdited' => $item->LastEdited,
519
                            'Title' => $item->getTitle(),
520
                            'ClassName' => $item->ClassName,
521
                            'SingularName' => $item->i18n_singular_name(),
522
                            'SiteWideSearchSortValue' => $this->getSortValue($item),
523
                            'CMSThumbnail' => DBField::create_field('HTMLText', $finder->getCMSThumbnail($item)),
524
                        ]
525
                    )
526
                );
527
            }
528
        }
529
        $finder->saveCache();
530
531
        if (! empty($this->sortOverride)) {
532
            return $list->sort($this->sortOverride);
533
        } else {
534
            return $list->sort(['SiteWideSearchSortValue' => 'ASC']);
535
        }
536
    }
537
538
    protected function getSortValue($item)
539
    {
540
        $className = $item->ClassName;
541
        $fields = $this->getAllValidFields($className);
542
        $fullWords = implode(' ', $this->words);
543
544
        $done = false;
545
        $score = 0;
546
        if ($fullWords !== '' && $fullWords !== '0') {
547
            $fieldValues = [];
548
            $fieldValuesAll = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $fieldValuesAll is dead and can be removed.
Loading history...
549
            foreach ($fields as $field) {
550
                $fieldValues[$field] = strtolower(strip_tags((string) $item->{$field}));
551
            }
552
553
            $fieldValuesAll = implode(' ', $fieldValues);
554
            $testWords = array_merge(
555
                [$fullWords],
556
                $this->words
557
            );
558
            $testWords = array_unique($testWords);
559
            foreach ($testWords as $wordKey => $word) {
560
                //match a exact field to full words / one word
561
                $fullWords = ! (bool) $wordKey;
562
                if (false === $done) {
563
                    $count = 0;
564
                    foreach ($fieldValues as $fieldValue) {
565
                        ++$count;
566
                        if ($fieldValue === $word) {
567
                            $score += (int) $wordKey + $count;
568
                            $done = true;
569
570
                            break;
571
                        }
572
                    }
573
                }
574
575
                // the full string / any of the words are present?
576
                if (false === $done) {
577
                    $pos = strpos($fieldValuesAll, $word);
578
                    if (false !== $pos) {
579
                        $score += (($pos + 1) / strlen($word)) * 1000;
580
                        $done = true;
581
                    }
582
                }
583
584
                // all individual words are present
585
                if (false === $done && $fullWords) {
586
                    $score += 1000;
587
                    $allMatch = true;
588
                    foreach ($this->words as $tmpWord) {
589
                        $pos = strpos($fieldValuesAll, $tmpWord);
590
                        if (false === $pos) {
591
                            $allMatch = false;
592
593
                            break;
594
                        }
595
                    }
596
                    if ($allMatch) {
597
                        $done = true;
598
                    }
599
                }
600
            }
601
        }
602
603
        //the older the item, the higher the scoare
604
        //1104622247 = 1 jan 2005
605
        return $score + (1 / (strtotime($item->LastEdited) - 1104537600));
606
    }
607
608
    protected function workOutInclusionsAndExclusions()
609
    {
610
        $this->excludedClasses = array_unique(
611
            array_merge(
612
                $this->Config()->get('default_exclude_classes'),
613
                $this->excludedClasses
614
            )
615
        );
616
        $this->excludedClassesWithSubClassess = $this->includeSubClasses($this->excludedClasses);
617
        $this->includedClasses = array_unique(
618
            array_merge(
619
                $this->Config()->get('default_include_classes'),
620
                $this->includedClasses
621
            )
622
        );
623
        $this->includedClassesWithSubClassess = $this->includeSubClasses($this->includedClasses);
624
        $this->excludedFields = array_unique(
625
            array_merge(
626
                $this->Config()->get('default_exclude_fields'),
627
                $this->excludedFields
628
            )
629
        );
630
631
        $this->includedFields = array_unique(
632
            array_merge(
633
                $this->Config()->get('default_include_fields'),
634
                $this->includedFields
635
            )
636
        );
637
        $this->includedClassFieldCombos = array_unique(
638
            array_merge(
639
                $this->Config()->get('default_include_class_field_combos'),
640
                $this->includedClassFieldCombos
641
            )
642
        );
643
        $this->defaultLists = array_unique(
644
            array_merge(
645
                $this->Config()->get('default_lists'),
646
                $this->defaultLists
647
            )
648
        );
649
    }
650
651
    protected function workOutWordsForSearching()
652
    {
653
        if ($this->searchWholePhrase) {
654
            $this->words = [implode(' ', $this->words)];
655
        }
656
657
        if (count($this->words) === 0) {
658
            user_error('No word has been provided');
659
        }
660
661
        $this->words = array_map('trim', $this->words);
662
        $this->words = array_map('strtolower', $this->words);
663
        $this->words = array_unique($this->words);
664
        $this->words = array_filter($this->words);
665
    }
666
667
    protected function getAllDataObjects(): array
668
    {
669
        return $this->getCache()->getAllDataObjects();
670
    }
671
672
    protected function getAllValidFields(string $className): array
673
    {
674
        return $this->getCache()->getAllValidFields($className, $this->isQuickSearch, $this->includedFields, $this->includedClassFieldCombos);
675
    }
676
677
    protected function includeClassTest(string $className): bool
678
    {
679
        if (count($this->includedClassesWithSubClassess) && ! in_array($className, $this->includedClassesWithSubClassess, true)) {
680
            if ($this->debug) {
681
                DB::alteration_message(' ... Skipping as not included ' . $className);
682
            }
683
            return false;
684
        }
685
        if (count($this->excludedClassesWithSubClassess) && in_array($className, $this->excludedClassesWithSubClassess, true)) {
686
            if ($this->debug) {
687
                DB::alteration_message(' ... Skipping as excluded ' . $className);
688
            }
689
            return false;
690
        }
691
        if ($this->debug) {
692
            DB::alteration_message(' ... including ' . $className);
693
        }
694
695
        return true;
696
    }
697
698
    protected function includeFieldTest(string $className, string $field): bool
699
    {
700
        if (isset($this->includedClassFieldCombos[$className][$field])) {
701
            return true;
702
        } elseif (count($this->includedFields) > 0) {
703
            return in_array($field, $this->includedFields, true);
704
        } elseif (count($this->excludedFields) > 0) {
705
            return ! in_array($field, $this->includedFields, true);
706
        } else {
707
            return false;
708
        }
709
    }
710
711
    protected function includeSubClasses(array $classes): array
712
    {
713
        $toAdd = [];
714
        foreach ($classes as $class) {
715
            $toAdd = array_merge($toAdd, ClassInfo::subclassesFor($class, false));
716
        }
717
        return array_unique(array_merge($classes, $toAdd));
718
    }
719
720
    protected function securityCheckInput(string $word): string
721
    {
722
        $word = trim($word);
723
        return Convert::raw2sql($word);
724
    }
725
726
    protected function getDefaultList(string $className): array
727
    {
728
        return $this->defaultLists[$className] ?? [];
729
    }
730
}
731