Passed
Pull Request — main (#3509)
by Yann
29:53
created

QueueItemRepository::containsItemWithRootPageId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
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;
19
20
use ApacheSolrForTypo3\Solr\Domain\Site\Site;
21
use ApacheSolrForTypo3\Solr\IndexQueue\Item;
22
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
23
use ApacheSolrForTypo3\Solr\System\Records\AbstractRepository;
24
use Doctrine\DBAL\ConnectionException;
25
use Doctrine\DBAL\Driver\Exception as DBALDriverException;
26
use Doctrine\DBAL\Exception as DBALException;
27
use PDO;
28
use Throwable;
29
use TYPO3\CMS\Core\Database\ConnectionPool;
30
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
31
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32
use TYPO3\CMS\Core\Utility\GeneralUtility;
33
34
/**
35
 * Class QueueItemRepository
36
 * Handles all CRUD operations to tx_solr_indexqueue_item table
37
 */
38
class QueueItemRepository extends AbstractRepository
39
{
40
    /**
41
     * @var string
42
     */
43
    protected string $table = 'tx_solr_indexqueue_item';
44
45
    /**
46
     * @var SolrLogManager
47
     */
48
    protected SolrLogManager $logger;
49
50
    /**
51
     * QueueItemRepository constructor.
52
     *
53
     * @param SolrLogManager|null $logManager
54
     */
55 170
    public function __construct(SolrLogManager $logManager = null)
56
    {
57 170
        $this->logger = $logManager ?? GeneralUtility::makeInstance(
58 170
            SolrLogManager::class,
59
            /** @scrutinizer ignore-type */
60 170
            __CLASS__
61 170
        );
62
    }
63
64
    /**
65
     * Fetches the last indexed row
66
     *
67
     * @param int $rootPageId The root page uid for which to get the last indexed row
68
     * @return array
69
     *
70
     * @throws DBALDriverException
71
     * @throws DBALException|\Doctrine\DBAL\DBALException
72
     */
73 5
    public function findLastIndexedRow(int $rootPageId): array
74
    {
75 5
        $queryBuilder = $this->getQueryBuilder();
76 5
        return $queryBuilder
77 5
            ->select('uid', 'indexed')
78 5
            ->from($this->table)
79 5
            ->where(
80
                /** @scrutinizer ignore-type */
81 5
                $queryBuilder->expr()->eq('root', $rootPageId)
82 5
            )
83 5
            ->andWhere(
84
                /** @scrutinizer ignore-type */
85 5
                $queryBuilder->expr()->neq('indexed', 0)
86 5
            )
87 5
            ->orderBy('indexed', 'DESC')
88 5
            ->setMaxResults(1)
89 5
            ->execute()
90 5
            ->fetchAllAssociative();
91
    }
92
93
    /**
94
     * Finds indexing errors for the current site
95
     *
96
     * @param Site $site
97
     * @return array Error items for the current site's Index Queue
98
     *
99
     * @throws DBALDriverException
100
     * @throws DBALException|\Doctrine\DBAL\DBALException
101
     */
102 3
    public function findErrorsBySite(Site $site): array
103
    {
104 3
        $queryBuilder = $this->getQueryBuilder();
105 3
        return $queryBuilder
106 3
            ->select('uid', 'item_type', 'item_uid', 'errors')
107 3
            ->from($this->table)
108 3
            ->andWhere(
109
                /** @scrutinizer ignore-type */
110 3
                $queryBuilder->expr()->notLike('errors', $queryBuilder->createNamedParameter('')),
111 3
                $queryBuilder->expr()->eq('root', $site->getRootPageId())
112 3
            )
113 3
            ->execute()
114 3
            ->fetchAllAssociative();
115
    }
116
117
    /**
118
     * Resets all the errors for all index queue items.
119
     *
120
     * @return int affected rows
121
     *
122
     * @throws DBALException|\Doctrine\DBAL\DBALException
123
     */
124 1
    public function flushAllErrors(): int
125
    {
126 1
        $queryBuilder = $this->getQueryBuilder();
127 1
        return (int)$this->getPreparedFlushErrorQuery($queryBuilder)
128 1
            ->execute();
129
    }
130
131
    /**
132
     * Flushes the errors for a single site.
133
     *
134
     * @param Site $site
135
     * @return int
136
     *
137
     * @throws DBALException|\Doctrine\DBAL\DBALException
138
     */
139 1
    public function flushErrorsBySite(Site $site): int
140
    {
141 1
        $queryBuilder = $this->getQueryBuilder();
142 1
        return (int)$this->getPreparedFlushErrorQuery($queryBuilder)
143 1
            ->andWhere(
144
                /** @scrutinizer ignore-type */
145 1
                $queryBuilder->expr()->eq('root', $site->getRootPageId())
146 1
            )
147 1
            ->execute();
148
    }
149
150
    /**
151
     * Flushes the error for a single item.
152
     *
153
     * @param Item $item
154
     * @return int affected rows
155
     *
156
     * @throws DBALException|\Doctrine\DBAL\DBALException
157
     */
158 2
    public function flushErrorByItem(Item $item): int
159
    {
160 2
        $queryBuilder = $this->getQueryBuilder();
161 2
        return (int)$this->getPreparedFlushErrorQuery($queryBuilder)
162 2
            ->andWhere(
163
                /** @scrutinizer ignore-type */
164 2
                $queryBuilder->expr()->eq('uid', $item->getIndexQueueUid())
165 2
            )
166 2
            ->execute();
167
    }
168
169
    /**
170
     * Initializes the QueryBuilder with a query the resets the error field for items that have an error.
171
     *
172
     * @param QueryBuilder $queryBuilder
173
     * @return QueryBuilder
174
     */
175 4
    private function getPreparedFlushErrorQuery(QueryBuilder $queryBuilder): QueryBuilder
176
    {
177 4
        return $queryBuilder
178 4
            ->update($this->table)
179 4
            ->set('errors', '')
180 4
            ->where(
181
                /** @scrutinizer ignore-type */
182 4
                $queryBuilder->expr()->notLike('errors', $queryBuilder->createNamedParameter(''))
183 4
            );
184
    }
185
186
    /**
187
     * Updates an existing queue entry by $itemType $itemUid and $rootPageId.
188
     *
189
     * @param string $itemType The item's type, usually a table name.
190
     * @param int $itemUid The item's uid, usually an integer uid, could be a
191
     *      different value for non-database-record types.
192
     * @param int $rootPageId The uid of the rootPage
193
     * @param int $changedTime The forced change time that should be used for updating
194
     * @param string $indexingConfiguration The name of the related indexConfiguration
195
     * @param int $indexingPriority
196
     * @return int affected rows
197
     *
198
     * @throws DBALException|\Doctrine\DBAL\DBALException
199
     */
200 19
    public function updateExistingItemByItemTypeAndItemUidAndRootPageId(
201
        string $itemType,
202
        int $itemUid,
203
        int $rootPageId,
204
        int $changedTime,
205
        string $indexingConfiguration = '',
206
        int $indexingPriority = 0
207
    ): int {
208 19
        $queryBuilder = $this->getQueryBuilder();
209 19
        $queryBuilder
210 19
            ->update($this->table)
211 19
            ->set('changed', $changedTime)
212 19
            ->set('indexing_priority', $indexingPriority)
213 19
            ->andWhere(
214
                /** @scrutinizer ignore-type */
215 19
                $queryBuilder->expr()->eq('item_type', $queryBuilder->createNamedParameter($itemType)),
216
                /** @scrutinizer ignore-type */
217 19
                $queryBuilder->expr()->eq('item_uid', $itemUid),
218
                /** @scrutinizer ignore-type */
219 19
                $queryBuilder->expr()->eq('root', $rootPageId)
220 19
            );
221
222 19
        if (!empty($indexingConfiguration)) {
223 19
            $queryBuilder->set('indexing_configuration', $indexingConfiguration);
224
        }
225
226 19
        return (int)$queryBuilder->execute();
227
    }
228
229
    /**
230
     * Adds an item to the index queue.
231
     *
232
     * Not meant for public use.
233
     *
234
     * @param string $itemType The item's type, usually a table name.
235
     * @param int $itemUid The item's uid, usually an integer uid, could be a different value for non-database-record types.
236
     * @param int $rootPageId
237
     * @param int $changedTime
238
     * @param string $indexingConfiguration The item's indexing configuration to use. Optional, overwrites existing / determined configuration.
239
     * @param int $indexingPriority
240
     * @return int the number of inserted rows, which is typically 1
241
     *
242
     * @throws DBALException|\Doctrine\DBAL\DBALException
243
     */
244 81
    public function add(
245
        string $itemType,
246
        int $itemUid,
247
        int $rootPageId,
248
        int $changedTime,
249
        string $indexingConfiguration,
250
        int $indexingPriority = 0
251
    ): int {
252 81
        $queryBuilder = $this->getQueryBuilder();
253 81
        return (int)$queryBuilder
254 81
            ->insert($this->table)
255 81
            ->values([
256 81
                'root' => $rootPageId,
257 81
                'item_type' => $itemType,
258 81
                'item_uid' => $itemUid,
259 81
                'changed' => $changedTime,
260 81
                'errors' => '',
261 81
                'indexing_configuration' => $indexingConfiguration,
262 81
                'indexing_priority' => $indexingPriority,
263 81
            ])
264 81
            ->execute();
265
    }
266
267
    /**
268
     * Retrieves the count of items that match certain filters. Each filter is passed as parts of the where claus combined with AND.
269
     *
270
     * @param array $sites
271
     * @param array $indexQueueConfigurationNames
272
     * @param array $itemTypes
273
     * @param array $itemUids
274
     * @param array $uids
275
     * @return int
276
     *
277
     * @throws DBALDriverException
278
     * @throws DBALException|\Doctrine\DBAL\DBALException
279
     */
280 1
    public function countItems(
281
        array $sites = [],
282
        array $indexQueueConfigurationNames = [],
283
        array $itemTypes = [],
284
        array $itemUids = [],
285
        array $uids = []
286
    ): int {
287 1
        $rootPageIds = Site::getRootPageIdsFromSites($sites);
288 1
        $indexQueueConfigurationList = implode(',', $indexQueueConfigurationNames);
289 1
        $itemTypeList = implode(',', $itemTypes);
290 1
        $itemUids = array_map('intval', $itemUids);
291 1
        $uids = array_map('intval', $uids);
292
293 1
        $queryBuilderForCountingItems = $this->getQueryBuilder();
294 1
        $queryBuilderForCountingItems->count('uid')->from($this->table);
295 1
        $queryBuilderForCountingItems = $this->addItemWhereClauses(
296 1
            $queryBuilderForCountingItems,
297 1
            $rootPageIds,
298 1
            $indexQueueConfigurationList,
299 1
            $itemTypeList,
300 1
            $itemUids,
301 1
            $uids
302 1
        );
303
304 1
        return (int)$queryBuilderForCountingItems
305 1
            ->execute()
306 1
            ->fetchOne();
307
    }
308
309
    /**
310
     * Gets the most recent changed time of a page's content elements
311
     *
312
     * @param int $pageUid
313
     * @return int|null Timestamp of the most recent content element change or null if nothing is found.
314
     *
315
     * @throws DBALDriverException
316
     * @throws DBALException|\Doctrine\DBAL\DBALException
317
     */
318 59
    public function getPageItemChangedTimeByPageUid(int $pageUid): ?int
319
    {
320 59
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
321 59
            ->getQueryBuilderForTable('tt_content');
322 59
        $queryBuilder->getRestrictions()->removeAll();
323 59
        $pageContentLastChangedTime = $queryBuilder
324 59
            ->add('select', $queryBuilder->expr()->max('tstamp', 'changed_time'))
325 59
            ->from('tt_content')
326 59
            ->where(
327
                /** @scrutinizer ignore-type */
328 59
                $queryBuilder->expr()->eq('pid', $pageUid)
329 59
            )
330 59
            ->execute()
331 59
            ->fetchAssociative();
332
333 59
        return $pageContentLastChangedTime['changed_time'];
334
    }
335
336
    /**
337
     * Gets the most recent changed time for an item taking into account
338
     * localized records.
339
     *
340
     * @param string $itemType The item's type, usually a table name.
341
     * @param int $itemUid The item's uid
342
     * @return int Timestamp of the most recent content element change
343
     *
344
     * @throws DBALDriverException
345
     * @throws DBALException|\Doctrine\DBAL\DBALException
346
     */
347 87
    public function getLocalizableItemChangedTime(string $itemType, int $itemUid): int
348
    {
349 87
        $localizedChangedTime = 0;
350
351 87
        if (isset($GLOBALS['TCA'][$itemType]['ctrl']['transOrigPointerField'])) {
352
            // table is localizable
353 87
            $translationOriginalPointerField = $GLOBALS['TCA'][$itemType]['ctrl']['transOrigPointerField'];
354 87
            $timeStampField = $GLOBALS['TCA'][$itemType]['ctrl']['tstamp'];
355
356 87
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($itemType);
357 87
            $queryBuilder->getRestrictions()->removeAll();
358 87
            $localizedChangedTime = $queryBuilder
359 87
                ->add('select', $queryBuilder->expr()->max($timeStampField, 'changed_time'))
360 87
                ->from($itemType)
361 87
                ->orWhere(
362
                    /** @scrutinizer ignore-type */
363 87
                    $queryBuilder->expr()->eq('uid', $itemUid),
364 87
                    $queryBuilder->expr()->eq($translationOriginalPointerField, $itemUid)
365 87
                )
366 87
                ->execute()
367 87
                ->fetchOne();
368
        }
369 87
        return (int)$localizedChangedTime;
370
    }
371
372
    /**
373
     * Returns prepared QueryBuilder for contains* methods in this repository
374
     *
375
     * @param string $itemType
376
     * @param int $itemUid
377
     * @return QueryBuilder
378
     */
379 94
    protected function getQueryBuilderForContainsMethods(string $itemType, int $itemUid): QueryBuilder
380
    {
381 94
        $queryBuilder = $this->getQueryBuilder();
382 94
        return $queryBuilder->count('uid')->from($this->table)
383 94
            ->andWhere(
384
                /** @scrutinizer ignore-type */
385 94
                $queryBuilder->expr()->eq('item_type', $queryBuilder->createNamedParameter($itemType)),
386
                /** @scrutinizer ignore-type */
387 94
                $queryBuilder->expr()->eq('item_uid', $itemUid)
388 94
            );
389
    }
390
391
    /**
392
     * Checks whether the Index Queue contains a specific item.
393
     *
394
     * @param string $itemType The item's type, usually a table name.
395
     * @param int $itemUid The item's uid
396
     * @return bool TRUE if the item is found in the queue, FALSE otherwise
397
     *
398
     * @throws DBALDriverException
399
     * @throws DBALException|\Doctrine\DBAL\DBALException
400
     */
401 10
    public function containsItem(string $itemType, int $itemUid): bool
402
    {
403 10
        return (bool)$this->getQueryBuilderForContainsMethods($itemType, $itemUid)
404 10
            ->execute()
405 10
            ->fetchOne();
406
    }
407
408
    /**
409
     * Checks whether the Index Queue contains a specific item.
410
     *
411
     * @param string $itemType The item's type, usually a table name.
412
     * @param int $itemUid The item's uid
413
     * @param int $rootPageId
414
     * @return bool TRUE if the item is found in the queue, FALSE otherwise
415
     *
416
     * @throws DBALDriverException
417
     * @throws DBALException|\Doctrine\DBAL\DBALException
418
     */
419 87
    public function containsItemWithRootPageId(string $itemType, int $itemUid, int $rootPageId): bool
420
    {
421 87
        $queryBuilder = $this->getQueryBuilderForContainsMethods($itemType, $itemUid);
422 87
        return (bool)$queryBuilder
423 87
            ->andWhere(/** @scrutinizer ignore-type */ $queryBuilder->expr()->eq('root', $rootPageId))
424 87
            ->execute()
425 87
            ->fetchOne();
426
    }
427
428
    /**
429
     * Checks whether the Index Queue contains a specific item that has been
430
     * marked as indexed.
431
     *
432
     * @param string $itemType The item's type, usually a table name.
433
     * @param int $itemUid The item's uid
434
     * @return bool TRUE if the item is found in the queue and marked as indexed, FALSE otherwise
435
     *
436
     * @throws DBALDriverException
437
     * @throws DBALException|\Doctrine\DBAL\DBALException
438
     */
439 4
    public function containsIndexedItem(string $itemType, int $itemUid): bool
440
    {
441 4
        $queryBuilder = $this->getQueryBuilderForContainsMethods($itemType, $itemUid);
442 4
        return (bool)$queryBuilder
443 4
            ->andWhere(/** @scrutinizer ignore-type */ $queryBuilder->expr()->gt('indexed', 0))
444 4
            ->execute()
445 4
            ->fetchOne();
446
    }
447
448
    /**
449
     * Removes an item from the Index Queue.
450
     *
451
     * @param string $itemType The type of the item to remove, usually a table name.
452
     * @param int|null $itemUid The uid of the item to remove
453
     *
454
     * @throws ConnectionException
455
     * @throws Throwable
456
     * @throws DBALException
457
     */
458 64
    public function deleteItem(string $itemType, int $itemUid = null)
459
    {
460 64
        $itemUids = empty($itemUid) ? [] : [$itemUid];
461 64
        $this->deleteItems([], [], [$itemType], $itemUids);
462
    }
463
464
    /**
465
     * Removes all items of a certain type from the Index Queue.
466
     *
467
     * @param string $itemType The type of items to remove, usually a table name.
468
     *
469
     * @throws ConnectionException
470
     * @throws Throwable
471
     * @throws DBALException
472
     */
473 1
    public function deleteItemsByType(string $itemType)
474
    {
475 1
        $this->deleteItem($itemType);
476
    }
477
478
    /**
479
     * Removes all items of a certain site from the Index Queue. Accepts an
480
     * optional parameter to limit the deleted items by indexing configuration.
481
     *
482
     * @param Site $site The site to remove items for.
483
     * @param string $indexingConfigurationName Name of a specific indexing configuration
484
     *
485
     * @throws ConnectionException
486
     * @throws Throwable
487
     * @throws DBALException
488
     */
489 7
    public function deleteItemsBySite(Site $site, string $indexingConfigurationName = '')
490
    {
491 7
        $indexingConfigurationNames = empty($indexingConfigurationName) ? [] : [$indexingConfigurationName];
492 7
        $this->deleteItems([$site], $indexingConfigurationNames);
493
    }
494
495
    /**
496
     * Removes items in the index queue filtered by the passed arguments.
497
     *
498
     * @param array $sites
499
     * @param array $indexQueueConfigurationNames
500
     * @param array $itemTypes
501
     * @param array $itemUids
502
     * @param array $uids
503
     *
504
     * @throws ConnectionException
505
     * @throws DBALException
506
     * @throws Throwable
507
     */
508 71
    public function deleteItems(
509
        array $sites = [],
510
        array $indexQueueConfigurationNames = [],
511
        array $itemTypes = [],
512
        array $itemUids = [],
513
        array $uids = []
514
    ): void {
515 71
        $rootPageIds = Site::getRootPageIdsFromSites($sites);
516 71
        $indexQueueConfigurationList = implode(',', $indexQueueConfigurationNames);
517 71
        $itemTypeList = implode(',', $itemTypes);
518 71
        $itemUids = array_map('intval', $itemUids);
519 71
        $uids = array_map('intval', $uids);
520
521 71
        $queryBuilderForDeletingItems = $this->getQueryBuilder();
522 71
        $queryBuilderForDeletingItems->delete($this->table);
523 71
        $queryBuilderForDeletingItems = $this->addItemWhereClauses($queryBuilderForDeletingItems, $rootPageIds, $indexQueueConfigurationList, $itemTypeList, $itemUids, $uids);
524
525 71
        $queryBuilderForDeletingProperties = $this->buildQueryForPropertyDeletion($queryBuilderForDeletingItems, $rootPageIds, $indexQueueConfigurationList, $itemTypeList, $itemUids, $uids);
526
527 71
        $queryBuilderForDeletingItems->getConnection()->beginTransaction();
528
        try {
529 71
            $queryBuilderForDeletingItems->execute();
530 71
            $queryBuilderForDeletingProperties->execute();
531
532 71
            $queryBuilderForDeletingItems->getConnection()->commit();
533
        } catch (Throwable $e) {
534
            $queryBuilderForDeletingItems->getConnection()->rollback();
535
            throw $e;
536
        }
537
    }
538
539
    /**
540
     * Initializes the query builder to delete items in the index queue filtered by the passed arguments.
541
     *
542
     * @param QueryBuilder $queryBuilderForDeletingItems
543
     * @param array $rootPageIds filter on a set of rootPageUids.
544
     * @param string $indexQueueConfigurationList
545
     * @param string $itemTypeList
546
     * @param array $itemUids filter on a set of item uids
547
     * @param array $uids filter on a set of queue item uids
548
     * @return QueryBuilder
549
     */
550 73
    private function addItemWhereClauses(
551
        QueryBuilder $queryBuilderForDeletingItems,
552
        array $rootPageIds,
553
        string $indexQueueConfigurationList,
554
        string $itemTypeList,
555
        array $itemUids,
556
        array $uids
557
    ): QueryBuilder {
558 73
        if (!empty($rootPageIds)) {
559 7
            $queryBuilderForDeletingItems->andWhere(
560
                /** @scrutinizer ignore-type */
561 7
                $queryBuilderForDeletingItems->expr()->in('root', $rootPageIds)
562 7
            );
563
        }
564
565 73
        if (!empty($indexQueueConfigurationList)) {
566 9
            $queryBuilderForDeletingItems->andWhere(
567
                /** @scrutinizer ignore-type */
568 9
                $queryBuilderForDeletingItems->expr()->in(
569 9
                    'indexing_configuration',
570 9
                    $queryBuilderForDeletingItems->createNamedParameter($indexQueueConfigurationList)
571 9
                )
572 9
            );
573
        }
574
575 73
        if (!empty($itemTypeList)) {
576 64
            $queryBuilderForDeletingItems->andWhere(
577
                /** @scrutinizer ignore-type */
578 64
                $queryBuilderForDeletingItems->expr()->in(
579 64
                    'item_type',
580 64
                    $queryBuilderForDeletingItems->createNamedParameter($itemTypeList)
581 64
                )
582 64
            );
583
        }
584
585 73
        if (!empty($itemUids)) {
586 63
            $queryBuilderForDeletingItems->andWhere(
587
                /** @scrutinizer ignore-type */
588 63
                $queryBuilderForDeletingItems->expr()->in('item_uid', $itemUids)
589 63
            );
590
        }
591
592 73
        if (!empty($uids)) {
593 1
            $queryBuilderForDeletingItems->andWhere(
594
                /** @scrutinizer ignore-type */
595 1
                $queryBuilderForDeletingItems->expr()->in('uid', $uids)
596 1
            );
597
        }
598
599 73
        return $queryBuilderForDeletingItems;
600
    }
601
602
    /**
603
     * Initializes a query builder to delete the indexing properties of an item by the passed conditions.
604
     *
605
     * @param QueryBuilder $queryBuilderForDeletingItems
606
     * @param array $rootPageIds
607
     * @param string $indexQueueConfigurationList
608
     * @param string $itemTypeList
609
     * @param array $itemUids
610
     * @param array $uids
611
     * @return QueryBuilder
612
     *
613
     * @throws DBALDriverException
614
     * @throws DBALException|\Doctrine\DBAL\DBALException
615
     */
616 71
    private function buildQueryForPropertyDeletion(
617
        QueryBuilder $queryBuilderForDeletingItems,
618
        array $rootPageIds,
619
        string $indexQueueConfigurationList,
620
        string $itemTypeList,
621
        array $itemUids,
622
        array $uids
623
    ): QueryBuilder {
624 71
        $queryBuilderForSelectingProperties = $queryBuilderForDeletingItems->getConnection()->createQueryBuilder();
625 71
        $queryBuilderForSelectingProperties
626 71
            ->select('items.uid')
627 71
            ->from('tx_solr_indexqueue_indexing_property', 'properties')
628 71
            ->innerJoin(
629 71
                'properties',
630 71
                $this->table,
631 71
                'items',
632 71
                (string)$queryBuilderForSelectingProperties->expr()->andX(
633 71
                    $queryBuilderForSelectingProperties->expr()->eq('items.uid', $queryBuilderForSelectingProperties->quoteIdentifier('properties.item_id')),
634 71
                    empty($rootPageIds) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.root', $rootPageIds),
635 71
                    empty($indexQueueConfigurationList) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.indexing_configuration', $queryBuilderForSelectingProperties->createNamedParameter($indexQueueConfigurationList)),
636 71
                    empty($itemTypeList) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.item_type', $queryBuilderForSelectingProperties->createNamedParameter($itemTypeList)),
637 71
                    empty($itemUids) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.item_uid', $itemUids),
638 71
                    empty($uids) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.uid', $uids)
639 71
                )
640 71
            );
641 71
        $propertyEntriesToDelete = implode(
642 71
            ',',
643 71
            array_column(
644 71
                $queryBuilderForSelectingProperties
645 71
                    ->execute()
646 71
                    ->fetchAllAssociative(),
647 71
                'uid'
648 71
            )
649 71
        );
650
651 71
        $queryBuilderForDeletingProperties = $queryBuilderForDeletingItems->getConnection()->createQueryBuilder();
652
653
        // make sure executing the property deletion query doesn't fail if there are no properties to delete
654 71
        if (empty($propertyEntriesToDelete)) {
655 70
            $propertyEntriesToDelete = '0';
656
        }
657
658 71
        $queryBuilderForDeletingProperties->delete('tx_solr_indexqueue_indexing_property')->where(
659 71
            $queryBuilderForDeletingProperties->expr()->in('item_id', $propertyEntriesToDelete)
660 71
        );
661
662 71
        return $queryBuilderForDeletingProperties;
663
    }
664
665
    /**
666
     * Removes all items from the Index Queue.
667
     *
668
     * @return int The number of affected rows. For a truncate this is unreliable as there is no meaningful information.
669
     */
670 1
    public function deleteAllItems(): int
671
    {
672 1
        return $this->getQueryBuilder()->getConnection()->truncate($this->table);
673
    }
674
675
    /**
676
     * Gets a single Index Queue item by its uid.
677
     *
678
     * @param int $uid Index Queue item uid
679
     * @return Item|null The request Index Queue item or NULL if no item with $itemId was found
680
     *
681
     * @throws DBALDriverException
682
     * @throws DBALException|\Doctrine\DBAL\DBALException
683
     */
684 36
    public function findItemByUid(int $uid): ?Item
685
    {
686 36
        $queryBuilder = $this->getQueryBuilder();
687 36
        $indexQueueItemRecord = $queryBuilder
688 36
            ->select('*')
689 36
            ->from($this->table)
690 36
            ->where(/** @scrutinizer ignore-type */ $queryBuilder->expr()->eq('uid', $uid))
691 36
            ->execute()
692 36
            ->fetchAssociative();
693
694 36
        if (!isset($indexQueueItemRecord['uid'])) {
695 7
            return null;
696
        }
697
698 29
        return GeneralUtility::makeInstance(Item::class, /** @scrutinizer ignore-type */ $indexQueueItemRecord);
699
    }
700
701
    /**
702
     * Gets Index Queue items by type and uid.
703
     *
704
     * @param string $itemType item type, usually  the table name
705
     * @param int $itemUid item uid
706
     * @return Item[] An array of items matching $itemType and $itemUid
707
     *
708
     * @throws ConnectionException
709
     * @throws DBALDriverException
710
     * @throws Throwable
711
     * @throws DBALException
712
     */
713 53
    public function findItemsByItemTypeAndItemUid(string $itemType, int $itemUid): array
714
    {
715 53
        $queryBuilder = $this->getQueryBuilder();
716 53
        $compositeExpression = $queryBuilder->expr()->andX(
717
            /** @scrutinizer ignore-type */
718 53
            $queryBuilder->expr()->eq('item_type', $queryBuilder->getConnection()->quote($itemType, PDO::PARAM_STR)),
719 53
            $queryBuilder->expr()->eq('item_uid', $itemUid)
720 53
        );
721 53
        return $this->getItemsByCompositeExpression($compositeExpression, $queryBuilder);
722
    }
723
724
    /**
725
     * Returns a collection of items by CompositeExpression.
726
     *
727
     * @param CompositeExpression|null $expression Optional expression to filter records.
728
     * @param QueryBuilder|null $queryBuilder QueryBuilder to use
729
     * @return array
730
     *
731
     * @throws ConnectionException
732
     * @throws DBALDriverException
733
     * @throws Throwable
734
     * @throws DBALException
735
     */
736 53
    protected function getItemsByCompositeExpression(
737
        CompositeExpression $expression = null,
738
        QueryBuilder $queryBuilder = null
739
    ): array {
740 53
        if (!$queryBuilder instanceof QueryBuilder) {
741
            $queryBuilder = $this->getQueryBuilder();
742
        }
743
744 53
        $queryBuilder->select('*')->from($this->table);
745 53
        if (isset($expression)) {
746 53
            $queryBuilder->where($expression);
747
        }
748
749 53
        $indexQueueItemRecords = $queryBuilder
750 53
            ->execute()
751 53
            ->fetchAllAssociative();
752 53
        return $this->getIndexQueueItemObjectsFromRecords($indexQueueItemRecords);
753
    }
754
755
    /**
756
     * Returns all items in the queue.
757
     *
758
     * @return Item[] all Items from Queue without restrictions
759
     *
760
     * @throws ConnectionException
761
     * @throws DBALDriverException
762
     * @throws Throwable
763
     * @throws DBALException
764
     */
765 5
    public function findAll(): array
766
    {
767 5
        $queryBuilder = $this->getQueryBuilder();
768 5
        $allRecords = $queryBuilder
769 5
            ->select('*')
770 5
            ->from($this->table)
771 5
            ->execute()
772 5
            ->fetchAllAssociative();
773 5
        return $this->getIndexQueueItemObjectsFromRecords($allRecords);
774
    }
775
776
    /**
777
     * Gets $limit number of items to index for a particular $site.
778
     *
779
     * @param Site $site TYPO3 site
780
     * @param int $limit Number of items to get from the queue
781
     * @return Item[] Items to index to the given solr server
782
     *
783
     * @throws ConnectionException
784
     * @throws DBALDriverException
785
     * @throws Throwable
786
     * @throws DBALException
787
     */
788 3
    public function findItemsToIndex(Site $site, int $limit = 50): array
789
    {
790 3
        $queryBuilder = $this->getQueryBuilder();
791
        // determine which items to index with this run
792 3
        $indexQueueItemRecords = $queryBuilder
793 3
            ->select('*')
794 3
            ->from($this->table)
795 3
            ->andWhere(
796
                /** @scrutinizer ignore-type */
797 3
                $queryBuilder->expr()->eq('root', $site->getRootPageId()),
798
                /** @scrutinizer ignore-type */
799 3
                $queryBuilder->expr()->gt('changed', 'indexed'),
800
                /** @scrutinizer ignore-type */
801 3
                $queryBuilder->expr()->lte('changed', time()),
802
                /** @scrutinizer ignore-type */
803 3
                $queryBuilder->expr()->eq('errors', $queryBuilder->createNamedParameter(''))
804 3
            )
805 3
            ->orderBy('indexing_priority', 'DESC')
806 3
            ->addOrderBy('changed', 'DESC')
807 3
            ->addOrderBy('uid', 'DESC')
808 3
            ->setMaxResults($limit)
809 3
            ->execute()
810 3
            ->fetchAllAssociative();
811
812 3
        return $this->getIndexQueueItemObjectsFromRecords($indexQueueItemRecords);
813
    }
814
815
    /**
816
     * Retrieves the count of items that match certain filters. Each filter is passed as parts of the where claus combined with AND.
817
     *
818
     * @param array $sites
819
     * @param array $indexQueueConfigurationNames
820
     * @param array $itemTypes
821
     * @param array $itemUids
822
     * @param array $uids
823
     * @param int $start
824
     * @param int $limit
825
     * @return array
826
     *
827
     * @throws ConnectionException
828
     * @throws DBALDriverException
829
     * @throws Throwable
830
     * @throws DBALException
831
     */
832 1
    public function findItems(
833
        array $sites = [],
834
        array $indexQueueConfigurationNames = [],
835
        array $itemTypes = [],
836
        array $itemUids = [],
837
        array $uids = [],
838
        int $start = 0,
839
        int $limit = 50
840
    ): array {
841 1
        $rootPageIds = Site::getRootPageIdsFromSites($sites);
842 1
        $indexQueueConfigurationList = implode(',', $indexQueueConfigurationNames);
843 1
        $itemTypeList = implode(',', $itemTypes);
844 1
        $itemUids = array_map('intval', $itemUids);
845 1
        $uids = array_map('intval', $uids);
846 1
        $itemQueryBuilder = $this->getQueryBuilder()->select('*')->from($this->table);
847 1
        $itemQueryBuilder = $this->addItemWhereClauses($itemQueryBuilder, $rootPageIds, $indexQueueConfigurationList, $itemTypeList, $itemUids, $uids);
848 1
        $itemRecords = $itemQueryBuilder->setFirstResult($start)
849 1
            ->setMaxResults($limit)
850 1
            ->execute()
851 1
            ->fetchAllAssociative();
852 1
        return $this->getIndexQueueItemObjectsFromRecords($itemRecords);
853
    }
854
855
    /**
856
     * Creates an array of ApacheSolrForTypo3\Solr\IndexQueue\Item objects from an array of
857
     * index queue records.
858
     *
859
     * @param array $indexQueueItemRecords Array of plain index queue records
860
     * @return array Array of ApacheSolrForTypo3\Solr\IndexQueue\Item objects
861
     *
862
     * @throws ConnectionException
863
     * @throws DBALDriverException
864
     * @throws Throwable
865
     * @throws DBALException
866
     */
867 56
    protected function getIndexQueueItemObjectsFromRecords(array $indexQueueItemRecords): array
868
    {
869 56
        $tableRecords = $this->getAllQueueItemRecordsByUidsGroupedByTable($indexQueueItemRecords);
870 56
        return $this->getQueueItemObjectsByRecords($indexQueueItemRecords, $tableRecords);
871
    }
872
873
    /**
874
     * Returns the records for suitable item type.
875
     *
876
     * @param array $indexQueueItemRecords
877
     * @return array
878
     *
879
     * @throws DBALDriverException
880
     * @throws DBALException|\Doctrine\DBAL\DBALException
881
     */
882 56
    protected function getAllQueueItemRecordsByUidsGroupedByTable(array $indexQueueItemRecords): array
883
    {
884 56
        $tableUids = [];
885 56
        $tableRecords = [];
886
        // grouping records by table
887 56
        foreach ($indexQueueItemRecords as $indexQueueItemRecord) {
888 55
            $tableUids[$indexQueueItemRecord['item_type']][] = $indexQueueItemRecord['item_uid'];
889
        }
890
891
        // fetching records by table, saves us a lot of single queries
892 56
        foreach ($tableUids as $table => $uids) {
893 55
            $uidList = implode(',', $uids);
894
895 55
            $queryBuilderForRecordTable = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
896 55
            $queryBuilderForRecordTable->getRestrictions()->removeAll();
897 55
            $resultsFromRecordTable = $queryBuilderForRecordTable
898 55
                ->select('*')
899 55
                ->from($table)
900 55
                ->where(/** @scrutinizer ignore-type */ $queryBuilderForRecordTable->expr()->in('uid', $uidList))
901 55
                ->execute();
902 55
            $records = [];
903 55
            while ($record = $resultsFromRecordTable->fetchAssociative()) {
904 52
                $records[$record['uid']] = $record;
905
            }
906
907 55
            $tableRecords[$table] = $records;
908 55
            $this->hookPostProcessFetchRecordsForIndexQueueItem($table, $uids, $tableRecords);
909
        }
910
911 56
        return $tableRecords;
912
    }
913
914
    /**
915
     * Calls defined in postProcessFetchRecordsForIndexQueueItem hook method.
916
     *
917
     * @param string $table
918
     * @param array $uids
919
     * @param array $tableRecords
920
     */
921 55
    protected function hookPostProcessFetchRecordsForIndexQueueItem(string $table, array $uids, array &$tableRecords)
922
    {
923 55
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessFetchRecordsForIndexQueueItem'] ?? null)) {
924 55
            return;
925
        }
926
        $params = ['table' => $table, 'uids' => $uids, 'tableRecords' => &$tableRecords];
927
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessFetchRecordsForIndexQueueItem'] as $reference) {
928
            GeneralUtility::callUserFunction($reference, $params, $this);
929
        }
930
    }
