Failed Conditions
Pull Request — main (#3585)
by Rafael
43:10
created

Typo3PageIndexer::getPageAccessRootline()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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;
19
20
use ApacheSolrForTypo3\Solr\Access\Rootline;
21
use ApacheSolrForTypo3\Solr\Domain\Search\ApacheSolrDocument\Builder;
22
use ApacheSolrForTypo3\Solr\FieldProcessor\Service;
23
use ApacheSolrForTypo3\Solr\IndexQueue\FrontendHelper\PageFieldMappingIndexer;
24
use ApacheSolrForTypo3\Solr\IndexQueue\Indexer;
25
use ApacheSolrForTypo3\Solr\IndexQueue\Item;
26
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
27
use ApacheSolrForTypo3\Solr\System\Logging\DebugWriter;
28
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
29
use ApacheSolrForTypo3\Solr\System\Solr\Document\Document;
30
use ApacheSolrForTypo3\Solr\System\Solr\SolrConnection;
31
use Doctrine\DBAL\Exception as DBALException;
32
use RuntimeException;
33
use Throwable;
34
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
35
use TYPO3\CMS\Core\Utility\GeneralUtility;
0 ignored issues
show
Bug introduced by
The type TYPO3\CMS\Core\Utility\GeneralUtility was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
37
use UnexpectedValueException;
38
39
/**
40
 * Page Indexer to index TYPO3 pages used by the Index Queue.
41
 *
42
 * @author Ingo Renner <[email protected]>
43
 * @author Daniel Poetzinger <[email protected]>
44
 * @author Timo Schmidt <[email protected]>
45
 */
46
class Typo3PageIndexer
47
{
48
    /**
49
     * ID of the current page's Solr document.
50
     */
51
    protected static string $pageSolrDocumentId = '';
52
53
    /**
54
     * The Solr document generated for the current page.
55
     */
56
    protected static Document $pageSolrDocument;
57
58
    /**
59
     * The mount point parameter used in the Frontend controller.
60
     */
61
    protected string $mountPointParameter = '';
62
63
    /**
64
     * Solr server connection.
65
     */
66
    protected ?SolrConnection $solrConnection = null;
67
68
    /**
69
     * Frontend page object (TSFE).
70
     */
71
    protected TypoScriptFrontendController $page;
72
73
    /**
74
     * URL to be indexed as the page's URL
75
     */
76
    protected string $pageUrl = '';
77
78
    /**
79
     * The page's access rootline
80
     */
81
    protected Rootline $pageAccessRootline;
82
83
    /**
84
     * Documents that have been sent to Solr
85
     */
86
    protected array $documentsSentToSolr = [];
87
88
    protected TypoScriptConfiguration $configuration;
89
90
    protected Item $indexQueueItem;
91
92
    protected SolrLogManager $logger;
93
94
    /**
95
     * Constructor
96
     *
97
     * @param TypoScriptFrontendController $page The page to index
98
     */
99 69
    public function __construct(TypoScriptFrontendController $page)
100
    {
101 69
        $this->logger = new SolrLogManager(__CLASS__, GeneralUtility::makeInstance(DebugWriter::class));
102
103 69
        $this->page = $page;
104 69
        $this->pageUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL');
105 69
        $this->configuration = Util::getSolrConfiguration();
106
107
        try {
108 69
            $this->initializeSolrConnection();
109
        } catch (Throwable $e) {
110
            $this->logger->log(
111
                SolrLogManager::ERROR,
112
                $e->getMessage() . ' Error code: ' . $e->getCode()
113
            );
114
115
            // TODO extract to a class "ExceptionLogger"
116
            if ($this->configuration->getLoggingExceptions()) {
117
                $this->logger->log(
118
                    SolrLogManager::ERROR,
119
                    'Exception while trying to index a page',
120
                    [
121
                        $e->__toString(),
122
                    ]
123
                );
124
            }
125
        }
126
127 69
        $this->pageAccessRootline = GeneralUtility::makeInstance(Rootline::class, /** @scrutinizer ignore-type */ '');
128
    }
129
130 69
    public function setIndexQueueItem(Item $indexQueueItem): void
131
    {
132 69
        $this->indexQueueItem = $indexQueueItem;
133
    }
134
135
    /**
136
     * Initializes the Solr server connection.
137
     *
138
     * @throws AspectNotFoundException
139
     * @throws DBALException
140
     * @throws Exception
141
     * @throws NoSolrConnectionFoundException
142
     */
143 69
    protected function initializeSolrConnection(): void
144
    {
145 69
        $solr = GeneralUtility::makeInstance(ConnectionManager::class)->getConnectionByPageId($this->page->id, Util::getLanguageUid());
146
147
        // do not continue if no server is available
148 69
        if (!$solr->getWriteService()->ping()) {
149
            throw new Exception(
150
                'No Solr instance available while trying to index a page.',
151
                1234790825
152
            );
153
        }
154
155 69
        $this->solrConnection = $solr;
156
    }
157
158
    /**
159
     * Gets the current page's Solr document ID.
160
     *
161
     * @return string The page's Solr document ID or empty string in case no document was generated yet.
162
     */
163
    public static function getPageSolrDocumentId(): string
164
    {
165
        return self::$pageSolrDocumentId;
166
    }
167
168
    /**
169
     * Gets the Solr document generated for the current page.
170
     *
171
     * @return Document|null The page's Solr document or NULL if it has not been generated yet.
172
     */
173 43
    public static function getPageSolrDocument(): ?Document
174
    {
175 43
        return self::$pageSolrDocument;
176
    }
177
178
    /**
179
     * Allows to provide a Solr server connection other than the one
180
     * initialized by the constructor.
181
     *
182
     * @param SolrConnection $solrConnection Solr connection
183
     * @throws Exception if the Solr server cannot be reached
184
     */
185 43
    public function setSolrConnection(SolrConnection $solrConnection): void
186
    {
187 43
        if (!$solrConnection->getWriteService()->ping()) {
188
            throw new Exception(
189
                'Could not connect to Solr server.',
190
                1323946472
191
            );
192
        }
193
194 43
        $this->solrConnection = $solrConnection;
195
    }
196
197
    /**
198
     * Indexes a page.
199
     *
200
     * @return bool TRUE after successfully indexing the page, FALSE on error
201
     *
202
     * @throws AspectNotFoundException
203
     * @throws DBALException
204
     * @throws Exception
205
     */
206 69
    public function indexPage(): bool
207
    {
208 69
        $documents = []; // this will become useful as soon as when starting to index individual records instead of whole pages
209
210 69
        if (is_null($this->solrConnection)) {
211
            // intended early return as it doesn't make sense to continue
212
            // and waste processing time if the solr server isn't available
213
            // anyways
214
            // FIXME use an exception
215
            return false;
216
        }
217
218 69
        $pageDocument = $this->getPageDocument();
219 69
        $pageDocument = $this->substitutePageDocument($pageDocument);
220
221 69
        self::$pageSolrDocument = $pageDocument;
222
        $documents[] = $pageDocument;
223 69
        $documents = $this->getAdditionalDocuments($pageDocument, $documents);
224 69
        $this->processDocuments($documents);
225 69
        $documents = Indexer::preAddModifyDocuments(
226 69
            $this->indexQueueItem,
227 69
            $this->page->getLanguage()->getLanguageId(),
228 69
            $documents
229 69
        );
230 69
231 69
        $pageIndexed = $this->addDocumentsToSolrIndex($documents);
232
        $this->documentsSentToSolr = $documents;
233 69
234 69
        return $pageIndexed;
235
    }
236 69
237
    /**
238
     * Builds the Solr document for the current page.
239
     *
240
     * @return Document A document representing the page
241
     *
242
     * @throws AspectNotFoundException
243
     * @throws DBALException
244 69
     */
245
    protected function getPageDocument(): Document
246 69
    {
247 69
        $documentBuilder = GeneralUtility::makeInstance(Builder::class);
248
        $document = $documentBuilder->fromPage($this->page, $this->pageUrl, $this->pageAccessRootline, $this->mountPointParameter);
249
250
        self::$pageSolrDocumentId = $document['id'];
251
252
        return $document;
253
    }
254
255
    // Logging
256
    // TODO replace by a central logger
257
258
    /**
259
     * Gets the mount point parameter that is used in the Frontend controller.
260
     */
261
    public function getMountPointParameter(): string
262
    {
263
        return $this->mountPointParameter;
264
    }
265
266
    // Misc
267
268
    /**
269
     * Sets the mount point parameter that is used in the Frontend controller.
270
     */
271
    public function setMountPointParameter(string $mountPointParameter): void
272
    {
273 69
        $this->mountPointParameter = $mountPointParameter;
274
    }
275 69
276 69
    /**
277
     * Allows third party extensions to replace or modify the page document
278 69
     * created by this indexer.
279
     *
280 69
     * @param Document $pageDocument The page document created by this indexer.
281
     * @return Document An Apache Solr document representing the currently indexed page
282
     */
283
    protected function substitutePageDocument(Document $pageDocument): Document
284
    {
285
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['Indexer']['indexPageSubstitutePageDocument'] ?? null)) {
286
            return $pageDocument;
287
        }
288
289
        $indexConfigurationName = $this->getIndexConfigurationNameForCurrentPage();
290
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['Indexer']['indexPageSubstitutePageDocument'] as $classReference) {
291
            $substituteIndexer = GeneralUtility::makeInstance($classReference);
292
293
            if (!$substituteIndexer instanceof SubstitutePageIndexer) {
294
                $message = get_class($substituteIndexer) . ' must implement interface ' . SubstitutePageIndexer::class;
295
                throw new UnexpectedValueException($message, 1310491001);
296
            }
297
298
            if ($substituteIndexer instanceof PageFieldMappingIndexer) {
299 43
                $substituteIndexer->setPageIndexingConfigurationName($indexConfigurationName);
300
            }
301 43
302
            $substituteDocument = $substituteIndexer->getPageDocument($pageDocument);
303
            $pageDocument = $substituteDocument;
304
        }
