Passed
Pull Request — task/3376-TYPO3_12_compatibili... (#3454)
by
unknown
42:31
created

AbstractUpdateHandler   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 296
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 26
eloc 73
dl 0
loc 296
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getAllChangeSetValuesMatch() 0 9 2
A getAllCurrentStateFieldsMatch() 0 8 2
A isRecursivePageUpdateRequired() 0 30 5
A isRecursiveUpdateRequired() 0 16 4
A getUpdateSubPagesRecursiveTriggerConfiguration() 0 3 1
A getRequiredUpdatedFields() 0 3 1
A __construct() 0 10 1
A getAllRelevantFieldsForCurrentState() 0 18 4
A addRequiredUpdatedField() 0 3 1
A getSubPageIds() 0 16 1
A getQueryBuilderForTable() 0 7 2
A getPagesRepository() 0 7 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace ApacheSolrForTypo3\Solr\Domain\Index\Queue\UpdateHandler;
19
20
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\RecordMonitor\Helper\ConfigurationAwareRecordService;
21
use ApacheSolrForTypo3\Solr\FrontendEnvironment;
22
use ApacheSolrForTypo3\Solr\IndexQueue\Queue;
23
use ApacheSolrForTypo3\Solr\System\Records\Pages\PagesRepository;
24
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
25
use Doctrine\DBAL\Driver\Exception as DBALDriverException;
26
use TYPO3\CMS\Core\Database\ConnectionPool;
27
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
28
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30
31
/**
32
 * Abstract update handler
33
 *
34
 * Base class for Handling updates or deletions on potential
35
 * relevant records
36
 * @todo: Replace QueryGenerator
37
 */
38
abstract class AbstractUpdateHandler
39
{
40
    /**
41
     * List of fields in the update field array that
42
     * are required for processing
43
     *
44
     * Note: For pages all fields except l10n_diffsource are
45
     *       kept, as additional fields can be configured in
46
     *       TypoScript, see AbstractDataUpdateEvent->__sleep.
47
     *
48
     * @var array
49
     */
50
    protected static array $requiredUpdatedFields = [];
51
52
    /**
53
     * Configuration used to check if recursive updates are required
54
     *
55
     * Update handlers may need to determine which update combination
56
     * require a recursive change.
57
     *
58
     * The structure needs to be:
59
     *
60
     * [
61
     *      [
62
     *           'currentState' => ['fieldName1' => 'value1'],
63
     *           'changeSet' => ['fieldName1' => 'value1']
64
     *      ]
65
     * ]
66
     *
67
     * When the all values of the currentState AND all values of the changeSet match, a recursive update
68
     * will be triggered.
69
     *
70
     * @var array
71
     */
72
    protected array $updateSubPagesRecursiveTriggerConfiguration = [];
73
74
    /**
75
     * @var ConfigurationAwareRecordService
76
     */
77
    protected ConfigurationAwareRecordService $configurationAwareRecordService;
78
79
    /**
80
     * @var FrontendEnvironment
81
     */
82
    protected FrontendEnvironment $frontendEnvironment;
83
84
    /**
85
     * @var TCAService
86
     */
87
    protected TCAService $tcaService;
88
89
    /**
90
     * @var Queue
91
     */
92
    protected Queue $indexQueue;
93
94
    /**
95
     * @var PagesRepository|null
96
     */
97
    protected ?PagesRepository $pagesRepository;
98
99
    /**
100
     * @var QueryBuilder[]
101
     */
102
    protected array $queryBuilders = [];
103
104
    /**
105
     * @param ConfigurationAwareRecordService $recordService
106
     * @param FrontendEnvironment $frontendEnvironment
107
     * @param TCAService $tcaService
108
     * @param Queue $indexQueue
109
     */
110
    public function __construct(
111
        ConfigurationAwareRecordService $recordService,
112
        FrontendEnvironment $frontendEnvironment,
113
        TCAService $tcaService,
114
        Queue $indexQueue
115
    ) {
116
        $this->configurationAwareRecordService = $recordService;
117
        $this->frontendEnvironment = $frontendEnvironment;
118
        $this->tcaService = $tcaService;
119
        $this->indexQueue = $indexQueue;
120
    }
121
122
    /**
123
     * Returns the required fields from the updated fields array
124
     *
125
     * @return array
126
     */
127
    public static function getRequiredUpdatedFields(): array
128
    {
129
        return static::$requiredUpdatedFields;
130
    }
131
132
    /**
133
     * Add required update field
134
     *
135
     * @param string $field
136
     */
137
    public static function addRequiredUpdatedField(string $field): void
138
    {
139
        static::$requiredUpdatedFields[] = $field;
140
    }
141
142
    /**
143
     * @return array
144
     */
145
    protected function getAllRelevantFieldsForCurrentState(): array
146
    {
147
        $allCurrentStateFieldnames = [];
148
149
        foreach ($this->getUpdateSubPagesRecursiveTriggerConfiguration() as $triggerConfiguration) {
150
            if (!isset($triggerConfiguration['currentState']) || !is_array($triggerConfiguration['currentState'])) {
151
                // when no "currentState" configuration for the trigger exists we can skip it
152
                continue;
153
            }
154
155
            // we collect the currentState fields to return a unique list of all fields
156
            $allCurrentStateFieldnames = array_merge(
157
                $allCurrentStateFieldnames,
158
                array_keys($triggerConfiguration['currentState'])
159
            );
160
        }
161
162
        return array_unique($allCurrentStateFieldnames);
163
    }
164
165
    /**
166
     * When the extend-to-subpages flag was set, we determine the affected subpages and return them.
167
     *
168
     * @param int $pageId
169
     * @return array
170
     */
171
    protected function getSubPageIds(int $pageId): array
172
    {
173
        // here we retrieve only the subpages of this page because the permission clause is not evaluated
174
        // on the root node.
175
        $permissionClause = ' 1 ' . $this->getPagesRepository()->getBackendEnableFields();
0 ignored issues
show
Unused Code introduced by
The assignment to $permissionClause is dead and can be removed.
Loading history...
176
        $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
177
        $treePageIds = $pageRepository->getDescendantPageIdsRecursive(
178
            $pageId,
179
            20,
180
            0
181
        );
182
183
        // the first one can be ignored because this is the page itself
184
        array_shift($treePageIds);
185
186
        return $treePageIds;
187
    }
188
189
    /**
190
     * Checks if a page update will trigger a recursive update of pages
191
     *
192
     * This can either be the case if some $changedFields are part of the RecursiveUpdateTriggerConfiguration or
193
     * columns have explicitly been configured via plugin.tx_solr.index.queue.recursiveUpdateFields
194
     *
195
     * @param int $pageId
196
     * @param array $updatedFields
197
     * @return bool
198
     * @throws DBALDriverException
199
     */
200
    protected function isRecursivePageUpdateRequired(int $pageId, array $updatedFields): bool
201
    {
202
        // First check RecursiveUpdateTriggerConfiguration
203
        $isRecursiveUpdateRequired = $this->isRecursiveUpdateRequired($pageId, $updatedFields);
204
        // If RecursiveUpdateTriggerConfiguration is false => check if changeFields are part of recursiveUpdateFields
205
        if ($isRecursiveUpdateRequired === false) {
206
            $solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId($pageId);
207
            $indexQueueConfigurationName = $this->configurationAwareRecordService->getIndexingConfigurationName(
208
                'pages',
209
                $pageId,
210
                $solrConfiguration
211
            );
212
            if ($indexQueueConfigurationName === null) {
213
                return false;
214
            }
215
            $updateFields = $solrConfiguration->getIndexQueueConfigurationRecursiveUpdateFields(
216
                $indexQueueConfigurationName
217
            );
218
219
            // Check if no additional fields have been defined and then skip recursive update
220
            if (empty($updateFields)) {
221
                return false;
222
            }
223
            // If the recursiveUpdateFields configuration is not part of the $changedFields skip recursive update
224
            if (!array_intersect_key($updatedFields, $updateFields)) {
225
                return false;
226
            }
227
        }
228
229
        return true;
230
    }
231
232
    /**
233
     * @param int $pageId
234
     * @param array $updatedFields
235
     * @return bool
236
     */
237
    protected function isRecursiveUpdateRequired(int $pageId, array $updatedFields): bool
238
    {
239
        $fieldsForCurrentState = $this->getAllRelevantFieldsForCurrentState();
240
        $fieldListToRetrieve = implode(',', $fieldsForCurrentState);
241
        $page = $this->getPagesRepository()->getPage($pageId, $fieldListToRetrieve, '', false);
242
        foreach ($this->getUpdateSubPagesRecursiveTriggerConfiguration() as $triggerConfiguration) {
243
            $allCurrentStateFieldsMatch = $this->getAllCurrentStateFieldsMatch($triggerConfiguration, $page);
0 ignored issues
show
Bug introduced by
It seems like $page can also be of type null; however, parameter $pageRecord of ApacheSolrForTypo3\Solr\...rrentStateFieldsMatch() does only seem to accept array, 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

243
            $allCurrentStateFieldsMatch = $this->getAllCurrentStateFieldsMatch($triggerConfiguration, /** @scrutinizer ignore-type */ $page);
Loading history...
244
            $allChangeSetValuesMatch = $this->getAllChangeSetValuesMatch($triggerConfiguration, $updatedFields);
245
246
            $aMatchingTriggerHasBeenFound = $allCurrentStateFieldsMatch && $allChangeSetValuesMatch;
247
            if ($aMatchingTriggerHasBeenFound) {
248
                return true;
249
            }
250
        }
251
252
        return false;
253
    }
254
255
    /**
256
     * @param array $triggerConfiguration
257
     * @param array $pageRecord
258
     * @return bool
259
     */
260
    protected function getAllCurrentStateFieldsMatch(array $triggerConfiguration, array $pageRecord): bool
261
    {
262
        $triggerConfigurationHasNoCurrentStateConfiguration = !array_key_exists('currentState', $triggerConfiguration);
263
        if ($triggerConfigurationHasNoCurrentStateConfiguration) {
264
            return true;
265
        }
266
        $diff = array_diff_assoc($triggerConfiguration['currentState'], $pageRecord);
267
        return empty($diff);
268
    }
269
270
    /**
271
     * @param array $triggerConfiguration
272
     * @param array $changedFields
273
     * @return bool
274
     */
275
    protected function getAllChangeSetValuesMatch(array $triggerConfiguration, array $changedFields): bool
276
    {
277
        $triggerConfigurationHasNoChangeSetStateConfiguration = !array_key_exists('changeSet', $triggerConfiguration);
278
        if ($triggerConfigurationHasNoChangeSetStateConfiguration) {
279
            return true;
280
        }
281
282
        $diff = array_diff_assoc($triggerConfiguration['changeSet'], $changedFields);
283
        return empty($diff);
284
    }
285
286
    /**
287
     * The implementation of this method need to retrieve a configuration to determine which record data
288
     * and change combination required a recursive change.
289
     *
290
     * The structure needs to be:
291
     *
292
     * [
293
     *      [
294
     *           'currentState' => ['fieldName1' => 'value1'],
295
     *           'changeSet' => ['fieldName1' => 'value1']
296
     *      ]
297
     * ]
298
     *
299
     * When the all values of the currentState AND all values of the changeSet match, a recursive update
300
     * will be triggered.
301
     *
302
     * @return array
303
     */
304
    protected function getUpdateSubPagesRecursiveTriggerConfiguration(): array
305
    {
306
        return $this->updateSubPagesRecursiveTriggerConfiguration;
307
    }
308
309
    /**
310
     * @return PagesRepository
311
     */
312
    protected function getPagesRepository(): PagesRepository
313
    {
314
        if (!isset($this->pagesRepository)) {
315
            $this->pagesRepository = GeneralUtility::makeInstance(PagesRepository::class);
316
        }
317
318
        return $this->pagesRepository;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->pagesRepository could return the type null which is incompatible with the type-hinted return ApacheSolrForTypo3\Solr\...s\Pages\PagesRepository. Consider adding an additional type-check to rule them out.
Loading history...
319
    }
320
321
    /**
322
     * Returns the prepared QueryBuilder for given table
323
     *
324
     * @param string $table
325
     * @return QueryBuilder
326
     */
327
    protected function getQueryBuilderForTable(string $table): QueryBuilder
328
    {
329
        if (!isset($this->queryBuilders[$table])) {
330
            $this->queryBuilders[$table] = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
331
        }
332
333
        return $this->queryBuilders[$table];
334
    }
335
}
336