931
932
    /**
933
     * Instantiates a list of Item objects from database records.
934
     *
935
     * @param array $indexQueueItemRecords records from database
936
     * @param array $tableRecords
937
     * @return array
938
     *
939
     * @throws ConnectionException
940
     * @throws Throwable
941
     * @throws DBALException
942
     */
943 56
    protected function getQueueItemObjectsByRecords(array $indexQueueItemRecords, array $tableRecords): array
944
    {
945 56
        $indexQueueItems = [];
946 56
        foreach ($indexQueueItemRecords as $indexQueueItemRecord) {
947 55
            if (isset($tableRecords[$indexQueueItemRecord['item_type']][$indexQueueItemRecord['item_uid']])) {
948 52
                $indexQueueItems[] = GeneralUtility::makeInstance(
949 52
                    Item::class,
950
                    /** @scrutinizer ignore-type */
951 52
                    $indexQueueItemRecord,
952
                    /** @scrutinizer ignore-type */
953 52
                    $tableRecords[$indexQueueItemRecord['item_type']][$indexQueueItemRecord['item_uid']]
954 52
                );
955
            } else {
956 3
                $this->logger->log(
957 3
                    SolrLogManager::ERROR,
958 3
                    'Record missing for Index Queue item. Item removed.',
959 3
                    [
960 3
                        $indexQueueItemRecord,
961 3
                    ]
962 3
                );
963 3
                $this->deleteItem(
964 3
                    $indexQueueItemRecord['item_type'],
965 3
                    $indexQueueItemRecord['item_uid']
966 3
                );
967
            }
968
        }
969
970 56
        return $indexQueueItems;
971
    }
