Passed
Branch main (7dbc49)
by Rafael
04:08
created

QueueItemRepository::containsItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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