305
306
        return $pageDocument;
307
    }
308
309
    /**
310
     * Retrieves the indexConfigurationName from the related queueItem, or falls back to pages when no queue item set.
311 69
     */
312
    protected function getIndexConfigurationNameForCurrentPage(): string
313 69
    {
314
        return isset($this->indexQueueItem) ? $this->indexQueueItem->getIndexingConfigurationName() : 'pages';
315
    }
316
317 69
    /**
318 69
     * Allows third party extensions to provide additional documents which
319 69
     * should be indexed for the current page.
320
     *
321 69
     * @param Document $pageDocument The main document representing this page.
322
     * @param Document[] $existingDocuments An array of documents already created for this page.
323
     * @return array An array of additional Document objects to index
324
     */
325
    protected function getAdditionalDocuments(Document $pageDocument, array $existingDocuments): array
326 69
    {
327 43
        $documents = $existingDocuments;
328
329
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['Indexer']['indexPageAddDocuments'] ?? null)) {
330 69
            return $documents;
331 69
        }
332
333
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['Indexer']['indexPageAddDocuments'] as $classReference) {
334 69
            $additionalIndexer = GeneralUtility::makeInstance($classReference);
335
336
            if (!$additionalIndexer instanceof AdditionalPageIndexer) {
337
                $message = get_class($additionalIndexer) . ' must implement interface ' . AdditionalPageIndexer::class;
338
                throw new UnexpectedValueException($message, 1310491024);
339
            }
340 69
341
            $additionalDocuments = $additionalIndexer->getAdditionalPageDocuments($pageDocument, $documents);
342 69
            if (!empty($additionalDocuments)) {
343
                $documents = array_merge($documents, $additionalDocuments);
344
            }
345
        }
