Passed
Push — release-11.5.x ( 126153...cde2ce )
by Rafael
43:05
created

AbstractIndexer   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 306
Duplicated Lines 0 %

Test Coverage

Coverage 75.76%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 45
eloc 114
c 1
b 0
f 0
dl 0
loc 306
ccs 75
cts 99
cp 0.7576
rs 8.8

8 Methods

Rating   Name   Duplication   Size   Complexity  
A isAllowedToOverrideField() 0 3 1
A addVirtualContentFieldToRecord() 0 7 2
B addDocumentFieldsFromTyposcript() 0 34 8
A isSerializedResultFromRegisteredHook() 0 19 5
A isSerializedValue() 0 4 2
B resolveFieldValue() 0 87 10
A isSerializedResultFromCustomContentElement() 0 20 5
C ensureFieldValueType() 0 37 12

How to fix   Complexity   

Complex Class

Complex classes like AbstractIndexer 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 AbstractIndexer, 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\ContentObject\Classification;
21
use ApacheSolrForTypo3\Solr\ContentObject\Multivalue;
22
use ApacheSolrForTypo3\Solr\ContentObject\Relation;
23
use ApacheSolrForTypo3\Solr\System\Solr\Document\Document;
24
use TYPO3\CMS\Core\Core\Environment;
25
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
28
use UnexpectedValueException;
29
30
/**
31
 * An abstract indexer class to collect a few common methods shared with other
32
 * indexers.
33
 *
34
 * @author Ingo Renner <[email protected]>
35
 */
