Passed
Pull Request — release-11.5.x (#3193)
by Markus
41:15
created

DataUpdateHandler   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 523
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 67
eloc 160
dl 0
loc 523
rs 3.04
c 1
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A handlePageUpdate() 0 20 6
A removeFromIndexAndQueue() 0 3 1
A getSiteRepository() 0 3 1
C processRecord() 0 49 15
A applyRecordChangesToQueue() 0 14 4
A getSolrConfigurationFromPageId() 0 3 1
A __construct() 0 20 1
A getValidatedPid() 0 10 2
A getIsTranslationParentRecordEnabled() 0 4 1
A getGarbageHandler() 0 3 1
A handleContentElementDeletion() 0 11 2
A removeFromIndexAndQueueWhenItemInQueue() 0 7 2
A getConfigurationPageId() 0 21 6
A updateCanonicalPages() 0 5 2
A handleRecordUpdate() 0 4 1
A updatePageIdItems() 0 4 2
A processPageRecord() 0 18 3
A handleContentElementUpdate() 0 8 2
A handleVersionSwap() 0 15 6
A getRecordRootPageIds() 0 9 2
A handleMovedPage() 0 3 1
A handleMovedRecord() 0 8 2
A applyPageChangesToQueue() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like DataUpdateHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataUpdateHandler, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types = 1);
2
namespace ApacheSolrForTypo3\Solr\Domain\Index\Queue\UpdateHandler;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2021 Markus Friedrich <[email protected]>
8
 *  All rights reserved
9
 *
10
 *  This script is part of the TYPO3 project. The TYPO3 project is
11
 *  free software; you can redistribute it and/or modify
12
 *  it under the terms of the GNU General Public License as published by
13
 *  the Free Software Foundation; either version 3 of the License, or
14
 *  (at your option) any later version.
15
 *
16
 *  The GNU General Public License can be found at
17
 *  http://www.gnu.org/copyleft/gpl.html.
18
 *
19
 *  This script is distributed in the hope that it will be useful,
20
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 *  GNU General Public License for more details.
23
 *
24
 *  This copyright notice MUST APPEAR in all copies of the script!
25
 ***************************************************************/
26
27
use TYPO3\CMS\Core\DataHandling\DataHandler;
28
use TYPO3\CMS\Backend\Utility\BackendUtility;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30
use ApacheSolrForTypo3\Solr\FrontendEnvironment;
31
use ApacheSolrForTypo3\Solr\IndexQueue\Queue;
32
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\RecordMonitor\Helper\RootPageResolver;
33
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\RecordMonitor\Helper\ConfigurationAwareRecordService;
34
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\RecordMonitor\Helper\MountPagesUpdater;
35
use ApacheSolrForTypo3\Solr\Domain\Site\SiteInterface;
36
use ApacheSolrForTypo3\Solr\Domain\Site\SiteRepository;
37
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
38
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
39
use ApacheSolrForTypo3\Solr\System\Records\Pages\PagesRepository;
40
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
41
use ApacheSolrForTypo3\Solr\Util;
42
43
44
/**
45
 * Data update handler
46
 *
47
 * Handles update on potential relevant records e.g.
48
 * an update might require index queue updates
49
 */