972
973
    /**
974
     * Marks an item as failed and causes the indexer to skip the item in the
975
     * next run.
976
     *
977
     * @param int|Item $item Either the item's Index Queue uid or the complete item
978
     * @param string $errorMessage Error message
979
     * @return int affected rows
980
     *
981
     * @throws DBALException|\Doctrine\DBAL\DBALException
982
     */
983 6
    public function markItemAsFailed($item, string $errorMessage = ''): int
984
    {
985 6
        $itemUid = ($item instanceof Item) ? $item->getIndexQueueUid() : (int)$item;
986 6
        $errorMessage = empty($errorMessage) ? '1' : $errorMessage;
987
988 6
        $queryBuilder = $this->getQueryBuilder();
989 6
        return (int)$queryBuilder
990 6
            ->update($this->table)
991 6
            ->set('errors', $errorMessage)
992 6
            ->where($queryBuilder->expr()->eq('uid', $itemUid))
993 6
            ->execute();
994
    }
995
996
    /**
997
     * Sets the timestamp of when an item last has been indexed.
998
     *
999
     * @param Item $item
1000
     * @return int affected rows
1001
     *
1002
     * @throws DBALException|\Doctrine\DBAL\DBALException
1003
     */
1004 2
    public function updateIndexTimeByItem(Item $item): int
