Passed
Pull Request — release-11.5.x (#3544)
by Markus
32:53 queued 29:28
created

Indexer   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 755
Duplicated Lines 0 %

Test Coverage

Coverage 87.59%

Importance

Changes 7
Bugs 2 Features 0
Metric Value
wmc 87
eloc 252
c 7
b 2
f 0
dl 0
loc 755
ccs 233
cts 266
cp 0.8759
rs 2

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
A index() 0 25 3
A isAFreeContentModeItemRecord() 0 17 5
B getTranslationOverlaysWithConfiguredSite() 0 33 10
A getSolrConnectionsByItem() 0 33 5
A getPageIdOfItem() 0 6 2
A preAddModifyDocuments() 0 21 4
A getBaseDocument() 0 6 1
A indexItem() 0 35 3
A getItemTypeConfiguration() 0 13 4
A getAdditionalDocuments() 0 28 6
A getFieldConfigurationFromItemRootPage() 0 5 1
A isRootPageIdPartOfRootLine() 0 11 1
A isLanguageInAFreeContentMode() 0 12 3
A itemToDocument() 0 14 2
A getFallbackOrder() 0 11 2
A getFieldConfigurationFromItemRecordPage() 0 8 2
A getConnectionsForIndexableLanguages() 0 18 3
A getLanguageFieldFromTable() 0 9 2
A getAccessRootline() 0 17 3
B getDefaultLanguageUid() 0 14 8
A getFullItemRecord() 0 9 2
A setLogging() 0 5 1
A processDocuments() 0 16 2
B getItemRecordOverlayed() 0 24 7
A log() 0 28 4

How to fix   Complexity   

Complex Class

Complex classes like Indexer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Indexer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace ApacheSolrForTypo3\Solr\IndexQueue;
19
20
use ApacheSolrForTypo3\Solr\ConnectionManager;
21
use ApacheSolrForTypo3\Solr\Domain\Search\ApacheSolrDocument\Builder;
22
use ApacheSolrForTypo3\Solr\Domain\Site\Site;
23
use ApacheSolrForTypo3\Solr\Domain\Site\SiteRepository;
24
use ApacheSolrForTypo3\Solr\FieldProcessor\Service;
25
use ApacheSolrForTypo3\Solr\FrontendEnvironment;
26
use ApacheSolrForTypo3\Solr\FrontendEnvironment\Exception\Exception as FrontendEnvironmentException;
27
use ApacheSolrForTypo3\Solr\FrontendEnvironment\Tsfe;
28
use ApacheSolrForTypo3\Solr\IndexQueue\Exception\IndexingException;
29
use ApacheSolrForTypo3\Solr\NoSolrConnectionFoundException;
30
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
31
use ApacheSolrForTypo3\Solr\System\Records\Pages\PagesRepository;
32
use ApacheSolrForTypo3\Solr\System\Solr\Document\Document;
33
use ApacheSolrForTypo3\Solr\System\Solr\ResponseAdapter;
34
use ApacheSolrForTypo3\Solr\System\Solr\SolrConnection;
35
use Doctrine\DBAL\Driver\Exception as DBALDriverException;
36
use Doctrine\DBAL\Exception as DBALException;
37
use InvalidArgumentException;
38
use RuntimeException;
39
use Throwable;
40
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
41
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
42
use TYPO3\CMS\Core\Site\SiteFinder;
43
use TYPO3\CMS\Core\Utility\GeneralUtility;
44
use TYPO3\CMS\Core\Utility\RootlineUtility;
45
use UnexpectedValueException;
46
47
/**
48
 * A general purpose indexer to be used for indexing of any kind of regular
49
 * records like tt_news, tt_address, and so on.
50
 * Specialized indexers can extend this class to handle advanced stuff like
51
 * category resolution in tt_news or file indexing.
52
 *
53
 * @author Ingo Renner <[email protected]>
54
 * @copyright  (c) 2009-2015 Ingo Renner <[email protected]>
55
 */
56
class Indexer extends AbstractIndexer
57
{
58
    /**
59
     * A Solr service instance to interact with the Solr server
60
     *
61
     * @var SolrConnection|null
62
     */
63
    protected ?SolrConnection $solr;
64
65
    /**
66
     * @var ConnectionManager
67
     */
68
    protected ConnectionManager $connectionManager;
69
70
    /**
71
     * Holds options for a specific indexer
72
     *
73
     * @var array
74
     */
75
    protected array $options = [];
76
77
    /**
78
     * To log or not to log... #Shakespeare
79
     *
80
     * @var bool
81
     */
82
    protected bool $loggingEnabled = false;
83
84
    /**
85
     * @var SolrLogManager
86
     */
87
    protected SolrLogManager $logger;
88
89
    /**
90
     * @var PagesRepository
91
     */
92
    protected PagesRepository $pagesRepository;
93
94
    /**
95
     * @var Builder
96
     */
97
    protected Builder $documentBuilder;
98
99
    /**
100
     * @var FrontendEnvironment
101
     */
102
    protected FrontendEnvironment $frontendEnvironment;
103
104
    /**
105
     * Constructor
106
     *
107
     * @param array $options array of indexer options
108
     * @param PagesRepository|null $pagesRepository
109
     * @param Builder|null $documentBuilder
110
     * @param SolrLogManager|null $logger
111
     * @param ConnectionManager|null $connectionManager
112
     * @param FrontendEnvironment|null $frontendEnvironment
113
     */
114 51
    public function __construct(
115
        array $options = [],
116
        PagesRepository $pagesRepository = null,
117
        Builder $documentBuilder = null,
118
        SolrLogManager $logger = null,
119
        ConnectionManager $connectionManager = null,
120
        FrontendEnvironment $frontendEnvironment = null
121
    ) {
122 51
        $this->options = $options;
123 51
        $this->pagesRepository = $pagesRepository ?? GeneralUtility::makeInstance(PagesRepository::class);
124 51
        $this->documentBuilder = $documentBuilder ?? GeneralUtility::makeInstance(Builder::class);
125 51
        $this->logger = $logger ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
126 51
        $this->connectionManager = $connectionManager ?? GeneralUtility::makeInstance(ConnectionManager::class);
127 51
        $this->frontendEnvironment = $frontendEnvironment ?? GeneralUtility::makeInstance(FrontendEnvironment::class);
128
    }
129
130
    /**
131
     * Indexes an item from the indexing queue.
132
     *
133
     * @param Item $item An index queue item
134
     * @return bool returns true when indexed, false when not
135
     * @throws DBALDriverException
136
     * @throws DBALException
137
     * @throws FrontendEnvironmentException
138
     * @throws NoSolrConnectionFoundException
139
     * @throws SiteNotFoundException
140
     */
141 20
    public function index(Item $item): bool
142
    {
143 20
        $indexed = true;
144
145 20
        $this->type = $item->getType();
146 20
        $this->setLogging($item);
147
148 20
        $solrConnections = $this->getSolrConnectionsByItem($item);
149 20
        foreach ($solrConnections as $systemLanguageUid => $solrConnection) {
150 20
            $this->solr = $solrConnection;
151
152 20
            if (!$this->indexItem($item, (int)$systemLanguageUid)) {
153
                /*
154
                 * A single language voting for "not indexed" should make the whole
155
                 * item count as being not indexed, even if all other languages are
156
                 * indexed.
157
                 * If there is no translation for a single language, this item counts
158
                 * as TRUE since it's not an error which that should make the item
159
                 * being reindexed during another index run.
160
                 */
161
                $indexed = false;
162
            }
163
        }
164
165 20
        return $indexed;
166
    }
167
168
    /**
169
     * Creates a single Solr Document for an item in a specific language.
170
     *
171
     * @param Item $item An index queue item to index.
172
     * @param int $language The language to use.
173
     * @return bool TRUE if item was indexed successfully, FALSE on failure
174
     * @throws DBALDriverException
175
     * @throws DBALException
176
     * @throws FrontendEnvironmentException
177
     * @throws IndexingException
178
     * @throws SiteNotFoundException
179
     */
180 22
    protected function indexItem(Item $item, int $language = 0): bool
181
    {
182 22
        $itemIndexed = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $itemIndexed is dead and can be removed.
Loading history...
183 22
        $documents = [];
184
185 22
        $itemDocument = $this->itemToDocument($item, $language);
186 22
        if (is_null($itemDocument)) {
187
            /*
188
             * If there is no itemDocument, this means there was no translation
189
             * for this record. This should not stop the current item to count as
190
             * being valid because not-indexing not-translated items is perfectly
191
             * fine.
192
             */
193
            return true;
194
        }
195
196 22
        $documents[] = $itemDocument;
197 22
        $documents = array_merge($documents, $this->getAdditionalDocuments($item, $language, $itemDocument));
198 22
        $documents = $this->processDocuments($item, $documents);
199 22
        $documents = self::preAddModifyDocuments($item, $language, $documents);
200
201 22
        $response = $this->solr->getWriteService()->addDocuments($documents);
0 ignored issues
show
Bug introduced by
The method getWriteService() does not exist on null. ( Ignorable by Annotation )

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

201
        $response = $this->solr->/** @scrutinizer ignore-call */ getWriteService()->addDocuments($documents);

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

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

Loading history...
202 22
        if ($response->getHttpStatus() === 200) {
203 21
            $itemIndexed = true;
204
        } else {
205 1
            $responseData = json_decode($response->getRawResponse() ?? '', true);
206 1
            throw new IndexingException(
207 1
                $response->getHttpStatusMessage() . ': ' . ($responseData['error']['msg'] ?? $response->getHttpStatus()),
208 1
                1678693955
209 1
            );
210
        }
211
212 21
        $this->log($item, $documents, $response);
213
214 21
        return $itemIndexed;
215
    }
216
217
    /**
218
     * Gets the full item record.
219
     *
220
     * This general record indexer simply gets the record from the item. Other
221
     * more specialized indexers may provide more data for their specific item
222
     * types.
223
     *
224
     * @param Item $item The item to be indexed
225
     * @param int $language Language Id (sys_language.uid)
226
     * @return array|null The full record with fields of data to be used for indexing or NULL to prevent an item from being indexed
227
     * @throws DBALDriverException
228
     * @throws FrontendEnvironmentException
229
     * @throws SiteNotFoundException
230
     */
231 20
    protected function getFullItemRecord(Item $item, int $language = 0): ?array
232
    {
233 20
        $itemRecord = $this->getItemRecordOverlayed($item, $language);
234
235 20
        if (!is_null($itemRecord)) {
236 20
            $itemRecord['__solr_index_language'] = $language;
237
        }
238
239 20
        return $itemRecord;
240
    }
241
242
    /**
243
     * Returns the overlaid item record.
244
     *
245
     * @param Item $item
246
     * @param int $language
247
     * @return array|mixed|null
248
     * @throws DBALDriverException
249
     * @throws FrontendEnvironmentException
250
     * @throws SiteNotFoundException
251
     */
252 20
    protected function getItemRecordOverlayed(Item $item, int $language): ?array
253
    {
254 20
        $itemRecord = $item->getRecord();
255 20
        $languageField = $GLOBALS['TCA'][$item->getType()]['ctrl']['languageField'] ?? null;
256
        // skip "free content mode"-record for other languages, if item is a "free content mode"-record
257 20
        if ($this->isAFreeContentModeItemRecord($item)
258 20
            && isset($languageField)
259 20
            && (int)($itemRecord[$languageField] ?? null) !== $language
260
        ) {
261
            return null;
262
        }
263
        // skip fallback for "free content mode"-languages
264 20
        if ($this->isLanguageInAFreeContentMode($item, $language)
265 20
            && isset($languageField)
266 20
            && (int)($itemRecord[$languageField] ?? null) !== $language
267
        ) {
268
            return null;
269
        }
270
271 20
        $pidToUse = $this->getPageIdOfItem($item);
272
273 20
        return GeneralUtility::makeInstance(Tsfe::class)
274 20
            ->getTsfeByPageIdAndLanguageId($pidToUse, $language, $item->getRootPageUid())
275 20
            ->sys_page->getLanguageOverlay($item->getType(), $itemRecord);
276
    }
277
278
    /**
279
     * @param Item $item
280
     *
281
     * @return bool
282
     */
283 20
    protected function isAFreeContentModeItemRecord(Item $item): bool
284
    {
285 20
        $languageField = $GLOBALS['TCA'][$item->getType()]['ctrl']['languageField'] ?? null;
286 20
        $itemRecord = $item->getRecord();
287
288 20
        $l10nParentField = $GLOBALS['TCA'][$item->getType()]['ctrl']['transOrigPointerField'] ?? null;
289 20
        if ($languageField === null || $l10nParentField === null) {
290
            return true;
291
        }
292 20
        $languageOfRecord = (int)($itemRecord[$languageField] ?? null);
293 20
        $l10nParentRecordUid = (int)($itemRecord[$l10nParentField] ?? null);
294
295 20
        if ($languageOfRecord > 0 && $l10nParentRecordUid === 0) {
296
            return true;
297
        }
298
299 20
        return false;
300
    }
301
302
    /**
303
     * Gets the configuration how to process an item's fields for indexing.
304
     *
305
     * @param Item $item An index queue item
306
     * @param int $language Language ID
307
     * @return array Configuration array from TypoScript
308
     * @throws DBALDriverException
309
     */
310 20
    protected function getItemTypeConfiguration(Item $item, int $language = 0): array
311
    {
312 20
        $indexConfigurationName = $item->getIndexingConfigurationName();
313 20
        $fields = $this->getFieldConfigurationFromItemRecordPage($item, $language, $indexConfigurationName);
314 20
        if (!$this->isRootPageIdPartOfRootLine($item) || count($fields) === 0) {
315 2
            $fields = $this->getFieldConfigurationFromItemRootPage($item, $language, $indexConfigurationName);
316 2
            if (count($fields) === 0) {
317
                throw new RuntimeException('The item indexing configuration "' . $item->getIndexingConfigurationName() .
318
                    '" on root page uid ' . $item->getRootPageUid() . ' could not be found!', 1455530112);
319
            }
320
        }
321
322 20
        return $fields;
323
    }
324
325
    /**
326
     * The method retrieves the field configuration of the items record page id (pid).
327
     *
328
     * @param Item $item
329
     * @param int $language
330
     * @param string $indexConfigurationName
331
     * @return array
332
     */
333 20
    protected function getFieldConfigurationFromItemRecordPage(Item $item, int $language, string $indexConfigurationName): array
334
    {
335
        try {
336 20
            $pageId = $this->getPageIdOfItem($item);
337 20
            $solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId($pageId, $language, $item->getRootPageUid());
338 20
            return $solrConfiguration->getIndexQueueFieldsConfigurationByConfigurationName($indexConfigurationName, []);
339
        } catch (Throwable $e) {
340
            return [];
341
        }
342
    }
343
344
    /**
345
     * @param Item $item
346
     * @return int
347
     */
348 20
    protected function getPageIdOfItem(Item $item): int
349
    {
350 20
        if ($item->getType() === 'pages') {
351 2
            return $item->getRecordUid();
352
        }
353 18
        return $item->getRecordPageId();
354
    }
355
356
    /**
357
     * The method returns the field configuration of the items root page id (uid of the related root page).
358
     *
359
     * @param Item $item
360
     * @param int $language
361
     * @param string $indexConfigurationName
362
     * @return array
363
     * @throws DBALDriverException
364
     */
365 2
    protected function getFieldConfigurationFromItemRootPage(Item $item, int $language, string $indexConfigurationName): array
366
    {
367 2
        $solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId($item->getRootPageUid(), $language);
0 ignored issues
show
Bug introduced by
It seems like $item->getRootPageUid() can also be of type null; however, parameter $pageId of ApacheSolrForTypo3\Solr\...nfigurationFromPageId() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

367
        $solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId(/** @scrutinizer ignore-type */ $item->getRootPageUid(), $language);
Loading history...
368
369 2
        return $solrConfiguration->getIndexQueueFieldsConfigurationByConfigurationName($indexConfigurationName, []);
370
    }
371
372
    /**
373
     * In case of additionalStoragePid config recordPageId can be outside siteroot.
374
     * In that case we should not read TS config of foreign siteroot.
375
     *
376
     * @param Item $item
377
     * @return bool
378
     */
379 20
    protected function isRootPageIdPartOfRootLine(Item $item): bool
380
    {
381 20
        $rootPageId = (int)$item->getRootPageUid();
382 20
        $buildRootlineWithPid = $this->getPageIdOfItem($item);
383 20
        $rootlineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $buildRootlineWithPid);
384 20
        $rootline = $rootlineUtility->get();
385
386 20
        $pageInRootline = array_filter($rootline, function ($page) use ($rootPageId) {
387 20
            return (int)$page['uid'] === $rootPageId;
388 20
        });
389 20
        return !empty($pageInRootline);
390
    }
391
392
    /**
393
     * Converts an item array (record) to a Solr document by mapping the
394
     * record's fields onto Solr document fields as configured in TypoScript.
395
     *
396
     * @param Item $item An index queue item
397
     * @param int $language Language Id
398
     *
399
     * @return Document|null The Solr document converted from the record
400
     *
401
     * @throws DBALDriverException
402
     * @throws FrontendEnvironmentException
403
     * @throws SiteNotFoundException
404
     */
405 20
    protected function itemToDocument(Item $item, int $language = 0): ?Document
406
    {
407 20
        $document = null;
408
409 20
        $itemRecord = $this->getFullItemRecord($item, $language);
410 20
        if (!is_null($itemRecord)) {
411 20
            $itemIndexingConfiguration = $this->getItemTypeConfiguration($item, $language);
412 20
            $document = $this->getBaseDocument($item, $itemRecord);
413 20
            $pidToUse = $this->getPageIdOfItem($item);
414 20
            $tsfe = GeneralUtility::makeInstance(Tsfe::class)->getTsfeByPageIdAndLanguageId($pidToUse, $language, $item->getRootPageUid());
415 20
            $document = $this->addDocumentFieldsFromTyposcript($document, $itemIndexingConfiguration, $itemRecord, $tsfe);
416
        }
417
418 20
        return $document;
419
    }
420
421
    /**
422
     * Creates a Solr document with the basic / core fields set already.
423
     *
424
     * @param Item $item The item to index
425
     * @param array $itemRecord The record to use to build the base document
426
     * @return Document A basic Solr document
427
     */
428 20
    protected function getBaseDocument(Item $item, array $itemRecord): Document
429
    {
430 20
        $type = $item->getType();
431 20
        $rootPageUid = $item->getRootPageUid();
432 20
        $accessRootLine = $this->getAccessRootline($item);
433 20
        return $this->documentBuilder->fromRecord($itemRecord, $type, $rootPageUid, $accessRootLine);
0 ignored issues
show
Bug introduced by
It seems like $rootPageUid can also be of type null; however, parameter $rootPageUid of ApacheSolrForTypo3\Solr\...t\Builder::fromRecord() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

433
        return $this->documentBuilder->fromRecord($itemRecord, $type, /** @scrutinizer ignore-type */ $rootPageUid, $accessRootLine);
Loading history...
Bug introduced by
It seems like $type can also be of type null; however, parameter $type of ApacheSolrForTypo3\Solr\...t\Builder::fromRecord() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

433
        return $this->documentBuilder->fromRecord($itemRecord, /** @scrutinizer ignore-type */ $type, $rootPageUid, $accessRootLine);
Loading history...
434
    }
435
436
    /**
437
     * Generates an Access Rootline for an item.
438
     *
439
     * @param Item $item Index Queue item to index.
440
     * @return mixed|string The Access Rootline for the item
441
     */
442 20
    protected function getAccessRootline(Item $item)
443
    {
444 20
        $accessRestriction = '0';
445 20
        $itemRecord = $item->getRecord();
446
447
        // TODO support access restrictions set on storage page
448
449 20
        if (isset($GLOBALS['TCA'][$item->getType()]['ctrl']['enablecolumns']['fe_group'])) {
450 2
            $accessRestriction = $itemRecord[$GLOBALS['TCA'][$item->getType()]['ctrl']['enablecolumns']['fe_group']];
451
452 2
            if (empty($accessRestriction)) {
453
                // public
454 2
                $accessRestriction = '0';
455
            }
456
        }
457
458 20
        return 'r:' . $accessRestriction;
459
    }
460
461
    /**
462
     * Sends the documents to the field processing service which takes care of
463
     * manipulating fields as defined in the field's configuration.
464
     *
465
     * @param Item $item An index queue item
466
     * @param array $documents An array of \ApacheSolrForTypo3\Solr\System\Solr\Document\Document objects to manipulate.
467
     * @return Document[] An array of manipulated Document objects.
468
     * @throws DBALDriverException
469
     * @throws DBALException
470
     */
471 20
    protected function processDocuments(Item $item, array $documents): array
472
    {
473
//        // needs to respect the TS settings for the page the item is on, conditions may apply
474
//        $solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId($item->getRootPageUid());
475
476 20
        $siteRepository = GeneralUtility::makeInstance(SiteRepository::class);
477 20
        $solrConfiguration = $siteRepository->getSiteByPageId($item->getRootPageUid())->getSolrConfiguration();
478 20
        $fieldProcessingInstructions = $solrConfiguration->getIndexFieldProcessingInstructionsConfiguration();
479
480
        // same as in the FE indexer
481 20
        if (is_array($fieldProcessingInstructions)) {
482 20
            $service = GeneralUtility::makeInstance(Service::class);
483 20
            $service->processDocuments($documents, $fieldProcessingInstructions);
484
        }
485
486 20
        return $documents;
487
    }
488
489
    /**
490
     * Allows third party extensions to provide additional documents which
491
     * should be indexed for the current item.
492
     *
493
     * @param Item $item The item currently being indexed.
494
     * @param int $language The language uid currently being indexed.
495
     * @param Document $itemDocument The document representing the item for the given language.
496
     * @return Document[] array An array of additional Document objects to index.
497
     */
498 29
    protected function getAdditionalDocuments(Item $item, int $language, Document $itemDocument): array
499
    {
500 29
        $documents = [];
501
502 29
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['IndexQueueIndexer']['indexItemAddDocuments'] ?? null)) {
503 8
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['IndexQueueIndexer']['indexItemAddDocuments'] as $classReference) {
504 8
                if (!class_exists($classReference)) {
505 3
                    throw new InvalidArgumentException('Class does not exits' . $classReference, 1490363487);
506
                }
507 5
                $additionalIndexer = GeneralUtility::makeInstance($classReference);
508 5
                if ($additionalIndexer instanceof AdditionalIndexQueueItemIndexer) {
509 3
                    $additionalDocuments = $additionalIndexer->getAdditionalItemDocuments($item, $language, $itemDocument);
510
511 3
                    if (is_array($additionalDocuments)) {
512 3
                        $documents = array_merge(
513 3
                            $documents,
514 3
                            $additionalDocuments
515 3
                        );
516
                    }
517
                } else {
518 2
                    throw new UnexpectedValueException(
519 2
                        get_class($additionalIndexer) . ' must implement interface ' . AdditionalIndexQueueItemIndexer::class,
520 2
                        1326284551
521 2
                    );
522
                }
523
            }