50
class DataUpdateHandler extends AbstractUpdateHandler
51
{
52
    /**
53
     * List of fields in the update field array that
54
     * are required for processing
55
     *
56
     * Note: For pages all fields except l10n_diffsource are
57
     *       kept, as additional fields can be configured in
58
     *       TypoScript, see AbstractDataUpdateEvent->_sleep.
59
     *
60
     * @var array
61
     */
62
    protected static $requiredUpdatedFields = [
63
        'pid',
64
    ];
65
66
    /**
67
     * Configuration used to check if recursive updates are required
68
     *
69
     * Holds the configuration when a recursive page queuing should be triggered, while processing record
70
     * updates
71
     *
72
     * Note: The SQL transaction is already committed, so the current state covers only "non"-changed fields.
73
     *
74
     * @var array
75
     */
76
    protected $updateSubPagesRecursiveTriggerConfiguration = [
77
        // the current page has the both fields "extendToSubpages" and "hidden" set from 1 to 0 => requeue subpages
78
        'HiddenAndExtendToSubpageWereDisabled' => [
79
            'changeSet' => [
80
                'hidden' => '0',
81
                'extendToSubpages' => '0'
82
            ]
83
        ],
84
        // the current page has the field "extendToSubpages" enabled and the field "hidden" was set to 0 => requeue subpages
85
        'extendToSubpageEnabledAndHiddenFlagWasRemoved' => [
86
            'currentState' =>  ['extendToSubpages' => '1'],
87
            'changeSet' => ['hidden' => '0']
88
        ],
89
        // the current page has the field "hidden" enabled and the field "extendToSubpages" was set to 0 => requeue subpages
90
        'hiddenIsEnabledAndExtendToSubPagesWasRemoved' => [
91
            'currentState' =>  ['hidden' => '1'],
92
            'changeSet' => ['extendToSubpages' => '0']
93
        ],
94
        // the field "no_search_sub_entries" of current page was set to 0
95
        'no_search_sub_entriesFlagWasAdded' => [
96
            'changeSet' => ['no_search_sub_entries' => '0']
97
        ],
98
    ];
99
100
    /**
101
     * @var MountPagesUpdater
102
     */
103
    protected $mountPageUpdater;
104
105
    /**
106
     * @var RootPageResolver
107
     */
108
    protected $rootPageResolver = null;
109
110
    /**
111
     * @var PagesRepository
112
     */
113
    protected $pagesRepository;
114
115
    /**
116
     * @var SolrLogManager
117
     */
118
    protected $logger = null;
119
120
    /**
121
     * @var DataHandler
122
     */
123
    protected $dataHandler;
124
125
    /**
126
     * @param ConfigurationAwareRecordService $recordService
127
     * @param FrontendEnvironment $frontendEnvironment
128
     * @param TCAService $tcaService
129
     * @param Queue $indexQueue
130
     * @param MountPagesUpdater $mountPageUpdater
131
     * @param RootPageResolver $rootPageResolver
132
     * @param PagesRepository $pagesRepository
133
     * @param SolrLogManager $solrLogManager
134
     * @param DataHandler $dataHandler
135
     */
136
    public function __construct(
137
        ConfigurationAwareRecordService $recordService ,
138
        FrontendEnvironment $frontendEnvironment,
139
        TCAService $tcaService,
140
        Queue $indexQueue,
141
        MountPagesUpdater $mountPageUpdater,
142
        RootPageResolver $rootPageResolver,
143
        PagesRepository $pagesRepository,
144
        DataHandler $dataHandler,
145
        SolrLogManager $solrLogManager = null
146
        ) {
147
            parent::__construct($recordService, $frontendEnvironment, $tcaService, $indexQueue);
148
149
            $this->mountPageUpdater = $mountPageUpdater;
150
            $this->rootPageResolver = $rootPageResolver;
151
            $this->pagesRepository = $pagesRepository;
152
            $this->dataHandler = $dataHandler;
153
            $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(
154
                SolrLogManager::class,
155
                /** @scrutinizer ignore-type */ __CLASS__
156
                );
157
    }
158
159
    /**
160
     * Handle content element update
161
     *
162
     * @param int $uid
163
     * @param array $updatedFields
164
     */
165
    public function handleContentElementUpdate(int $uid, array $updatedFields = []): void
166
    {
167
        $pid = $updatedFields['pid'] ?? $this->getValidatedPid('tt_content', $uid);
168
        if ($pid === null) {
169
            return;
170
        }
171
172
        $this->processPageRecord($pid, (int)$pid, $updatedFields);
173
    }
174
175
    /**
176
     * Handles the deletion of a content element
177
     *
178
     * @param int $uid
179
     */
180
    public function handleContentElementDeletion(int $uid): void
181
    {
182
        // @TODO: Should be checked, is possibly unnecessary as
183
        //        also done via GarbageCollector & PageStrategy
184
185
        $pid = $this->getValidatedPid('tt_content', $uid);
186
        if ($pid === null) {
187
            return;
188
        }
189
190
        $this->indexQueue->updateItem('pages', $pid, Util::getExectionTime());
191
    }
192
193
    /**
194
     * Handles page updates
195
     *
196
     * @param int $uid
197
     * @param array $updatedFields
198
     */
199
    public function handlePageUpdate(int $uid, array $updatedFields = []): void
200
    {
201
        try {
202
            if (isset($updatedFields['l10n_parent']) && intval($updatedFields['l10n_parent']) > 0) {
203
                $pid = $updatedFields['l10n_parent'];
204
            } elseif ($this->rootPageResolver->getIsRootPageId($uid)) {
205
                $pid = $uid;
206
            } else {
207
                $pid = $updatedFields['pid'] ?? $this->getValidatedPid('pages', $uid);
208
            }
209
        } catch (\Throwable $e) {
210
            $pid = null;
211
        }
212
213
        if ($pid === null) {
214
            $this->removeFromIndexAndQueueWhenItemInQueue('pages', $uid);
215
            return;
216
        }
217
218
        $this->processPageRecord($uid, (int)$pid, $updatedFields);
219
    }
220
221
    /**
222
     * Handles record updates
223
     *
224
     * @param int $uid
225
     * @param string $table
226
     */
227
    public function handleRecordUpdate(int $uid, string $table): void
228
    {
229
        $rootPageIds = $this->getRecordRootPageIds($table, $uid);
230
        $this->processRecord($table, $uid, $rootPageIds);
231
    }
232
233
    /**
234
     * Handles a version swap
235
     *
236
     * @param int $uid
237
     * @param string $table
238
     */
239
    public function handleVersionSwap(int $uid, string $table): void
240
    {
241
        $isPageRelatedRecord = ($table === 'tt_content' || $table === 'pages');
242
        if($isPageRelatedRecord) {
243
            $uid = ($table === 'tt_content' ? $this->getValidatedPid($table, $uid) : $uid);
244
            if ($uid === null) {
245
                return;
246
            }
247
            $this->applyPageChangesToQueue($uid);
248
        } else {
249
            $recordPageId = $this->getValidatedPid($table, $uid);
250
            if ($recordPageId === null) {
251
                return;
252
            }
253
            $this->applyRecordChangesToQueue($table, $uid, $recordPageId);
254
        }
255
    }
256
257
    /**
258
     * Handle page move
259
     *
260
     * @param int $uid
261
     */
262
    public function handleMovedPage(int $uid): void
263
    {
264
        $this->applyPageChangesToQueue($uid);
265
    }
266
267
    /**
268
     * Handle record move
269
     *
270
     * @param int $uid
271
     * @param string $table
272
     */
273
    public function handleMovedRecord(int $uid, string $table): void
274
    {
275
        $pid = $this->getValidatedPid($table, $uid);
276
        if ($pid === null) {
277
            return;
278
        }
279
280
        $this->applyRecordChangesToQueue($table, $uid, $pid);
281
    }
282
283
    /**
284
     * Adds a page to the queue and updates mounts, when it is enabled, otherwise ensure that the page is removed
285
     * from the queue.
286
     *
287
     * @param int $uid
288
     */
289
    protected function applyPageChangesToQueue(int $uid): void
290
    {
291
        $solrConfiguration = $this->getSolrConfigurationFromPageId($uid);
292
        $record = $this->configurationAwareRecordService->getRecord('pages', $uid, $solrConfiguration);
293
        if (!empty($record) && $this->tcaService->isEnabledRecord('pages', $record)) {
294
            $this->mountPageUpdater->update($uid);
295
            $this->indexQueue->updateItem('pages', $uid);
296
        } else {
297
            $this->removeFromIndexAndQueueWhenItemInQueue('pages', $uid);
298
        }
299
    }
300
301
    /**
302
     * Adds a record to the queue if it is monitored and enabled, otherwise it removes the record from the queue.
303
     *
304
     * @param string $table
305
     * @param int $uid
306
     * @param int $pid
307
     */
308
    protected function applyRecordChangesToQueue(string $table, int $uid, int $pid): void
309
    {
310
        $solrConfiguration = $this->getSolrConfigurationFromPageId($pid);
311
        $isMonitoredTable = $solrConfiguration->getIndexQueueIsMonitoredTable($table);
312
313
        if ($isMonitoredTable) {
314
            $record = $this->configurationAwareRecordService->getRecord($table, $uid, $solrConfiguration);
315
316
            if (!empty($record) && $this->tcaService->isEnabledRecord($table, $record)) {
317
                $uid = $this->tcaService->getTranslationOriginalUidIfTranslated($table, $record, $uid);
318
                $this->indexQueue->updateItem($table, $uid);
319
            } else {
320
                // TODO should be moved to garbage collector
321
                $this->removeFromIndexAndQueueWhenItemInQueue($table, $uid);
322
            }
323
        }
324
    }
325
326
    /**
327
     * Removes record from the index queue and from the solr index
328
     *
329
     * @param string $recordTable Name of table where the record lives
330
     * @param int $recordUid Id of record
331
     */
332
    protected function removeFromIndexAndQueue(string $recordTable, int $recordUid): void
333
    {
334
        $this->getGarbageHandler()->collectGarbage($recordTable, $recordUid);
335
    }
336
337
    /**
338
     * Removes record from the index queue and from the solr index when the item is in the queue.
339
     *
340
     * @param string $recordTable Name of table where the record lives
341
     * @param int $recordUid Id of record
342
     */
343
    protected function removeFromIndexAndQueueWhenItemInQueue(string $recordTable, int $recordUid): void
344
    {
345
        if (!$this->indexQueue->containsItem($recordTable, $recordUid)) {
346
            return;
347
        }
348
349
        $this->removeFromIndexAndQueue($recordTable, $recordUid);
350
    }
351
352
    /**
353
     * @param $pageId
354
     * @return TypoScriptConfiguration
355
     */
356
    protected function getSolrConfigurationFromPageId(int $pageId): TypoScriptConfiguration
357
    {
358
        return $this->frontendEnvironment->getSolrConfigurationFromPageId($pageId);
359
    }
360
361
    /**
362
     * Fetch record root page ids
363
     *
364
     * @param string $recordTable The table the record belongs to
365
     * @param int $recordUid
366
     * @return int[]
367
     */
368
    protected function getRecordRootPageIds(string $recordTable, int $recordUid): array
369
    {
370
        try {
371
            $rootPageIds = $this->rootPageResolver->getResponsibleRootPageIds($recordTable, $recordUid);
372
        } catch (\InvalidArgumentException $e) {
373
            $rootPageIds = [];
374
        }
375
376
        return $rootPageIds;
377
    }
378
379
    /**
380
     * Processes a page record
381
     *
382
     * Note: Also used if content element is updated, the page
383
     * of the content element is processed here
384
     *
385
     * @param int $uid
386
     * @param int $pid
387
     * @param array $updatedFields
388
     */
389
    protected function processPageRecord(int $uid, int $pid, array $updatedFields = []): void
390
    {
391
        $configurationPageId = $this->getConfigurationPageId('pages', (int)$pid, $uid);
392
        if ($configurationPageId === 0) {
393
            $this->mountPageUpdater->update($uid);
394
            return;
395
        }
396
        $rootPageIds = [$configurationPageId];
397
398
        $this->processRecord('pages', $uid, $rootPageIds);
399
400
        $this->updateCanonicalPages($uid);
401
        $this->mountPageUpdater->update($uid);
402
403
        $recursiveUpdateRequired = $this->isRecursivePageUpdateRequired($uid, $updatedFields);
404
        if ($recursiveUpdateRequired) {
405
            $treePageIds = $this->getSubPageIds($uid);
406
            $this->updatePageIdItems($treePageIds);
407
        }
408
    }
409
410
    /**
411
     * Process a record
412
     *
413
     * @param string $recordTable
414
     * @param int $recordUid
415
     * @param array $rootPageIds
416
     */
417
    protected function processRecord(string $recordTable, int $recordUid, array $rootPageIds): void
418
    {
419
        if (empty($rootPageIds)) {
420
            $this->removeFromIndexAndQueueWhenItemInQueue($recordTable, $recordUid);
421
            return;
422
        }
423
424
        foreach ($rootPageIds as $configurationPageId) {
425
            $site = $this->getSiteRepository()->getSiteByPageId($configurationPageId);
426
            if (!$site instanceof SiteInterface) {
427
                continue;
428
            }
429
            $solrConfiguration = $site->getSolrConfiguration();
430
            $isMonitoredRecord = $solrConfiguration->getIndexQueueIsMonitoredTable($recordTable);
431
            if (!$isMonitoredRecord) {
432
                // when it is a non monitored record, we can skip it.
433
                continue;
434
            }
435
436
            $record = $this->configurationAwareRecordService->getRecord($recordTable, $recordUid, $solrConfiguration);
437
            if (empty($record)) {
438
                // TODO move this part to the garbage collector
439
                // check if the item should be removed from the index because it no longer matches the conditions
440
                $this->removeFromIndexAndQueueWhenItemInQueue($recordTable, $recordUid);
441
                continue;
442
            }
443
            // Clear existing index queue items to prevent mount point duplicates.
444
            // This needs to be done before the overlay handling, because handling an overlay record should
445
            // not trigger a deletion.
446
            $isTranslation = !empty($record['sys_language_uid']) && $record['sys_language_uid'] !== 0;
447
            if ($recordTable === 'pages' && !$isTranslation) {
448
                $this->indexQueue->deleteItem('pages', $recordUid);
449
            }
450
451
            // The pages localized record can not consist without l10n_parent, so apply "free-content-mode" on records only.
452
            if ($recordTable === 'pages' || !$site->hasFreeContentModeLanguages() || !in_array($record['sys_language_uid'], $site->getFreeContentModeLanguages())) {
0 ignored issues
show
Bug introduced by
The method getFreeContentModeLanguages() does not exist on ApacheSolrForTypo3\Solr\Domain\Site\SiteInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ApacheSolrForTypo3\Solr\Domain\Site\SiteInterface. ( Ignorable by Annotation )

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

452
            if ($recordTable === 'pages' || !$site->hasFreeContentModeLanguages() || !in_array($record['sys_language_uid'], $site->/** @scrutinizer ignore-call */ getFreeContentModeLanguages())) {
Loading history...
Bug introduced by
The method hasFreeContentModeLanguages() does not exist on ApacheSolrForTypo3\Solr\Domain\Site\SiteInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ApacheSolrForTypo3\Solr\Domain\Site\SiteInterface. ( Ignorable by Annotation )

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

452
            if ($recordTable === 'pages' || !$site->/** @scrutinizer ignore-call */ hasFreeContentModeLanguages() || !in_array($record['sys_language_uid'], $site->getFreeContentModeLanguages())) {
Loading history...
453
                $recordUid = $this->tcaService->getTranslationOriginalUidIfTranslated($recordTable, $record, $recordUid);
454
            }
455
456
            // only update/insert the item if we actually found a record
457
            $isLocalizedRecord = $this->tcaService->isLocalizedRecord($recordTable, $record);
458
459
            if ($isLocalizedRecord && !$this->getIsTranslationParentRecordEnabled($recordTable, $recordUid)) {
460
                // we have a localized record without a visible parent record. Nothing to do.
461
                continue;
462
            }
463
464
            if ($this->tcaService->isEnabledRecord($recordTable, $record)) {
465
                $this->indexQueue->updateItem($recordTable, $recordUid);
466
            }
467
        }
468
    }
469
470
    /**
471
     * This method is used to determine the pageId that should be used to retrieve the index queue configuration.
472
     *
473
     * @param string $recordTable
474
     * @param int $recordPageId
475
     * @param int $recordUid
476
     * @return int
477
     */
478
    protected function getConfigurationPageId(string $recordTable, int $recordPageId, int $recordUid): int
479
    {
480
        $rootPageId = $this->rootPageResolver->getRootPageId($recordPageId);
481
        $rootPageRecord = BackendUtility::getRecord('pages', $rootPageId, '*');
482
        if (isset($rootPageRecord['sys_language_uid'])
483
            && (int)$rootPageRecord['sys_language_uid'] > 0
484
            && isset($rootPageRecord['l10n_parent'])
485
            && (int)$rootPageRecord['l10n_parent'] > 0
486
        ) {
487
            $rootPageId = $recordPageId = $rootPageRecord['l10n_parent'];
488
        }
489
        if ($this->rootPageResolver->getIsRootPageId($rootPageId)) {
490
            return $recordPageId;
491
        }
492
493
        $alternativeSiteRoots = $this->rootPageResolver->getAlternativeSiteRootPagesIds(
494
            $recordTable,
495
            $recordUid,
496
            $recordPageId
497
        );
498
        return (int)array_pop($alternativeSiteRoots);
499
    }
500
501
    /**
502
     * Checks if the parent record of the translated record is enabled.
503
     *
504
     * @param string $recordTable
505
     * @param int $recordUid
506
     * @return bool
507
     */
508
    protected function getIsTranslationParentRecordEnabled(string $recordTable, int $recordUid): bool
509
    {
510
        $l10nParentRecord = (array)BackendUtility::getRecord($recordTable, $recordUid, '*', '', false);
511
        return $this->tcaService->isEnabledRecord($recordTable, $l10nParentRecord);
512
    }
513
514
    /**
515
     * Applies the updateItem instruction on a collection of pageIds.
516
     *
517
     * @param array $treePageIds
518
     */
519
    protected function updatePageIdItems(array $treePageIds): void
520
    {
521
        foreach ($treePageIds as $treePageId) {
522
            $this->indexQueue->updateItem('pages', $treePageId);
523
        }
524
    }
525
526
    /**
527
     * Triggers Index Queue updates for other pages showing content from the
528
     * page currently being updated.
529
     *
530
     * @param int $pageId UID of the page currently being updated
531
     */
532
    protected function updateCanonicalPages(int $pageId): void
533
    {
534
        $canonicalPages = $this->pagesRepository->findPageUidsWithContentsFromPid((int)$pageId);
535
        foreach ($canonicalPages as $page) {
536
            $this->indexQueue->updateItem('pages', $page['uid']);
537
        }
538
    }
539
540
    /**
541
     * Retrieves the pid of a record, returns null if no pid could be found
542
     *
543
     * @param string $table
544
     * @param int $uid
545
     * @return int|null
546
     */
547
    protected function getValidatedPid(string $table, int $uid): ?int
548
    {
549
        $pid = $this->dataHandler->getPID($table, $uid);
550
        if ($pid === false) {
551
            $message = 'Record without valid pid was processed ' . $table . ':' . $uid;
552
            $this->logger->log(SolrLogManager::WARNING, $message);
553
            return null;
554
        }
555
556
        return (int)$pid;
557
    }
558
559
    /**
560
     * @return GarbageHandler
561
     */
562
    protected function getGarbageHandler(): GarbageHandler
563
    {
564
        return GeneralUtility::makeInstance(GarbageHandler::class);
565
    }
566
567
    /**
568
     * @return SiteRepository
569
     */
570
    protected function getSiteRepository(): SiteRepository
571
    {
572
        return GeneralUtility::makeInstance(SiteRepository::class);
573
    }
574
}
575