36
abstract class AbstractIndexer
37
{
38
    /**
39
     * Holds the type of the data to be indexed, usually that is the table name.
40
     *
41
     * @var string
42
     */
43
    protected string $type = '';
44
45
    /**
46
     * Holds field names that are denied to overwrite in thy indexing configuration.
47
     *
48
     * @var array
49
     */
50
    protected static array $unAllowedOverrideFields = ['type'];
51
52
    /**
53
     * @param string $solrFieldName
54
     * @return bool
55
     */
56 62
    public static function isAllowedToOverrideField(string $solrFieldName): bool
57
    {
58 62
        return !in_array($solrFieldName, static::$unAllowedOverrideFields);
59
    }
60
61
    /**
62
     * Adds fields to the document as defined in $indexingConfiguration
63
     *
64
     * @param Document $document base document to add fields to
65
     * @param array $indexingConfiguration Indexing configuration / mapping
66
     * @param array $data Record data
67
     * @return Document Modified document with added fields
68
     */
69 20
    protected function addDocumentFieldsFromTyposcript(Document $document, array $indexingConfiguration, array $data, TypoScriptFrontendController $tsfe): Document
70
    {
71 20
        $data = static::addVirtualContentFieldToRecord($document, $data);
72
73
        // mapping of record fields => solr document fields, resolving cObj
74 20
        foreach ($indexingConfiguration as $solrFieldName => $recordFieldName) {
75 20
            if (is_array($recordFieldName)) {
76
                // configuration for a content object, skipping
77 16
                continue;
78
            }
79
80 20
            if (!static::isAllowedToOverrideField($solrFieldName)) {
81
                throw new InvalidFieldNameException(
82
                    'Must not overwrite field .' . $solrFieldName,
83
                    1435441863
84
                );
85
            }
86
87 20
            $fieldValue = $this->resolveFieldValue($indexingConfiguration, $solrFieldName, $data, $tsfe);
88
            if ($fieldValue === null) {
89 20
                continue;
90
            }
91 9
92
            if (is_array($fieldValue)) {
93 20
                // multi value
94 19
                $document->setField($solrFieldName, $fieldValue);
95
            } else {
96
                if ($fieldValue !== '' && $fieldValue !== null) {
97
                    $document->setField($solrFieldName, $fieldValue);
98
                }
99 20
            }
100
        }
101
102
        return $document;
103
    }
104
105
    /**
106
     * Adds the content of the field 'content' from the solr document as virtual field __solr_content in the record,
107
     * to have it available in typoscript.
108
     *
109
     * @param Document $document
110 28
     * @param array $data
111
     * @return array
112 28
     */
113 8
    public static function addVirtualContentFieldToRecord(Document $document, array $data): array
114 8
    {
115
        if (isset($document['content'])) {
116 20
            $data['__solr_content'] = $document['content'];
117
            return $data;
118
        }
119
        return $data;
120
    }
121
122
    /**
123
     * Resolves a field to its value depending on its configuration.
124
     *
125
     * This enables you to configure the indexer to put the item/record through
126
     * cObj processing if wanted/needed. Otherwise, the plain item/record value
127
     * is taken.
128
     *
129
     * @param array $indexingConfiguration Indexing configuration as defined in plugin.tx_solr_index.queue.[indexingConfigurationName].fields
130
     * @param string $solrFieldName A Solr field name that is configured in the indexing configuration
131
     * @param array $data A record or item's data
132 20
     * @param TypoScriptFrontendController $tsfe
133
     * @return string|null The resolved string value to be indexed; null if value could not be resolved
134
     */
135
    protected function resolveFieldValue(
136
        array $indexingConfiguration,
137
        string $solrFieldName,
138 20
        array $data,
139
        TypoScriptFrontendController $tsfe
140
    ) {
141
        if (isset($indexingConfiguration[$solrFieldName . '.'])) {
142
            // configuration found => need to resolve a cObj
143 16
144 16
            // need to change directory to make IMAGE content objects work in BE context
145
            // see http://blog.netzelf.de/lang/de/tipps-und-tricks/tslib_cobj-image-im-backend
146 16
            $backupWorkingDirectory = getcwd();
147 16
            chdir(Environment::getPublicPath() . '/');
148 16
149 16
            $tsfe->cObj->start($data, $this->type);
150
            $fieldValue = $tsfe->cObj->cObjGetSingle(
151
                $indexingConfiguration[$solrFieldName],
152 16
                $indexingConfiguration[$solrFieldName . '.']
153
            );
154 16
155
            chdir($backupWorkingDirectory);
156
157
            if ($this->isSerializedValue(
158
                $indexingConfiguration,
159 16
                $solrFieldName
160
            )
161
            ) {
162 19
                $fieldValue = unserialize($fieldValue);
163
            }
164
        } elseif (
165
            substr($indexingConfiguration[$solrFieldName], 0, 1) === '<'
166
        ) {
167
            $referencedTsPath = trim(substr(
168
                $indexingConfiguration[$solrFieldName],
169
                1
170
            ));
171
            $typoScriptParser = GeneralUtility::makeInstance(TypoScriptParser::class);
172
            // $name and $conf is loaded with the referenced values.
173
            list($name, $conf) = $typoScriptParser->getVal($referencedTsPath, $GLOBALS['TSFE']->tmpl->setup);
174
175
            // need to change directory to make IMAGE content objects work in BE context
176
            // see http://blog.netzelf.de/lang/de/tipps-und-tricks/tslib_cobj-image-im-backend
177
            $backupWorkingDirectory = getcwd();
178
            chdir(Environment::getPublicPath() . '/');
179
180
            $tsfe->cObj->start($data, $this->type);
181
            $fieldValue = $tsfe->cObj->cObjGetSingle($name, $conf);
182
183
            chdir($backupWorkingDirectory);
184
185
            if ($this->isSerializedValue(
186
                $indexingConfiguration,
187
                $solrFieldName
188
            )
189
            ) {
190 19
                $fieldValue = unserialize($fieldValue);
191
            }
192
        } else {
193
            $indexingFieldName = $indexingConfiguration[$solrFieldName] ?? null;
194
            if (empty($indexingFieldName) ||
195
                !is_string($indexingFieldName) ||
196 20
                !array_key_exists($indexingFieldName, $data)) {
197
                return null;
198 20
            }
199
            $fieldValue = $data[$indexingFieldName];
200
        }
201 20
202 9
        // detect and correct type for dynamic fields
203 9
204
        // find last underscore, substr from there, cut off last character (S/M)
205
        $fieldType = substr(
206
            $solrFieldName,
207
            strrpos($solrFieldName, '_') + 1,
208
            -1
209 20
        );
210
        if (is_array($fieldValue)) {
211
            foreach ($fieldValue as $key => $value) {
212 20
                $fieldValue[$key] = $this->ensureFieldValueType(
213
                    $value,
214
                    $fieldType
215
                );
216
            }
217
        } else {
218
            $fieldValue = $this->ensureFieldValueType($fieldValue, $fieldType);
219
        }
220
221
        return $fieldValue;
222
    }
223
224
    // Utility methods
225
226 27
    /**
227
     * Uses a field's configuration to detect whether its value returned by a
228 27
     * content object is expected to be serialized and thus needs to be
229 26
     * unserialized.
230
     *
231
     * @param array $indexingConfiguration Current item's indexing configuration
232
     * @param string $solrFieldName Current field being indexed
233
     * @return bool TRUE if the value is expected to be serialized, FALSE otherwise
234
     */
235
    public static function isSerializedValue(array $indexingConfiguration, string $solrFieldName): bool
236
    {
237
        return static::isSerializedResultFromRegisteredHook($indexingConfiguration, $solrFieldName)
238
            || static::isSerializedResultFromCustomContentElement($indexingConfiguration, $solrFieldName);
239 25
    }
240
241 25
    /**
242
     * Checks if the response comes from a custom content element that returns a serialized value.
243
     *
244 25
     * @param array $indexingConfiguration
245 1
     * @param string $solrFieldName
246
     * @return bool
247
     */
248
    protected static function isSerializedResultFromCustomContentElement(array $indexingConfiguration, string $solrFieldName): bool
249 25
    {
250 2
        $isSerialized = false;
251
252
        // SOLR_CLASSIFICATION - always returns serialized array
253
        if (($indexingConfiguration[$solrFieldName] ?? null) == Classification::CONTENT_OBJECT_NAME) {
254 25
            $isSerialized = true;
255 12
        }
256
257
        // SOLR_MULTIVALUE - always returns serialized array
258 25
        if (($indexingConfiguration[$solrFieldName] ?? null) == Multivalue::CONTENT_OBJECT_NAME) {
259
            $isSerialized = true;
260
        }
261
262
        // SOLR_RELATION - returns serialized array if multiValue option is set
263
        if (($indexingConfiguration[$solrFieldName] ?? null) == Relation::CONTENT_OBJECT_NAME && !empty($indexingConfiguration[$solrFieldName . '.']['multiValue'])) {
264
            $isSerialized = true;
265
        }
266
267
        return $isSerialized;
268 27
    }
269
270 27
    /**
271 24
     * Checks registered hooks if a SerializedValueDetector detects a serialized response.
272
     *
273
     * @param array $indexingConfiguration
274 3
     * @param string $solrFieldName
275 2
     * @return bool
276 2
     */
277 1
    protected static function isSerializedResultFromRegisteredHook(array $indexingConfiguration, string $solrFieldName): bool
278 1
    {
279
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['detectSerializedValue'] ?? null)) {
280
            return false;
281 1
        }