524
        }
525 24
        return $documents;
526
    }
527
528
    /**
529
     * Provides a hook to manipulate documents right before they get added to
530
     * the Solr index.
531
     *
532
     * @param Item $item The item currently being indexed.
533
     * @param int $language The language uid of the documents
534
     * @param array $documents An array of documents to be indexed
535
     * @return array An array of modified documents
536
     */
537 94
    public static function preAddModifyDocuments(Item $item, int $language, array $documents): array
538
    {
539 94
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['IndexQueueIndexer']['preAddModifyDocuments'] ?? null)) {
540 3
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['IndexQueueIndexer']['preAddModifyDocuments'] as $classReference) {
541 3
                $documentsModifier = GeneralUtility::makeInstance($classReference);
542
543 3
                if ($documentsModifier instanceof PageIndexerDocumentsModifier) {
544 2
                    $documents = $documentsModifier->modifyDocuments($item, $language, $documents);
545
                } else {
546 1
                    throw new RuntimeException(
547 1
                        'The class "' . get_class($documentsModifier)
548 1
                        . '" registered as document modifier in hook
549
							preAddModifyDocuments must implement interface
550 1
							ApacheSolrForTypo3\Solr\IndexQueue\PageIndexerDocumentsModifier',
551 1
                        1309522677
552 1
                    );
553
                }
554
            }
