Passed
Push — master ( 3cb69a...782ded )
by Nicolaas
03:32
created

SearchApi::getLinksInner()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 12
rs 10
1
<?php
2
3
namespace Sunnysideup\SiteWideSearch\Api;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Config\Configurable;
8
use SilverStripe\Core\Environment;
9
use SilverStripe\Core\Extensible;
10
use SilverStripe\Core\Injector\Injectable;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\ORM\ArrayList;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\DB;
15
use SilverStripe\ORM\FieldType\DBDatetime;
16
use SilverStripe\ORM\FieldType\DBField;
17
use SilverStripe\ORM\FieldType\DBString;
18
use SilverStripe\Security\LoginAttempt;
19
use SilverStripe\Security\MemberPassword;
20
use SilverStripe\Security\RememberLoginHash;
21
use SilverStripe\Versioned\ChangeSet;
22
use SilverStripe\Versioned\Versioned;
23
use SilverStripe\Versioned\ReadingMode;
24
use SilverStripe\Versioned\ChangeSetItem;
25
use SilverStripe\View\ArrayData;
26
27
use SilverStripe\SessionManager\Models\LoginSession;
28
use Sunnysideup\SiteWideSearch\Helpers\Cache;
29
use Sunnysideup\SiteWideSearch\Helpers\FindEditableObjects;
30
31
class SearchApi
32
{
33
    use Extensible;
34
    use Configurable;
35
    use Injectable;
36
37
    /**
38
     * @var string
39
     */
40
    private const CACHE_NAME = 'SearchApi';
41
42
    protected $debug = false;
43
44
    protected $isQuickSearch = false;
45
46
    protected $searchWholePhrase = false;
47
48
    protected $baseClass = DataObject::class;
49
50
    protected $quickSearchType = 'all';
51
52
    protected $excludedClasses = [];
53
54
    protected $includedClasses = [];
55
56
    protected $excludedFields = [];
57
58
    protected $includedFields = [];
59
60
    protected $includedClassFieldCombos = [];
61
62
    protected $sortOverride = null;
63
64
    protected $words = [];
65
66
    protected $replace = '';
67
68
    /**
69
     * format is as follows:
70
     * ```php
71
     *      [
72
     *          'AllDataObjects' => [
73
     *              'BaseClassUsed' => [
74
     *                  0 => ClassNameA,
75
     *                  1 => ClassNameB,
76
     *              ],
77
     *          ],
78
     *          'AllValidFields' => [
79
     *              'ClassNameA' => [
80
     *                  'FieldA' => 'FieldA'
81
     *              ],
82
     *          ],
83
     *          'IndexedFields' => [
84
     *              'ClassNameA' => [
85
     *                  0 => ClassNameA,
86
     *                  1 => ClassNameB,
87
     *              ],
88
     *          ],
89
     *          'ListOfTextClasses' => [
90
     *              0 => ClassNameA,
91
     *              1 => ClassNameB,
92
     *          ],
93
     *          'ValidFieldTypes' => [
94
     *              'Varchar(30)' => true,
95
     *              'Boolean' => false,
96
     *          ],
97
     *     ],
98
     * ```
99
     * we use true rather than false to be able to use empty to work out if it has been tested before.
100
     *
101
     * @var array
102
     */
103
    protected $cache = [];
104
105
    private $objects = [];
106
107
    private static $limit_of_count_per_data_object = 999;
108
109
    private static $hours_back_for_recent = 48;
110
111
    private static $limit_per_class_for_recent = 5;
112
113
    private static $default_exclude_classes = [
114
        MemberPassword::class,
115
        LoginAttempt::class,
116
        ChangeSet::class,
117
        ChangeSetItem::class,
118
        RememberLoginHash::class,
119
        LoginSession::class,
120
    ];
121
122
    private static $default_exclude_fields = [
123
        'ClassName',
124
        'LastEdited',
125
        'Created',
126
        'ID',
127
    ];
128
129
    private static $default_include_classes = [];
130
131
    private static $default_include_fields = [];
132
133
    private static $default_include_class_field_combos = [];
134
135
    public function setDebug(bool $b): SearchApi
136
    {
137
        $this->debug = $b;
138
139
        return $this;
140
    }
141
142
    public function setQuickSearchType(string $s): SearchApi
143
    {
144
        if($s === 'all') {
145
            $this->isQuickSearch = false;
146
            $this->quickSearchType = '';
147
        } elseif($s === 'limited') {
148
            $this->isQuickSearch = true;
149
            $this->quickSearchType = '';
150
        } elseif(class_exists($s)) {
151
            $this->quickSearchType = $s;
152
            $object = Injector::inst()->get($s);
153
            $this->setIncludedClasses($object->getClassesToSearch());
154
            $this->setIncludedFields($object->getFieldsToSearch());
155
            $this->setIncludedClassFieldCombos($object->getIncludedClassFieldCombos());
156
            $this->setSortOverride($object->getSortOverride());
157
        } else {
158
            user_error('QuickSearchType must be either "all" or "limited" or a defined quick search class. Provided was: ' . $s);
159
        }
160
161
        return $this;
162
    }
163
164
    public function setIsQuickSearch(bool $b): SearchApi
165
    {
166
        $this->isQuickSearch = $b;
167
168
        return $this;
169
    }
170
171
    public function setSearchWholePhrase(bool $b): SearchApi
172
    {
173
        $this->searchWholePhrase = $b;
174
175
        return $this;
176
    }
177
178
    public function setBaseClass(string $class): SearchApi
179
    {
180
        $this->baseClass = $class;
181
182
        return $this;
183
    }
184
185
    public function setExcludedClasses(array $a): SearchApi
186
    {
187
        $this->excludedClasses = $a;
188
189
        return $this;
190
    }
191
192
    public function setIncludedClasses(array $a): SearchApi
193
    {
194
        $this->includedClasses = $a;
195
        return $this;
196
    }
197
198
    public function setExcludedFields(array $a): SearchApi
199
    {
200
        $this->excludedFields = $a;
201
202
        return $this;
203
    }
204
205
    public function setIncludedFields(array $a): SearchApi
206
    {
207
        $this->includedFields = $a;
208
209
        return $this;
210
    }
211
212
    public function setIncludedClassFieldCombos(array $a): SearchApi
213
    {
214
        $this->includedClassFieldCombos = $a;
215
216
        return $this;
217
    }
218
219
    public function setSortOverride(?array $a = null): SearchApi
220
    {
221
        $this->sortOverride = $a;
222
223
        return $this;
224
    }
225
226
    public function setWordsAsString(string $s): SearchApi
227
    {
228
        $this->words = explode(' ', $s);
229
230
        return $this;
231
    }
232
233
    public function setWords(array $a): SearchApi
234
    {
235
        $this->words = array_combine($a, $a);
236
237
        return $this;
238
    }
239
240
    public function addWord(string $s): SearchApi
241
    {
242
        $this->words[$s] = $s;
243
244
        return $this;
245
    }
246
247
    public function getFileCache()
248
    {
249
        return Injector::inst()->get(Cache::class);
250
    }
251
252
    public function initCache(): self
253
    {
254
        $this->cache = $this->getFileCache()->getCacheValues(self::CACHE_NAME . '_' . $this->quickSearchType);
255
256
        return $this;
257
    }
258
259
    public function saveCache(): self
260
    {
261
        $this->getFileCache()->setCacheValues(self::CACHE_NAME . '_' . $this->quickSearchType, $this->cache);
262
263
        return $this;
264
    }
265
266
    // public function __construct()
267
    // {
268
    //     Environment::increaseTimeLimitTo(300);
269
    //     Environment::setMemoryLimitMax(-1);
270
    //     Environment::increaseMemoryLimitTo(-1);
271
    // }
272
273
    public function buildCache(?string $word = ''): SearchApi
274
    {
275
        $this->getLinksInner($word);
276
277
        return $this;
278
279
    }
280
281
    public function getLinks(?string $word = ''): ArrayList
282
    {
283
        return $this->getLinksInner($word);
284
    }
285
286
    protected function getLinksInner(?string $word = ''): ArrayList
287
    {
288
        $this->initCache();
289
290
        //always do first ...
291
        $matches = $this->getMatches($word);
292
293
        $list = $this->turnMatchesIntoList($matches);
294
295
        $this->saveCache();
296
297
        return $list;
298
299
    }
300
301
    public function doReplacement(string $word, string $replace): int
302
    {
303
        $this->initCache();
304
        $count = 0;
305
        // we should have these already.
306
        foreach ($this->objects as $item) {
307
            if ($item->canEdit()) {
308
                $fields = $this->getAllValidFields($item->ClassName);
309
                foreach ($fields as $field) {
310
                    $new = str_replace($word, $replace, $item->{$field});
311
                    if ($new !== $item->{$field}) {
312
                        ++$count;
313
                        $item->{$field} = $new;
314
                        $this->writeAndPublishIfAppropriate($item);
315
316
                        if ($this->debug) {
317
                            DB::alteration_message('<h2>Match:  ' . $item->ClassName . $item->ID . '</h2>' . $new . '<hr />');
318
                        }
319
                    }
320
                }
321
            }
322
        }
323
324
        return $count;
325
    }
326
327
    protected function writeAndPublishIfAppropriate($item)
328
    {
329
        if ($item->hasExtension(Versioned::class)) {
330
            $myStage = Versioned::get_stage();
331
            Versioned::set_stage(Versioned::DRAFT);
332
            // is it on live and is live the same as draft
333
            $canBePublished = $item->isPublished() && !$item->isModifiedOnDraft();
334
            $item->writeToStage(Versioned::DRAFT);
335
            if ($canBePublished) {
336
                $item->publishSingle();
337
            }
338
            Versioned::set_stage($myStage);
339
        } else {
340
            $item->write();
341
        }
342
343
    }
344
345
    protected function getMatches(?string $word = ''): array
346
    {
347
        $startInner = 0;
348
        $startOuter = 0;
349
        if ($this->debug) {
350
            $startOuter = microtime(true);
351
        }
352
        $this->workOutInclusionsAndExclusions();
353
        $this->workOutWords($word);
0 ignored issues
show
Bug introduced by
It seems like $word can also be of type null; however, parameter $word of Sunnysideup\SiteWideSear...archApi::workOutWords() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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