346
347
        return $documents;
348
    }
349
350
    /**
351
     * Sends the given documents to the field processing service which takes
352
     * care of manipulating fields as defined in the field's configuration.
353 69
     *
354
     * @param array $documents An array of documents to manipulate
355 69
     * @throws DBALException
356
     * @throws Exception
357 69
     */
358 59
    protected function processDocuments(array $documents): void
359
    {
360
        $processingInstructions = $this->configuration->getIndexFieldProcessingInstructionsConfiguration();
361 10
        if (count($processingInstructions) > 0) {
362 10
            $service = GeneralUtility::makeInstance(Service::class);
363
            $service->processDocuments($documents, $processingInstructions);
364 10
        }
365
    }
366
367
    /**
368
     * Adds the collected documents to the Solr index.
369 10
     *
370 10
     * @param array $documents An array of Document objects.
371 1
     * @return bool TRUE if documents were added successfully, FALSE otherwise
372
     */
373
    protected function addDocumentsToSolrIndex(array $documents): bool
374
    {
375 10
        $documentsAdded = false;
376
377
        if (!count($documents)) {
378
            return false;
379
        }
380
381
        try {
382
            $this->logger->log(SolrLogManager::INFO, 'Adding ' . count($documents) . ' documents.', $documents);
383
384
            // chunk adds by 20
385
            $documentChunks = array_chunk($documents, 20);
386 69
            foreach ($documentChunks as $documentChunk) {
387
                $response = $this->solrConnection->getWriteService()->addDocuments($documentChunk);
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

387
                $response = $this->solrConnection->/** @scrutinizer ignore-call */ getWriteService()->addDocuments($documentChunk);

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...
388 69
                if ($response->getHttpStatus() != 200) {
389 69
                    throw new RuntimeException('Solr Request failed.', 1331834983);
390 69
                }
391 69
            }
392
393
            $documentsAdded = true;
394
        } catch (Throwable $e) {
395
            $this->logger->log(SolrLogManager::ERROR, $e->getMessage() . ' Error code: ' . $e->getCode());
396
397
            if ($this->configuration->getLoggingExceptions()) {
398
                $this->logger->log(SolrLogManager::ERROR, 'Exception while adding documents', [$e->__toString()]);
399
            }
400
        }
401 69
402
        return $documentsAdded;
403 69
    }