555
        }
556
557 93
        return $documents;
558
    }
559
560
    // Initialization
561
562
    /**
563
     * Gets the Solr connections applicable for an item.
564
     *
565
     * The connections include the default connection and connections to be used
566
     * for translations of an item.
567
     *
568
     * @param Item $item An index queue item
569
     * @return array An array of ApacheSolrForTypo3\Solr\System\Solr\SolrConnection connections, the array's keys are the sys_language_uid of the language of the connection
570
     * @throws DBALDriverException
571
     * @throws NoSolrConnectionFoundException
572
     */
573 23
    protected function getSolrConnectionsByItem(Item $item): array
574
    {
575 23
        $solrConnections = [];
576
577 23
        $rootPageId = $item->getRootPageUid();
578 23
        if ($item->getType() === 'pages') {
579 4
            $pageId = $item->getRecordUid();
580
        } else {
581 19
            $pageId = $item->getRecordPageId();
582
        }
583
584
        // Solr configurations possible for this item
585 23
        $site = $item->getSite();
586 23
        $solrConfigurationsBySite = $site->getAllSolrConnectionConfigurations();
587 23
        $siteLanguages = [];
588 23
        foreach ($solrConfigurationsBySite as $solrConfiguration) {
589 23
            $siteLanguages[] = $solrConfiguration['language'];
590
        }
591
592 23
        $defaultLanguageUid = $this->getDefaultLanguageUid($item, $site->getRootPage(), $siteLanguages);
593 23
        $translationOverlays = $this->getTranslationOverlaysWithConfiguredSite((int)$pageId, $site, $siteLanguages);
0 ignored issues
show
Bug introduced by
It seems like $site can also be of type null; however, parameter $site of ApacheSolrForTypo3\Solr\...aysWithConfiguredSite() does only seem to accept ApacheSolrForTypo3\Solr\Domain\Site\Site, maybe add an additional type check? ( Ignorable by Annotation )

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

593
        $translationOverlays = $this->getTranslationOverlaysWithConfiguredSite((int)$pageId, /** @scrutinizer ignore-type */ $site, $siteLanguages);
Loading history...
594
595 23
        $defaultConnection = $this->connectionManager->getConnectionByPageId($rootPageId, $defaultLanguageUid, $item->getMountPointIdentifier() ?? '');
0 ignored issues
show
Bug introduced by
It seems like $rootPageId can also be of type null; however, parameter $pageId of ApacheSolrForTypo3\Solr\...getConnectionByPageId() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

595
        $defaultConnection = $this->connectionManager->getConnectionByPageId(/** @scrutinizer ignore-type */ $rootPageId, $defaultLanguageUid, $item->getMountPointIdentifier() ?? '');
Loading history...
596 23
        $translationConnections = $this->getConnectionsForIndexableLanguages($translationOverlays);
597
598 23
        if ($defaultLanguageUid == 0) {
599 21
            $solrConnections[0] = $defaultConnection;
600
        }
601
602 23
        foreach ($translationConnections as $systemLanguageUid => $solrConnection) {
603 20
            $solrConnections[$systemLanguageUid] = $solrConnection;
604
        }
605 23
        return $solrConnections;
606
    }
