Passed
Pull Request — task/3376-TYPO3_12_compatibili... (#3473)
by Rafael
42:05
created

AbstractUpdateHandler::getQueryGenerator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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\Utility\GeneralUtility;
29
30
/**
31
 * Abstract update handler
32
 *
33
 * Base class for Handling updates or deletions on potential
34
 * relevant records
35
 */
36
abstract class AbstractUpdateHandler
37
{
38
    /**
39
     * List of fields in the update field array that
40
     * are required for processing
41
     *
42
     * Note: For pages all fields except l10n_diffsource are
43
     *       kept, as additional fields can be configured in
44
     *       TypoScript, see AbstractDataUpdateEvent->__sleep.
45
     *
46
     * @var array
47
     */
48
    protected static array $requiredUpdatedFields = [];
49
50
    /**
51
     * Configuration used to check if recursive updates are required
52
     *
53
     * Update handlers may need to determine which update combination
54
     * require a recursive change.
55
     *
56
     * The structure needs to be:
57
     *
58
     * [
59
     *      [
60
     *           'currentState' => ['fieldName1' => 'value1'],
61
     *           'changeSet' => ['fieldName1' => 'value1']
62
     *      ]
63
     * ]
64
     *
65
     * When the all values of the currentState AND all values of the changeSet match, a recursive update
66
     * will be triggered.
67
     *
68
     * @var array
69
     */
70
    protected array $updateSubPagesRecursiveTriggerConfiguration = [];
71
72
    /**
73
     * @var ConfigurationAwareRecordService
74
     */
75
    protected ConfigurationAwareRecordService $configurationAwareRecordService;
76
77
    /**
78
     * @var FrontendEnvironment
79
     */
80
    protected FrontendEnvironment $frontendEnvironment;
81
82
    /**
83
     * @var TCAService
84
     */
85
    protected TCAService $tcaService;
86
87
    /**
88
     * @var Queue
89
     */
90
    protected Queue $indexQueue;
91
92
    /**
93
     * @var PagesRepository|null
94
     */
95
    protected ?PagesRepository $pagesRepository;
96
97
    /**
98
     * @var QueryBuilder[]
99
     */
100
    protected array $queryBuilders = [];
101
102
    /**
103
     * @param ConfigurationAwareRecordService $recordService
104
     * @param FrontendEnvironment $frontendEnvironment
105
     * @param TCAService $tcaService
106
     * @param Queue $indexQueue
107
     */
108
    public function __construct(
109
        ConfigurationAwareRecordService $recordService,
110
        FrontendEnvironment $frontendEnvironment,
111
        TCAService $tcaService,
112
        Queue $indexQueue
113
    ) {
114
        $this->configurationAwareRecordService = $recordService;
115
        $this->frontendEnvironment = $frontendEnvironment;
116
        $this->tcaService = $tcaService;
117
        $this->indexQueue = $indexQueue;
118
    }
119
120
    /**
121
     * Returns the required fields from the updated fields array
122
     *
123
     * @return array
124
     */
125
    public static function getRequiredUpdatedFields(): array
126
    {
127
        return static::$requiredUpdatedFields;
128
    }
129
130
    /**
131
     * Add required update field
132
     *
133
     * @param string $field
134
     */
135
    public static function addRequiredUpdatedField(string $field): void
136
    {
137
        static::$requiredUpdatedFields[] = $field;
138
    }
139
140
    /**
141
     * @return array
142
     */
143
    protected function getAllRelevantFieldsForCurrentState(): array
144
    {
145
        $allCurrentStateFieldnames = [];
146
147
        foreach ($this->getUpdateSubPagesRecursiveTriggerConfiguration() as $triggerConfiguration) {
148
            if (!isset($triggerConfiguration['currentState']) || !is_array($triggerConfiguration['currentState'])) {
149
                // when no "currentState" configuration for the trigger exists we can skip it
150
                continue;
151
            }
152
153
            // we collect the currentState fields to return a unique list of all fields
154
            $allCurrentStateFieldnames = array_merge(
155
                $allCurrentStateFieldnames,
156
                array_keys($triggerConfiguration['currentState'])
157
            );
158
        }
159
160
        return array_unique($allCurrentStateFieldnames);
161
    }
162
163
    /**
164
     * When the extend-to-subpages flag was set, we determine the affected subpages and return them.
165
     *
166
     * @param int $pageId
167
     * @return array
168
     */
169
    protected function getSubPageIds(int $pageId): array
170
    {
171
        // here we retrieve only the subpages of this page because the permission clause is not evaluated
172
        // on the root node.
173
        $permissionClause = ' 1 ' . $this->getPagesRepository()->getBackendEnableFields();
174
        $treePageIdList = $this->getPagesRepository()->getTreeList($pageId, 20, 0, $permissionClause);
175
        $treePageIds = array_map('intval', explode(',', $treePageIdList));
176
177
        // the first one can be ignored because this is the page itself
178
        array_shift($treePageIds);
179
180
        return $treePageIds;
181
    }
182
183
    /**
184
     * Checks if a page update will trigger a recursive update of pages
185
     *
186
     * This can either be the case if some $changedFields are part of the RecursiveUpdateTriggerConfiguration or
187
     * columns have explicitly been configured via plugin.tx_solr.index.queue.recursiveUpdateFields
188
     *
189
     * @param int $pageId
190
     * @param array $updatedFields
191
     * @return bool
192
     * @throws DBALDriverException
193
     */
194
    protected function isRecursivePageUpdateRequired(int $pageId, array $updatedFields): bool
195
    {
196
        // First check RecursiveUpdateTriggerConfiguration
197
        $isRecursiveUpdateRequired = $this->isRecursiveUpdateRequired($pageId, $updatedFields);
198
        // If RecursiveUpdateTriggerConfiguration is false => check if changeFields are part of recursiveUpdateFields
199
        if ($isRecursiveUpdateRequired === false) {
200
            $solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId($pageId);
201
            $indexQueueConfigurationName = $this->configurationAwareRecordService->getIndexingConfigurationName(
202
                'pages',
203
                $pageId,
204
                $solrConfiguration
205
            );
206
            if ($indexQueueConfigurationName === null) {
207
                return false;
208
            }
209
            $updateFields = $solrConfiguration->getIndexQueueConfigurationRecursiveUpdateFields(
210
                $indexQueueConfigurationName
211
            );
212
213
            // Check if no additional fields have been defined and then skip recursive update
214
            if (empty($updateFields)) {
215
                return false;
216
            }
217
            // If the recursiveUpdateFields configuration is not part of the $changedFields skip recursive update
218
            if (!array_intersect_key($updatedFields, $updateFields)) {
219
                return false;
220
            }
221
        }
222
223
        return true;
224
    }
225
226
    /**
227
     * @param int $pageId
228
     * @param array $updatedFields
229
     * @return bool
230
     */
231
    protected function isRecursiveUpdateRequired(int $pageId, array $updatedFields): bool
232
    {
233
        $fieldsForCurrentState = $this->getAllRelevantFieldsForCurrentState();
234
        $fieldListToRetrieve = implode(',', $fieldsForCurrentState);
235
        $page = $this->getPagesRepository()->getPage($pageId, $fieldListToRetrieve, '', false);
236
        foreach ($this->getUpdateSubPagesRecursiveTriggerConfiguration() as $triggerConfiguration) {
237
            $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

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