404
405 69
    /**
406
     * Gets the current page's URL.
407
     *
408
     * @return string URL of the current page.
409
     */
410 69
    public function getPageUrl(): string
411
    {
412
        return $this->pageUrl;
413 69
    }
414 69
415 69
    /**
416 69
     * Sets the URL to use for the page document.
417
     *
418
     * @param string $url The page's URL.
419
     */
420
    public function setPageUrl(string $url): void
421 69
    {
422
        $this->pageUrl = $url;
423
    }
424
425
    /**
426
     * Gets the page's access rootline.
427
     *
428
     * @return Rootline The page's access rootline
429
     */
430 69
    public function getPageAccessRootline(): Rootline
431
    {
432
        return $this->pageAccessRootline;
433
    }
434
435
    /**
436
     * Sets the page's access rootline.
437
     *
438
     * @param Rootline $accessRootline The page's access rootline
439
     */
440
    public function setPageAccessRootline(Rootline $accessRootline): void
441
    {
442
        $this->pageAccessRootline = $accessRootline;
443
    }
444
445
    /**
446
     * Gets the documents that have been sent to Solr
447
     *
448 43
     * @return array An array of Document objects
449
     */
450 43
    public function getDocumentsSentToSolr(): array
451
    {
452
        return $this->documentsSentToSolr;
453
    }
454
}
455