607
608
    /**
609
     * @param int $pageId
610
     * @param Site $site
611
     * @param array $siteLanguages
612
     * @return array
613
     */
614 23
    protected function getTranslationOverlaysWithConfiguredSite(int $pageId, Site $site, array $siteLanguages): array
615
    {
616 23
        $translationOverlays = $this->pagesRepository->findTranslationOverlaysByPageId($pageId);
617 23
        $translatedLanguages = [];
618 23
        foreach ($translationOverlays as $key => $translationOverlay) {
619 6
            if (!in_array($translationOverlay['sys_language_uid'], $siteLanguages)) {
620
                unset($translationOverlays[$key]);
621
            } else {
622 6
                $translatedLanguages[] = (int)$translationOverlay['sys_language_uid'];
623
            }
624
        }
625
626 23
        if (count($translationOverlays) + 1 !== count($siteLanguages)) {
627
            // not all Languages are translated
628
            // add Language Fallback
629 22
            foreach ($siteLanguages as $languageId) {
630 22
                if ($languageId !== 0 && !in_array((int)$languageId, $translatedLanguages, true)) {
631 22
                    $fallbackLanguageIds = $this->getFallbackOrder($site, (int)$languageId);
632 22
                    foreach ($fallbackLanguageIds as $fallbackLanguageId) {
633 21
                        if ($fallbackLanguageId === 0 || in_array((int)$fallbackLanguageId, $translatedLanguages, true)) {
634 15
                            $translationOverlay = [
635 15
                                'pid' => $pageId,
636 15
                                'sys_language_uid' => $languageId,
637 15
                                'l10n_parent' => $pageId,
638 15
                            ];
639 15
                            $translationOverlays[] = $translationOverlay;
640 15
                            continue 2;
641
                        }
642
                    }
643
                }
644
            }
645
        }
646 23
        return $translationOverlays;
647
    }
