Passed
Pull Request — main (#3378)
by Mario
50:21 queued 18:39
created

AbstractInitializer::buildSelectStatement()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.0054

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 13
ccs 8
cts 9
cp 0.8889
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2.0054
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\IndexQueue\Initializer;
19
20
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\QueueItemRepository;
21
use ApacheSolrForTypo3\Solr\Domain\Site\Site;
22
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
23
use ApacheSolrForTypo3\Solr\System\Records\Pages\PagesRepository;
24
use Doctrine\DBAL\Driver\Exception as DBALDriverException;
25
use Doctrine\DBAL\Exception as DBALException;
26
use TYPO3\CMS\Backend\Utility\BackendUtility;
27
use TYPO3\CMS\Core\Database\ConnectionPool;
28
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
29
use TYPO3\CMS\Core\Messaging\FlashMessageService;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
32
/**
33
 * Abstract Index Queue initializer with implementation  of methods for common
34
 * needs during Index Queue initialization.
35
 *
36
 * @author Ingo Renner <[email protected]>
37
 */
38
abstract class AbstractInitializer implements IndexQueueInitializer
39
{
40
    /**
41
     * Site to initialize
42
     *
43
     * @var Site|null
44
     */
45
    protected ?Site $site;
46
47
    /**
48
     * The type of items this initializer is handling.
49
     *
50
     * @var string
51
     */
52
    protected string $type;
53
54
    /**
55
     * Index Queue configuration.
56
     *
57
     * @var array
58
     */
59
    protected array $indexingConfiguration = [];
60
61
    /**
62
     * Indexing configuration name.
63
     *
64
     * @var string
65
     */
66
    protected string $indexingConfigurationName;
67
68
    /**
69
     * Flash message queue
70
     *
71
     * @var FlashMessageQueue
72
     */
73
    protected FlashMessageQueue $flashMessageQueue;
74
75
    /**
76
     * @var SolrLogManager
77
     */
78
    protected SolrLogManager $logger;
79
80
    /**
81
     * @var QueueItemRepository
82
     */
83
    protected QueueItemRepository $queueItemRepository;
84
85
    /**
86
     * @var PagesRepository
87
     */
88
    protected PagesRepository $pagesRepository;
89
90
    /**
91
     * Constructor, prepares the flash message queue
92
     * @param QueueItemRepository|null $queueItemRepository
93
     */
94 14
    public function __construct(
95
        QueueItemRepository $queueItemRepository = null,
96
        PagesRepository $pagesRepository = null
97
    ) {
98 14
        $this->logger = GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
99 14
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
100 14
        $this->flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier('solr.queue.initializer');
101 14
        $this->queueItemRepository = $queueItemRepository ?? GeneralUtility::makeInstance(QueueItemRepository::class);
102 14
        $this->pagesRepository = $pagesRepository ?? GeneralUtility::makeInstance(PagesRepository::class);
103
    }
104
105
    /**
106
     * Sets the site for the initializer.
107
     *
108
     * @param Site $site The site to initialize Index Queue items for.
109
     */
110 14
    public function setSite(Site $site)
111
    {
112 14
        $this->site = $site;
113
    }
114
115
    /**
116
     * Set the type (usually a Db table name) of items to initialize.
117
     *
118
     * @param string $type Type to initialize.
119
     */
120
    public function setType(string $type)
121
    {
122
        $this->type = $type;
123
    }
124
125
    /**
126
     * Sets the configuration for how to index a type of items.
127
     *
128
     * @param array $indexingConfiguration Indexing configuration from TypoScript
129
     */
130 12
    public function setIndexingConfiguration(array $indexingConfiguration)
131
    {
132 12
        $this->indexingConfiguration = $indexingConfiguration;
133
    }
134
135
    /**
136
     * Sets the name of the indexing configuration to initialize.
137
     *
138
     * @param string $indexingConfigurationName Indexing configuration name
139
     */
140 14
    public function setIndexingConfigurationName(string $indexingConfigurationName)
141
    {
142 14
        $this->indexingConfigurationName = $indexingConfigurationName;
143
    }
144
145
    /**
146
     * Initializes Index Queue items for a certain site and indexing
147
     * configuration.
148
     *
149
     * @return bool TRUE if initialization was successful, FALSE on error.
150
     * @throws DBALDriverException
151
     */
152 12
    public function initialize(): bool
153
    {
154
        /** @var ConnectionPool $connectionPool */
155 12
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
156
157 12
        $fetchItemsQuery = $this->buildSelectStatement() . ', "" as errors '
158 12
            . 'FROM ' . $this->type . ' '
159
            . 'WHERE '
160 12
            . $this->buildPagesClause()
161 12
            . $this->buildTcaWhereClause()
162 12
            . $this->buildUserWhereClause();
163
164
        try {
165 12
            if ($connectionPool->getConnectionForTable($this->type)->getParams() === $connectionPool->getConnectionForTable('tx_solr_indexqueue_item')->getParams()) {
166
                // If both tables are in the same DB, send only one query to copy all datas from one table to the other
167 12
                $initializationQuery = 'INSERT INTO tx_solr_indexqueue_item (root, item_type, item_uid, indexing_configuration, indexing_priority, changed, errors) ' . $fetchItemsQuery;
168 12
                $logData = ['query' => $initializationQuery];
169 12
                $logData['rows'] = $this->queueItemRepository->initializeByNativeSQLStatement($initializationQuery);
170
            } else {
171
                // If tables are using distinct connections, start by fetching items matching criteria
172
                $logData = ['query' => $fetchItemsQuery];
173
                $items = $connectionPool->getConnectionForTable($this->type)->fetchAllAssociative($fetchItemsQuery);
174
                $logData['rows'] = count($items);
175
176
                if (count($items)) {
177
                    // Add items to the queue (if any)
178
                    $logData['rows'] = $connectionPool
179
                        ->getConnectionForTable('tx_solr_indexqueue_item')
180 12
                        ->bulkInsert('tx_solr_indexqueue_item', $items, array_keys($items[0]));
181
                }
182
            }
183
        } catch (DBALException $DBALException) {
184
            $logData['error'] = $DBALException->getCode() . ': ' . $DBALException->getMessage();
185
        }
186
187 12
        $this->logInitialization($logData);
188 12
        return true;
189
    }
190
191
    /**
192
     * Builds the SELECT part of the Index Queue initialization query.
193
     */
194 14
    protected function buildSelectStatement(): string
195
    {
196 14
        $changedField = $GLOBALS['TCA'][$this->type]['ctrl']['tstamp'];
197 14
        if (!empty($GLOBALS['TCA'][$this->type]['ctrl']['enablecolumns']['starttime'])) {
198 14
            $changedField = 'GREATEST(' . $GLOBALS['TCA'][$this->type]['ctrl']['enablecolumns']['starttime'] . ',' . $GLOBALS['TCA'][$this->type]['ctrl']['tstamp'] . ')';
199
        }
200
        return 'SELECT '
201 14
            . '\'' . $this->site->getRootPageId() . '\' as root, '
0 ignored issues
show
Bug introduced by
The method getRootPageId() does not exist on null. ( Ignorable by Annotation )

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

201
            . '\'' . $this->site->/** @scrutinizer ignore-call */ getRootPageId() . '\' as root, '

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
202 14
            . '\'' . $this->type . '\' AS item_type, '
203
            . 'uid AS item_uid, '
204 14
            . '\'' . $this->indexingConfigurationName . '\' as indexing_configuration, '
205 14
            . $this->getIndexingPriority() . ' AS indexing_priority, '
206
            . $changedField . ' AS changed';
207
    }
208
209
    // initialization query building
210
211
    /**
212
     * Reads the indexing priority for an indexing configuration.
213
     *
214
     * @return int Indexing priority
215
     */
216 14
    protected function getIndexingPriority(): int
217
    {
218 14
        $priority = 0;
219
220 14
        if (!empty($this->indexingConfiguration['indexingPriority'])) {
221
            $priority = (int)$this->indexingConfiguration['indexingPriority'];
222
        }
223
224 14
        return $priority;
225
    }
226
227
    /**
228
     * Builds a part of the WHERE clause of the Index Queue initialization
229
     * query. This part selects the limits items to be selected from the pages
230
     * in a site only, plus additional pages that may have been configured.
231
     * @throws DBALDriverException
232
     */
233 12
    protected function buildPagesClause(): string
234
    {
235 12
        $pages = $this->getPages();
236 12
        $pageIdField = ($this->type === 'pages') ? 'uid' : 'pid';
237
238 12
        return $pageIdField . ' IN(' . implode(',', $pages) . ')';
239
    }
240
241
    /**
242
     * Gets the pages in a site plus additional pages that may have been
243
     * configured.
244
     *
245
     * @return array A (sorted) array of page IDs in a site
246
     * @throws DBALDriverException
247
     */
248 12
    protected function getPages(): array
249
    {
250 12
        $pages = $this->site->getPages(null, $this->indexingConfigurationName);
251 12
        $additionalPageIds = [];
252 12
        if (!empty($this->indexingConfiguration['additionalPageIds'])) {
253
            $additionalPageIds = GeneralUtility::intExplode(',', $this->indexingConfiguration['additionalPageIds']);
254
        }
255
256 12
        $pages = array_merge($pages, $additionalPageIds);
257 12
        sort($pages, SORT_NUMERIC);
258
259 12
        $pagesWithinNoSearchSubEntriesPages = $this->pagesRepository->findAllPagesWithinNoSearchSubEntriesMarkedPages();
260
        // @todo: log properly if $additionalPageIds are within $pagesWithinNoSearchSubEntriesPages
261 12
        return array_values(array_diff($pages, $pagesWithinNoSearchSubEntriesPages));
262
    }
263
264
    /**
265
     * Builds the WHERE clauses of the Index Queue initialization query based
266
     * on TCA information for the type to be initialized.
267
     *
268
     * @return string Conditions to only add indexable items to the Index Queue
269
     */
270 14
    protected function buildTcaWhereClause(): string
271
    {
272 14
        $tcaWhereClause = '';
273 14
        $conditions = [];
274
275 14
        if (isset($GLOBALS['TCA'][$this->type]['ctrl']['delete'])) {
276 14
            $conditions['delete'] = $GLOBALS['TCA'][$this->type]['ctrl']['delete'] . ' = 0';
277
        }
278
279 14
        if (isset($GLOBALS['TCA'][$this->type]['ctrl']['enablecolumns']['disabled'])) {
280 14
            $conditions['disabled'] = $GLOBALS['TCA'][$this->type]['ctrl']['enablecolumns']['disabled'] . ' = 0';
281
        }
282
283 14
        if (isset($GLOBALS['TCA'][$this->type]['ctrl']['enablecolumns']['endtime'])) {
284
            // only include records with a future endtime or default value (0)
285 14
            $endTimeFieldName = $GLOBALS['TCA'][$this->type]['ctrl']['enablecolumns']['endtime'];
286 14
            $conditions['endtime'] = '(' . $endTimeFieldName . ' > ' . time() . ' OR ' . $endTimeFieldName . ' = 0)';
287
        }
288
289 14
        if (BackendUtility::isTableLocalizable($this->type)) {
290 14
            $conditions['languageField'] = [
291
                // default language
292 14
                $GLOBALS['TCA'][$this->type]['ctrl']['languageField'] . ' = 0',
293
                // all languages
294 14
                $GLOBALS['TCA'][$this->type]['ctrl']['languageField'] . ' = -1',
295
            ];
296
            // all "free"-Mode languages for "non-pages"-records only
297 14
            if ($this->type !== 'pages' && $this->site->hasFreeContentModeLanguages()) {
298
                $conditions['languageField'][]
299
                    = $GLOBALS['TCA'][$this->type]['ctrl']['languageField']
300
                    . ' IN(/* free content mode */ '
301
                        . implode(',', $this->site->getFreeContentModeLanguages())
302
                    . ')';
303
            }
304
305 14
            if (isset($GLOBALS['TCA'][$this->type]['ctrl']['transOrigPointerField'])) {
306 14
                $conditions['languageField'][] = $GLOBALS['TCA'][$this->type]['ctrl']['transOrigPointerField'] . ' = 0'; // translations without original language source
307
            }
308 14
            $conditions['languageField'] = '(' . implode(
309
                ' OR ',
310 14
                $conditions['languageField']
311
            ) . ')';
312
        }
313
314 14
        if (!empty($GLOBALS['TCA'][$this->type]['ctrl']['versioningWS'])) {
315
            // versioning is enabled for this table: exclude draft workspace records
316
            /* @see \TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction::buildExpression */
317 14
            $conditions['versioningWS'] = 't3ver_wsid = 0';
318
        }
319
320 14
        if (count($conditions)) {
321 14
            $tcaWhereClause = ' AND ' . implode(' AND ', $conditions);
322
        }
323
324 14
        return $tcaWhereClause;
325
    }
326
327
    /**
328
     * Builds the WHERE clauses of the Index Queue initialization query based
329
     * on TypoScript configuration for the type to be initialized.
330
     *
331
     * @return string Conditions to add items to the Index Queue based on TypoScript configuration
332
     */
333 14
    protected function buildUserWhereClause(): string
334
    {
335 14
        $condition = '';
336
337
        // FIXME replace this with the mechanism described below
338 14
        if (isset($this->indexingConfiguration['additionalWhereClause'])) {
339 12
            $condition = ' AND ' . $this->indexingConfiguration['additionalWhereClause'];
340
        }
341
342 14
        return $condition;
343
344
        // TODO add a query builder implementation based on TypoScript configuration
345
346
        /* example TypoScript
347
348
                @see http://docs.jboss.org/drools/release/5.4.0.Final/drools-expert-docs/html_single/index.html
349
                @see The Java Rule Engine API (JSR94)
350
351
                tt_news {
352
353
                        // RULES cObject provided by EXT:rules, simply evaluates to boolean TRUE or FALSE
354
                    conditions = RULES
355
                    conditions {
356
357
                        and {
358
359
                            10 {
360
                                field = pid
361
                                value = 2,3,5
362
                                condition = in / equals / notEquals / greaterThan / lessThan / greaterThanOrEqual / lessThanOrEqual
363
                            }
364
365
                            20 {
366
                                field = ...
367
                                value = ...
368
                                condition = ...
369
370
                                or {
371
                                    10 {
372
                                        field = ...
373
                                        value = ...
374
                                        condition =  ...
375
                                    }
376
377
                                    20 {
378
                                        field = ...
379
                                        value = ...
380
                                        condition = ...
381
                                    }
382
                                }
383
                            }
384
385
                        }
386
387
                    }
388
389
                    fields {
390
                        // field mapping
391
                    }
392
                }
393
        */
394
    }
395
396
    /**
397
     * Writes the passed log data to the log.
398
     *
399
     * @param array $logData
400
     */
401 14
    protected function logInitialization(array $logData)
402
    {
403 14
        if (!$this->site->getSolrConfiguration()->getLoggingIndexingIndexQueueInitialization()) {
404 14
            return;
405
        }
406
407
        $logSeverity = isset($logData['error']) ? SolrLogManager::ERROR : SolrLogManager::NOTICE;
408
        $logData = array_merge($logData, [
409
            'site' => $this->site->getLabel(),
410
            'indexing configuration name' => $this->indexingConfigurationName,
411
            'type' => $this->type,
412
        ]);
413
414
        $message = 'Index Queue initialized for indexing configuration ' . $this->indexingConfigurationName;
415
        $this->logger->log($logSeverity, $message, $logData);
416
    }
417
}
418