Passed
Push — release-11.5.x ( 39fc07...8ccd81 )
by Markus
34:52 queued 29:33
created

IndexService   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 275
Duplicated Lines 0 %

Test Coverage

Coverage 94.19%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 25
eloc 81
dl 0
loc 275
ccs 81
cts 86
cp 0.9419
rs 10
c 1
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A generateIndexingErrorLog() 0 9 1
A indexItem() 0 30 5
A getIndexerByItem() 0 16 2
A initializeHttpServerEnvironment() 0 14 2
A emitSignal() 0 3 1
A getProgress() 0 3 1
A getFailCount() 0 3 1
A restoreOriginalHttpHost() 0 10 2
A __construct() 0 10 1
A getContextTask() 0 3 1
B indexItems() 0 38 7
A setContextTask() 0 3 1
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace ApacheSolrForTypo3\Solr\Domain\Index;
17
18
use ApacheSolrForTypo3\Solr\ConnectionManager;
19
use ApacheSolrForTypo3\Solr\Domain\Site\Site;
20
use ApacheSolrForTypo3\Solr\IndexQueue\Indexer;
21
use ApacheSolrForTypo3\Solr\IndexQueue\Item;
22
use ApacheSolrForTypo3\Solr\IndexQueue\Queue;
23
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
24
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
25
use ApacheSolrForTypo3\Solr\Task\IndexQueueWorkerTask;
26
use RuntimeException;
27
use Throwable;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
30
use TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException;
31
use TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException;
32
33
/**
34
 * Service to perform indexing operations
35
 *
36
 * @author Timo Hund <[email protected]>
37
 */