648
649
    /**
650
     * @param Site $site
651
     * @param int $languageId
652
     * @return array
653
     */
654 22
    protected function getFallbackOrder(Site $site, int $languageId): array
655
    {
656 22
        $fallbackChain = [];
657 22
        $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
658
        try {
659 22
            $site = $siteFinder->getSiteByRootPageId($site->getRootPageId());
660 21
            $languageAspect = LanguageAspectFactory::createFromSiteLanguage($site->getLanguageById($languageId));
661 21
            $fallbackChain = $languageAspect->getFallbackChain();
662 1
        } catch (SiteNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
663
        }
664 22
        return $fallbackChain;
665
    }
666
667
    /**
668
     * @param Item $item An index queue item
669
     * @param array $rootPage
670
     * @param array $siteLanguages
671
     *
672
     * @return int
673
     * @throws RuntimeException
674
     */
675 23
    protected function getDefaultLanguageUid(Item $item, array $rootPage, array $siteLanguages): int
676
    {
677 23
        $defaultLanguageUid = 0;
678 23
        if (($rootPage['l18n_cfg'] & 1) == 1 && count($siteLanguages) == 1 && $siteLanguages[min(array_keys($siteLanguages))] > 0) {
679
            $defaultLanguageUid = $siteLanguages[min(array_keys($siteLanguages))];
680 23
        } elseif (($rootPage['l18n_cfg'] & 1) == 1 && count($siteLanguages) > 1) {
681 2
            unset($siteLanguages[array_search('0', $siteLanguages)]);
682 2
            $defaultLanguageUid = $siteLanguages[min(array_keys($siteLanguages))];
683 21
        } elseif (($rootPage['l18n_cfg'] & 1) == 1 && count($siteLanguages) == 1) {
684
            $message = 'Root page ' . (int)$item->getRootPageUid() . ' is set to hide default translation, but no other language is configured!';
685
            throw new RuntimeException($message);
686
        }
687
688 23
        return $defaultLanguageUid;
689
    }