1005
    {
1006 2
        $queryBuilder = $this->getQueryBuilder();
1007 2
        return (int)$queryBuilder
1008 2
            ->update($this->table)
1009 2
            ->set('indexed', time())
1010 2
            ->where($queryBuilder->expr()->eq('uid', $item->getIndexQueueUid()))
1011 2
            ->execute();
1012
    }
1013
1014
    /**
1015
     * Sets the change timestamp of an item.
1016
     *
1017
     * @param Item $item
1018
     * @param int $changedTime
1019
     * @return int affected rows
1020
     *
1021
     * @throws DBALException|\Doctrine\DBAL\DBALException
1022
     */
1023
    public function updateChangedTimeByItem(Item $item, int $changedTime = 0): int
1024
    {
1025
        $queryBuilder = $this->getQueryBuilder();
1026
        return (int)$queryBuilder
1027
            ->update($this->table)
1028
            ->set('changed', $changedTime)
1029
            ->where($queryBuilder->expr()->eq('uid', $item->getIndexQueueUid()))
1030
            ->execute();
1031
    }
1032
1033
    /**
1034
     * Initializes Queue by given sql
1035
     *
1036
     * Note: Do not use platform specific functions!
1037
     *
1038
     * @param string $sqlStatement Native SQL statement
1039
     * @return int The number of affected rows.
1040
     *
1041
     * @throws DBALException
1042
     * @internal
1043
     */