38
class IndexService
39
{
40
    /**
41
     * @var Site
42
     */
43
    protected Site $site;
44
45
    /**
46
     * @var IndexQueueWorkerTask|null
47
     */
48
    protected ?IndexQueueWorkerTask $contextTask = null;
49
50
    /**
51
     * @var Queue
52
     */
53
    protected Queue $indexQueue;
54
55
    /**
56
     * @var Dispatcher
57
     */
58
    protected $signalSlotDispatcher;
59
60
    /**
61
     * @var SolrLogManager
62
     */
63
    protected SolrLogManager $logger;
64
65
    /**
66
     * IndexService constructor.
67
     * @param Site $site
68
     * @param Queue|null $queue
69
     * @param Dispatcher|null $dispatcher
70
     * @param SolrLogManager|null $solrLogManager
71
     */
72 6
    public function __construct(
73
        Site $site,
74
        Queue $queue = null,
75
        Dispatcher $dispatcher = null,
76
        SolrLogManager $solrLogManager = null
77
    ) {
78 6
        $this->site = $site;
79 6
        $this->indexQueue = $queue ?? GeneralUtility::makeInstance(Queue::class);
80 6
        $this->signalSlotDispatcher = $dispatcher ?? GeneralUtility::makeInstance(Dispatcher::class);
81 6
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
82
    }
83
84
    /**
85
     * @param IndexQueueWorkerTask $contextTask
86
     */
87 1
    public function setContextTask(IndexQueueWorkerTask $contextTask)
88
    {
89 1
        $this->contextTask = $contextTask;
90
    }
91
92
    /**
93
     * @return IndexQueueWorkerTask
94
     */
95 4
    public function getContextTask(): ?IndexQueueWorkerTask
96
    {
97 4
        return $this->contextTask;
98
    }
99
100
    /**
101
     * Indexes items from the Index Queue.
102
     *
103
     * @param int $limit
104
     * @return bool
105
     * @throws InvalidSlotException
106
     * @throws InvalidSlotReturnException
107
     */
108 4
    public function indexItems(int $limit): bool
109
    {
110 4
        $errors     = 0;
111 4
        $indexRunId = uniqid();
112 4
        $configurationToUse = $this->site->getSolrConfiguration();
113 4
        $enableCommitsSetting = $configurationToUse->getEnableCommits();
114
115
        // get items to index
116 4
        $itemsToIndex = $this->indexQueue->getItemsToIndex($this->site, $limit);
117
118 4
        $this->emitSignal('beforeIndexItems', [$itemsToIndex, $this->getContextTask(), $indexRunId]);
119
120 4
        foreach ($itemsToIndex as $itemToIndex) {
121
            try {
122
                // try indexing
123 4
                $this->emitSignal('beforeIndexItem', [$itemToIndex, $this->getContextTask(), $indexRunId]);
124 4
                $this->indexItem($itemToIndex, $configurationToUse);
125 3
                $this->emitSignal('afterIndexItem', [$itemToIndex, $this->getContextTask(), $indexRunId]);
126 1
            } catch (Throwable $e) {
127 1
                $errors++;
128 1
                $this->indexQueue->markItemAsFailed($itemToIndex, $e->getCode() . ': ' . $e->__toString());
129 1
                $this->generateIndexingErrorLog($itemToIndex, $e);
130
            }
131
        }
132
133 4
        $this->emitSignal('afterIndexItems', [$itemsToIndex, $this->getContextTask(), $indexRunId]);
134
135 4
        if ($enableCommitsSetting && count($itemsToIndex) > 0) {
136 1
            $solrServers = GeneralUtility::makeInstance(ConnectionManager::class)->getConnectionsBySite($this->site);
137 1
            foreach ($solrServers as $solrServer) {
138 1
                $response = $solrServer->getWriteService()->commit(false, false);
139 1
                if ($response->getHttpStatus() !== 200) {
140
                    $errors++;
141
                }
142
            }
143
        }
144
145 4
        return $errors === 0;
146
    }
147
148
    /**
149
     * Generates a message in the error log when an error occurred.
150
     *
151
     * @param Item $itemToIndex
152
     * @param Throwable $e
153
     */
154 1
    protected function generateIndexingErrorLog(Item $itemToIndex, Throwable $e)
155
    {
156 1
        $message = 'Failed indexing Index Queue item ' . $itemToIndex->getIndexQueueUid();
157 1
        $data = ['code' => $e->getCode(), 'message' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'item' => (array)$itemToIndex];
158
159 1
        $this->logger->log(
160 1
            SolrLogManager::ERROR,
161 1
            $message,
162 1
            $data
163 1
        );
164
    }
165
166
    /**
167
     * Builds an emits a signal for the IndexService.
168
     *
169
     * @param string $name
170
     * @param array $arguments
171
     * @return mixed
172
     * @throws InvalidSlotException
173
     * @throws InvalidSlotReturnException
174
     */
175 4
    protected function emitSignal(string $name, array $arguments = [])
176
    {
177 4
        return $this->signalSlotDispatcher->dispatch(__CLASS__, $name, $arguments);
178
    }
179
180
    /**
181
     * Indexes an item from the Index Queue.
182
     *
183
     * @param Item $item An index queue item to index
184
     * @param TypoScriptConfiguration $configuration
185
     * @return bool TRUE if the item was successfully indexed, FALSE otherwise
186
     * @throws Throwable
187
     */
188 3
    protected function indexItem(Item $item, TypoScriptConfiguration $configuration): bool
189
    {
190 3
        $indexer = $this->getIndexerByItem($item->getIndexingConfigurationName(), $configuration);
191
        // Remember original http host value
192 3
        $originalHttpHost = $_SERVER['HTTP_HOST'] ?? null;
193
194 3
        $itemChangedDate = $item->getChanged();
195 3
        $itemChangedDateAfterIndex = 0;
196
197
        try {
198 3
            $this->initializeHttpServerEnvironment($item);
199 3
            $itemIndexed = $indexer->index($item);
200
201
            // update IQ item so that the IQ can determine what's been indexed already
202 2
            if ($itemIndexed) {
203 2
                $this->indexQueue->updateIndexTimeByItem($item);
204 2
                $itemChangedDateAfterIndex = $item->getChanged();
205
            }
206
207 2
            if ($itemChangedDateAfterIndex > $itemChangedDate && $itemChangedDateAfterIndex > time()) {
208 2
                $this->indexQueue->setForcedChangeTimeByItem($item, $itemChangedDateAfterIndex);
0 ignored issues
show
Bug introduced by
It seems like $itemChangedDateAfterIndex can also be of type null; however, parameter $forcedChangeTime of ApacheSolrForTypo3\Solr\...orcedChangeTimeByItem() 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

208
                $this->indexQueue->setForcedChangeTimeByItem($item, /** @scrutinizer ignore-type */ $itemChangedDateAfterIndex);
Loading history...
209
            }
210 1
        } catch (Throwable $e) {
211 1
            $this->restoreOriginalHttpHost($originalHttpHost);
212 1
            throw $e;
213
        }
214
215 2
        $this->restoreOriginalHttpHost($originalHttpHost);
216
217 2
        return $itemIndexed;
218
    }
219
220
    /**
221
     * A factory method to get an indexer depending on an item's configuration.
222
     *
223
     * By default, all items are indexed using the default indexer
224
     * (ApacheSolrForTypo3\Solr\IndexQueue\Indexer) coming with EXT:solr. Pages by default are
225
     * configured to be indexed through a dedicated indexer
226
     * (ApacheSolrForTypo3\Solr\IndexQueue\PageIndexer). In all other cases a dedicated indexer
227
     * can be specified through TypoScript if needed.
228
     *
229
     * @param string $indexingConfigurationName Indexing configuration name.
230
     * @param TypoScriptConfiguration $configuration
231
     * @return Indexer
232
     */
233 1
    protected function getIndexerByItem(
234
        string $indexingConfigurationName,
235
        TypoScriptConfiguration $configuration
236
    ): Indexer {
237 1
        $indexerClass = $configuration->getIndexQueueIndexerByConfigurationName($indexingConfigurationName);
238 1
        $indexerConfiguration = $configuration->getIndexQueueIndexerConfigurationByConfigurationName($indexingConfigurationName);
239
240 1
        $indexer = GeneralUtility::makeInstance($indexerClass, /** @scrutinizer ignore-type */ $indexerConfiguration);
241 1
        if (!($indexer instanceof Indexer)) {
242
            throw new RuntimeException(
243
                'The indexer class "' . $indexerClass . '" for indexing configuration "' . $indexingConfigurationName . '" is not a valid indexer. Must be a subclass of ApacheSolrForTypo3\Solr\IndexQueue\Indexer.',
244
                1260463206
245
            );
246
        }
247
248 1
        return $indexer;
249
    }
250
251
    /**
252
     * Gets the indexing progress.
253
     *
254
     * @return float Indexing progress as a two decimal precision float. f.e. 44.87
255
     */
256 1
    public function getProgress(): float
257
    {
258 1
        return $this->indexQueue->getStatisticsBySite($this->site)->getSuccessPercentage();
259
    }
260
261
    /**
262
     * Returns the amount of failed queue items for the current site.
263
     *
264
     * @return int
265
     */
266 1
    public function getFailCount(): int
267
    {
268 1
        return $this->indexQueue->getStatisticsBySite($this->site)->getFailedCount();
269
    }
270
271
    /**
272
     * Initializes the $_SERVER['HTTP_HOST'] environment variable in CLI
273
     * environments dependent on the Index Queue item's root page.
274
     *
275
     * When the Index Queue Worker task is executed by a cron job there is no
276
     * HTTP_HOST since we are in a CLI environment. RealURL needs the host
277
     * information to generate a proper URL though. Using the Index Queue item's
278
     * root page information we can determine the correct host although being
279
     * in a CLI environment.
280
     *
281
     * @param Item $item Index Queue item to use to determine the host.
282
     * @param
283
     */
284 3
    protected function initializeHttpServerEnvironment(Item $item)
285
    {
286 3
        static $hosts = [];
287 3
        $rootPageId = $item->getRootPageUid();
288 3
        $hostFound = !empty($hosts[$rootPageId]);
289
290 3
        if (!$hostFound) {
291 3
            $hosts[$rootPageId] = $item->getSite()->getDomain();
292
        }
293
294 3
        $_SERVER['HTTP_HOST'] = $hosts[$rootPageId];
295
296
        // needed since TYPO3 7.5
297 3
        GeneralUtility::flushInternalRuntimeCaches();
298
    }
299
300
    /**
301
     * @param string|null $originalHttpHost
302
     */
303 2
    protected function restoreOriginalHttpHost(?string $originalHttpHost)
304
    {
305 2
        if (!is_null($originalHttpHost)) {
306 1
            $_SERVER['HTTP_HOST'] = $originalHttpHost;
307
        } else {
308 1
            unset($_SERVER['HTTP_HOST']);
309
        }
310
311
        // needed since TYPO3 7.5
312 2
        GeneralUtility::flushInternalRuntimeCaches();
313
    }
314
}
315