690
691
    /**
692
     * Checks for which languages connections have been configured and returns
693
     * these connections.
694
     *
695
     * @param array $translationOverlays An array of translation overlays to check for configured connections.
696
     * @return array An array of ApacheSolrForTypo3\Solr\System\Solr\SolrConnection connections.
697
     * @throws DBALDriverException
698
     */
699 23
    protected function getConnectionsForIndexableLanguages(array $translationOverlays): array
700
    {
701 23
        $connections = [];
702
703 23
        foreach ($translationOverlays as $translationOverlay) {
704 21
            $pageId = $translationOverlay['l10n_parent'];
705 21
            $languageId = $translationOverlay['sys_language_uid'];
706
707
            try {
708 21
                $connection = $this->connectionManager->getConnectionByPageId($pageId, $languageId);
709 20
                $connections[$languageId] = $connection;
710 1
            } catch (NoSolrConnectionFoundException $e) {
711
                // ignore the exception as we seek only those connections
712
                // actually available
713
            }
714
        }
715
716 23
        return $connections;
717
    }
718
719
    // Utility methods
720
721
    // FIXME extract log() and setLogging() to ApacheSolrForTypo3\Solr\IndexQueue\AbstractIndexer
722
    // FIXME extract an interface Tx_Solr_IndexQueue_ItemInterface