1044 14
    public function initializeByNativeSQLStatement(string $sqlStatement): int
1045
    {
1046 14
        return $this->getQueryBuilder()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getQueryBu...tatement($sqlStatement) could return the type string which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
1047 14
            ->getConnection()
1048 14
            ->executeStatement($sqlStatement);
1049
    }
1050
1051
    /**
1052
     * Retrieves an array of pageIds from mountPoints that already have a queue entry.
1053
     *
1054
     * @param string $identifier identifier of the mount point
1055
     * @return array pageIds from mountPoints that already have a queue entry
1056
     *
1057
     * @throws DBALDriverException
1058
     * @throws DBALException|\Doctrine\DBAL\DBALException
1059
     */
1060 8
    public function findPageIdsOfExistingMountPagesByMountIdentifier(string $identifier): array
1061
    {
1062 8
        $queryBuilder = $this->getQueryBuilder();
1063 8
        $resultSet = $queryBuilder
1064 8
            ->select('item_uid')
1065 8
            ->add('select', $queryBuilder->expr()->count('*', 'queueItemCount'), true)
1066 8
            ->from($this->table)
1067 8
            ->where(
1068 8
                $queryBuilder->expr()->eq('item_type', $queryBuilder->createNamedParameter('pages')),
1069 8
                $queryBuilder->expr()->eq('pages_mountidentifier', $queryBuilder->createNamedParameter($identifier))
1070 8
            )
1071 8
            ->groupBy('item_uid')
1072 8
            ->execute();
1073
1074 8
        $mountedPagesIdsWithQueueItems = [];
1075 8
        while ($record = $resultSet->fetchAssociative()) {
1076
            if ($record['queueItemCount'] > 0) {
1077
                $mountedPagesIdsWithQueueItems[] = $record['item_uid'];
1078
            }
1079
        }
1080
1081 8
        return $mountedPagesIdsWithQueueItems;
1082
    }