282 1
283 1
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['detectSerializedValue'] as $classReference) {
284
            $serializedValueDetector = GeneralUtility::makeInstance($classReference);
285
            if (!$serializedValueDetector instanceof SerializedValueDetector) {
286 1
                $message = get_class($serializedValueDetector) . ' must implement interface ' . SerializedValueDetector::class;
287
                throw new UnexpectedValueException($message, 1404471741);
288
            }
289
290
            $isSerialized = (boolean)$serializedValueDetector->isSerializedValue($indexingConfiguration, $solrFieldName);
291
            if ($isSerialized) {
292
                return true;
293
            }
294
        }
295
        return false;
296 20
    }
297
298 20
    /**
299 20
     * Makes sure a field's value matches a (dynamic) field's type.
300 20
     *
301
     * @param mixed $value Value to be added to a document
302
     * @param string $fieldType The dynamic field's type
303
     * @return mixed Returns the value in the correct format for the field type
304 20
     */
305 20
    protected function ensureFieldValueType($value, string $fieldType)
306
    {
307
        switch ($fieldType) {
308 20
            case 'int':
309
            case 'tInt':
310
                $value = (int)$value;
311
                break;
312 20
313
            case 'float':
314
            case 'tFloat':
315
                $value = (float)$value;
316
                break;
317
            case 'long':
318
                // long and double do not exist in PHP
319 20
                // simply make sure it somehow looks like a number
320 20
                // <insert PHP rant here>
321 20
            case 'tLong':
322
                // remove anything that's not a number or negative/minus sign
323
                $value = preg_replace('/[^0-9\\-]/', '', $value);
324
                if (trim($value) === '') {
325
                    $value = 0;
326
                }
327
                break;
328
            case 'double':
329
            case 'tDouble':
330
            case 'tDouble4':
331
                // as long as it's numeric we'll take it, int or float doesn't matter
332 20
                if (!is_numeric($value)) {
333
                    $value = 0;
334
                }
335
                break;
336
337
            default:
338
                // assume things are correct for non-dynamic fields
339
        }
340
341
        return $value;
342
    }
343
}
344