723
724
    /**
725
     * Enables logging dependent on the configuration of the item's site
726
     *
727
     * @param Item $item An item being indexed
728
     * @throws DBALDriverException
729
     */
730 21
    protected function setLogging(Item $item)
731
    {
732 21
        $solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId($item->getRootPageUid());
0 ignored issues
show
Bug introduced by
It seems like $item->getRootPageUid() can also be of type null; however, parameter $pageId of ApacheSolrForTypo3\Solr\...nfigurationFromPageId() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

732
        $solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId(/** @scrutinizer ignore-type */ $item->getRootPageUid());
Loading history...
733 21
        $this->loggingEnabled = $solrConfiguration->getLoggingIndexingQueueOperationsByConfigurationNameWithFallBack(
734 21
            $item->getIndexingConfigurationName()
735 21
        );
736
    }
737
738
    /**
739
     * Logs the item and what document was created from it
740
     *
741
     * @param Item $item The item that is being indexed.
742
     * @param array $itemDocuments An array of Solr documents created from the item's data
743
     * @param ResponseAdapter $response The Solr response for the particular index document
744
     */
745 21
    protected function log(Item $item, array $itemDocuments, ResponseAdapter $response)
746
    {
747 21
        if (!$this->loggingEnabled) {
748 21
            return;
749
        }
750
751
        $message = 'Index Queue indexing ' . $item->getType() . ':' . $item->getRecordUid() . ' - ';
752
753
        // preparing data
754
        $documents = [];
755
        foreach ($itemDocuments as $document) {
756
            $documents[] = (array)$document;
757
        }
758
759
        $logData = ['item' => (array)$item, 'documents' => $documents, 'response' => (array)$response];
760
761
        if ($response->getHttpStatus() == 200) {
762
            $severity = SolrLogManager::NOTICE;
763
            $message .= 'Success';
764
        } else {
765
            $severity = SolrLogManager::ERROR;
766
            $message .= 'Failure';
767
768
            $logData['status'] = $response->getHttpStatus();
769
            $logData['status message'] = $response->getHttpStatusMessage();
770
        }
771
772
        $this->logger->log($severity, $message, $logData);
773
    }
774
775
    /**
776
     * Returns the language field from given table or null
777
     *
778
     * @param string $tableName
779
     * @return string|null
780
     */
781
    protected function getLanguageFieldFromTable(string $tableName): ?string
782
    {
783
        $tableControl = $GLOBALS['TCA'][$tableName]['ctrl'] ?? [];
784
785
        if (!empty($tableControl['languageField'])) {
786
            return $tableControl['languageField'];
787
        }
788
789
        return null;
790
    }
791
792
    /**
793
     * Checks the given language, if it is in "free" mode.
794
     *
795
     * @param Item $item
796
     * @param int $language
797
     * @return bool
798
     */
799 20
    protected function isLanguageInAFreeContentMode(Item $item, int $language): bool
800
    {
801 20
        if ($language === 0) {
802 20
            return false;
803
        }
804 18
        $typo3site = $item->getSite()->getTypo3SiteObject();
805 18
        $typo3siteLanguage = $typo3site->getLanguageById($language);
806 18
        $typo3siteLanguageFallbackType = $typo3siteLanguage->getFallbackType();
807 18
        if ($typo3siteLanguageFallbackType === 'free') {
808
            return true;
809
        }
810 18
        return false;
811
    }
812
}
813