1083
1084
    /**
1085
     * Retrieves an array of items for mount destinations matched by root page ID, Mount Identifier and a list of mounted page IDs.
1086
     *
1087
     * @param int $rootPid
1088
     * @param string $identifier identifier of the mount point
1089
     * @param array $mountedPids An array of mounted page IDs
1090
     * @return array
1091
     *
1092
     * @throws DBALDriverException
1093
     * @throws DBALException|\Doctrine\DBAL\DBALException
1094
     */
1095 8
    public function findAllIndexQueueItemsByRootPidAndMountIdentifierAndMountedPids(
1096
        int $rootPid,
1097
        string $identifier,
1098
        array $mountedPids
1099
    ): array {
1100 8
        $queryBuilder = $this->getQueryBuilder();
1101 8
        return $queryBuilder
1102 8
            ->select('*')
1103 8
            ->from($this->table)
1104 8
            ->where(
1105 8
                $queryBuilder->expr()->eq('root', $queryBuilder->createNamedParameter($rootPid, PDO::PARAM_INT)),
1106 8
                $queryBuilder->expr()->eq('item_type', $queryBuilder->createNamedParameter('pages')),
1107 8
                $queryBuilder->expr()->in('item_uid', $mountedPids),
1108 8
                $queryBuilder->expr()->eq('has_indexing_properties', $queryBuilder->createNamedParameter(1, PDO::PARAM_INT)),
1109 8
                $queryBuilder->expr()->eq('pages_mountidentifier', $queryBuilder->createNamedParameter($identifier))
1110 8
            )
1111 8
            ->execute()
1112 8
            ->fetchAllAssociative();
1113
    }
1114
1115
    /**
1116
     * Updates has_indexing_properties field for given Item
1117
     *
1118
     * @param int $itemUid
1119
     * @param bool $hasIndexingPropertiesFlag
1120
     * @return int number of affected rows, 1 on success
1121
     *
1122
     * @throws DBALException|\Doctrine\DBAL\DBALException
1123
     */
1124 10
    public function updateHasIndexingPropertiesFlagByItemUid(int $itemUid, bool $hasIndexingPropertiesFlag): int
1125
    {
1126 10
        $queryBuilder = $this->getQueryBuilder();
1127 10
        return (int)$queryBuilder
1128 10
            ->update($this->table)
1129 10
            ->where(
1130
                /** @scrutinizer ignore-type */
1131 10
                $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($itemUid, PDO::PARAM_INT))
1132 10
            )
1133 10
            ->set(
1134 10
                'has_indexing_properties',
1135 10
                $queryBuilder->createNamedParameter($hasIndexingPropertiesFlag, PDO::PARAM_INT),
1136 10
                false
1137 10
            )->execute();
1138
    }
1139
}
1140