Passed
Pull Request — main (#3475)
by Markus
51:18 queued 26:25
created

Relation   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 409
Duplicated Lines 0 %

Test Coverage

Coverage 98.73%

Importance

Changes 0
Metric Value
wmc 43
eloc 154
c 0
b 0
f 0
dl 0
loc 409
ccs 155
cts 157
cp 0.9873
rs 8.96

11 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveForeignTableLabelField() 0 16 3
A __construct() 0 11 1
A getRelatedItems() 0 20 4
A getLanguageUid() 0 3 1
A getRelatedRecords() 0 13 2
A render() 0 20 4
B resolveRelatedValue() 0 53 8
A getProtectedTsfeObjectFromContentObjectRenderer() 0 6 1
A sortByKeyInIN() 0 9 2
B getRelatedItemsFromForeignTable() 0 51 8
B getRelatedItemsFromMMTable() 0 50 9

How to fix   Complexity   

Complex Class

Complex classes like Relation 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 Relation, and based on these observations, apply Extract Interface, too.

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\ContentObject;
17
18
use ApacheSolrForTypo3\Solr\System\Language\FrontendOverlayService;
19
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
20
use Doctrine\DBAL\Driver\Statement;
21
use Doctrine\DBAL\Exception as DBALException;
22
use ReflectionClass;
23
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
24
use TYPO3\CMS\Core\Database\ConnectionPool;
25
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
26
use TYPO3\CMS\Core\Database\RelationHandler;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
29
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
30
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
31
32
/**
33
 * A content object (cObj) to resolve relations between database records
34
 *
35
 * Configuration options:
36
 *
37
 * localField: the record's field to use to resolve relations
38
 * foreignLabelField: Usually the label field to retrieve from the related records is determined automatically using TCA, using this option the desired field can be specified explicitly
39
 * multiValue: whether to return related records suitable for a multi value field
40
 * singleValueGlue: when not using multiValue, the related records need to be concatenated using a glue string, by default this is ", ". Using this option a custom glue can be specified. The custom value must be wrapped by pipe (|) characters.
41
 * relationTableSortingField: field in a mm relation table to sort by, usually "sorting"
42
 * enableRecursiveValueResolution: if the specified remote table's label field is a relation to another table, the value will be resolved by following the relation recursively.
43
 * removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
44
 * removeDuplicateValues: Removes duplicate values
45
 *
46
 * @author Ingo Renner <[email protected]>
47
 */
48
class Relation extends AbstractContentObject
49
{
50
    public const CONTENT_OBJECT_NAME = 'SOLR_RELATION';
51
52
    /**
53
     * Content object configuration
54
     *
55
     * @var array
56
     */
57
    protected array $configuration = [];
58
59
    /**
60
     * @var TCAService
61
     */
62
    protected TCAService $tcaService;
63
64
    /**
65
     * @var FrontendOverlayService
66
     */
67
    protected FrontendOverlayService $frontendOverlayService;
68
69
    /**
70
     * @var TypoScriptFrontendController|null
71
     */
72
    protected ?TypoScriptFrontendController $typoScriptFrontendController = null;
73
74
    /**
75
     * Relation constructor.
76
     * @param TCAService|null $tcaService
77
     * @param FrontendOverlayService|null $frontendOverlayService
78
     */
79 39
    public function __construct(
80
        ContentObjectRenderer $cObj,
81
        TCAService $tcaService = null,
82
        FrontendOverlayService $frontendOverlayService = null
83
    ) {
84 39
        parent::__construct($cObj);
85 39
        $this->configuration['enableRecursiveValueResolution'] = 1;
86 39
        $this->configuration['removeEmptyValues'] = 1;
87 39
        $this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
88 39
        $this->typoScriptFrontendController = $this->getProtectedTsfeObjectFromContentObjectRenderer($cObj);
89 39
        $this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class, $this->tcaService, $this->typoScriptFrontendController);
90
    }
91
92
    /**
93
     * Executes the SOLR_RELATION content object.
94
     *
95
     * Resolves relations between records. Currently, supported relations are
96
     * TYPO3-style m:n relations.
97
     * May resolve single value and multi value relations.
98
     *
99
     * @inheritDoc
100
     *
101
     * @param array $conf
102
     * @return string
103
     * @throws AspectNotFoundException
104
     * @throws DBALException
105
     * @noinspection PhpMissingReturnTypeInspection, because foreign source inheritance See {@link AbstractContentObject::render()}
106
     */
107 39
    public function render($conf = [])
108
    {
109 39
        $this->configuration = array_merge($this->configuration, $conf);
110
111 39
        $relatedItems = $this->getRelatedItems($this->cObj);
112
113 39
        if (!empty($this->configuration['removeDuplicateValues'])) {
114 2
            $relatedItems = array_unique($relatedItems);
115
        }
116
117 39
        if (empty($conf['multiValue'])) {
118
            // single value, need to concatenate related items
119 23
            $singleValueGlue = !empty($conf['singleValueGlue']) ? trim($conf['singleValueGlue'], '|') : ', ';
120 23
            $result = implode($singleValueGlue, $relatedItems);
121
        } else {
122
            // multi value, need to serialize as content objects must return strings
123 16
            $result = serialize($relatedItems);
124
        }
125
126 39
        return $result;
127
    }
128
129
    /**
130
     * Gets the related items of the current record's configured field.
131
     *
132
     * @param ContentObjectRenderer $parentContentObject parent content object
133
     *
134
     * @return array Array of related items, values already resolved from related records
135
     *
136
     * @throws AspectNotFoundException
137
     * @throws DBALException
138
     */
139 39
    protected function getRelatedItems(ContentObjectRenderer $parentContentObject): array
140
    {
141 39
        list($table, $uid) = explode(':', $parentContentObject->currentRecord);
142 39
        $uid = (int)$uid;
143 39
        $field = $this->configuration['localField'];
144
145 39
        if (!$this->tcaService->/** @scrutinizer ignore-call */ getHasConfigurationForField($table, $field)) {
146
            return [];
147
        }
148
149 39
        $overlayUid = $this->frontendOverlayService->/** @scrutinizer ignore-call */ getUidOfOverlay($table, $field, $uid);
150 39
        $fieldTCA = $this->tcaService->getConfigurationForField($table, $field);
151
152 39
        if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
153 23
            $relatedItems = $this->getRelatedItemsFromMMTable($table, $overlayUid, $fieldTCA);
154
        } else {
155 23
            $relatedItems = $this->getRelatedItemsFromForeignTable($table, $overlayUid, $fieldTCA, $parentContentObject);
156
        }
157
158 39
        return $relatedItems;
159
    }
160
161
    /**
162
     * Gets the related items from a table using the n:m relation.
163
     *
164
     * @param string $localTableName Local table name
165
     * @param int $localRecordUid Local record uid
166
     * @param array $localFieldTca The local table's TCA
167
     * @return array Array of related items, values already resolved from related records
168
     *
169
     * @throws AspectNotFoundException
170
     * @throws DBALException
171
     */
172 23
    protected function getRelatedItemsFromMMTable(string $localTableName, int $localRecordUid, array $localFieldTca): array
173
    {
174 23
        $relatedItems = [];
175 23
        $foreignTableName = $localFieldTca['config']['foreign_table'];
176 23
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
177 23
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
178 23
        $mmTableName = $localFieldTca['config']['MM'];
179
180
        // Remove the first option of foreignLabelField for recursion
181 23
        if (strpos($this->configuration['foreignLabelField'] ?? '', '.') !== false) {
182 3
            $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
183 3
            unset($foreignTableLabelFieldArr[0]);
184 3
            $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
185
        }
186
187 23
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
188 23
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
189 23
        $selectUids = $relationHandler->tableArray[$foreignTableName];
190 23
        if (!is_array($selectUids) || count($selectUids) <= 0) {
191 2
            return $relatedItems;
192
        }
193
194 23
        $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
195 23
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
196 23
        foreach ($relatedRecords as $record) {
197 23
            $contentObject->start($record, $foreignTableName);
198
199 23
            if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
200 23
                && !empty($this->configuration['enableRecursiveValueResolution'])
201
            ) {
202 7
                $this->configuration['localField'] = $foreignTableLabelField;
203 7
                if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
204 2
                    $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
205 2
                    unset($foreignTableLabelFieldArr[0]);
206 2
                    $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
207
                } else {
208 5
                    unset($this->configuration['foreignLabelField']);
209
                }
210
211 7
                $relatedItems = array_merge($relatedItems, $this->getRelatedItems($contentObject));
212 7
                continue;
213
            }
214 16
            if ($this->getLanguageUid() > 0) {
215 7
                $record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
216
            }
217
218 16
            $relatedItems[] = $contentObject->stdWrap($record[$foreignTableLabelField] ?? '', $this->configuration) ?? '';
219
        }
220
221 23
        return $relatedItems;
222
    }
223
224
    /**
225
     * Resolves the field to use as the related item's label depending on TCA
226
     * and TypoScript configuration
227
     *
228
     * @param array $foreignTableTca The foreign table's TCA
229
     *
230
     * @return string|null The field to use for the related item's label
231
     */
232 39
    protected function resolveForeignTableLabelField(array $foreignTableTca): ?string
233
    {
234 39
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'] ?? null;
235
236
        // when foreignLabelField is not enabled we can return directly
237 39
        if (empty($this->configuration['foreignLabelField'])) {
238 29
            return $foreignTableLabelField;
239
        }
240
241 22
        if (strpos($this->configuration['foreignLabelField'] ?? '', '.') !== false) {
242 11
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
243
        } else {
244 21
            $foreignTableLabelField = $this->configuration['foreignLabelField'];
245
        }
246
247 22
        return $foreignTableLabelField;
248
    }
249
250
    /**
251
     * Gets the related items from a table using a 1:n relation.
252
     *
253
     * @param string $localTableName Local table name
254
     * @param int $localRecordUid Local record uid
255
     * @param array $localFieldTca The local table's TCA
256
     * @param ContentObjectRenderer $parentContentObject parent content object
257
     * @return array Array of related items, values already resolved from related records
258
     *
259
     * @throws AspectNotFoundException
260
     * @throws DBALException
261
     */
262 23
    protected function getRelatedItemsFromForeignTable(
263
        string $localTableName,
264
        int $localRecordUid,
265
        array $localFieldTca,
266
        ContentObjectRenderer $parentContentObject
267
    ): array {
268 23
        $relatedItems = [];
269 23
        $foreignTableName = $localFieldTca['config']['foreign_table'];
270 23
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
271 23
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
272 23
        $localField = $this->configuration['localField'];
273
274
        /** @var $relationHandler RelationHandler */
275 23
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
276 23
        if (!empty($localFieldTca['config']['MM'] ?? '')) {
277 5
            $relationHandler->start(
278 5
                '',
279 5
                $foreignTableName,
280 5
                $localFieldTca['config']['MM'],
281 5
                $localRecordUid,
282 5
                $localTableName,
283 5
                $localFieldTca['config']
284 5
            );
285
        } else {
286 23
            $itemList = $parentContentObject->data[$localField] ?? '';
287 23
            $relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
288
        }
289
290 23
        $selectUids = $relationHandler->tableArray[$foreignTableName];
291 23
        if (!is_array($selectUids) || count($selectUids) <= 0) {
292
            return $relatedItems;
293
        }
294
295 23
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
296 23
        foreach ($relatedRecords as $relatedRecord) {
297 23
            $resolveRelatedValues = $this->resolveRelatedValue(
298 23
                $relatedRecord,
299 23
                $foreignTableTca,
300 23
                $foreignTableLabelField,
0 ignored issues
show
Bug introduced by
It seems like $foreignTableLabelField can also be of type null; however, parameter $foreignTableLabelField of ApacheSolrForTypo3\Solr\...::resolveRelatedValue() 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

300
                /** @scrutinizer ignore-type */ $foreignTableLabelField,
Loading history...
301 23
                $parentContentObject,
302 23
                $foreignTableName
303 23
            );
304
305 23
            foreach ($resolveRelatedValues as $resolveRelatedValue) {
306 23
                if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
307 23
                    $relatedItems[] = $resolveRelatedValue;
308
                }
309
            }
310
        }
311
312 23
        return $relatedItems;
313
    }
314
315
    /**
316
     * Resolves the value of the related field. If the related field's value is
317
     * a relation itself, this method takes care of resolving it recursively.
318
     *
319
     * @param array $relatedRecord Related record as array
320
     * @param array $foreignTableTca TCA of the related table
321
     * @param string $foreignTableLabelField Field name of the foreign label field
322
     * @param ContentObjectRenderer $parentContentObject cObject
323
     * @param string $foreignTableName Related record table name
324
     *
325
     * @return array
326
     *
327
     * @throws AspectNotFoundException
328
     * @throws DBALException
329
     */
330 23
    protected function resolveRelatedValue(
331
        array $relatedRecord,
332
        array $foreignTableTca,
333
        string $foreignTableLabelField,
334
        ContentObjectRenderer $parentContentObject,
335
        string $foreignTableName = ''
336
    ): array {
337 23
        if ($this->getLanguageUid() > 0 && !empty($foreignTableName)) {
338 3
            $relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
339
        }
340
341 23
        $values = [$relatedRecord[$foreignTableLabelField]];
342
343
        if (
344 23
            !empty($foreignTableName)
345 23
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
346 23
            && $this->configuration['enableRecursiveValueResolution']
347
        ) {
348
            // backup
349 11
            $backupRecord = $parentContentObject->data;
350 11
            $backupConfiguration = $this->configuration;
351
352
            // adjust configuration for next level
353 11
            $this->configuration['localField'] = $foreignTableLabelField;
354 11
            $parentContentObject->data = $relatedRecord;
355 11
            if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
356 9
                list(, $this->configuration['foreignLabelField']) = explode(
357 9
                    '.',
358 9
                    $this->configuration['foreignLabelField'],
359 9
                    2
360 9
                );
361
            } else {
362 7
                $this->configuration['foreignLabelField'] = '';
363
            }
364
365
            // recursion
366 11
            $relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
367 11
                $foreignTableName,
368 11
                $relatedRecord['uid'],
369 11
                $foreignTableTca['columns'][$foreignTableLabelField],
370 11
                $parentContentObject
371 11
            );
372 11
            $values = $relatedItemsFromForeignTable;
373
374
            // restore
375 11
            $this->configuration = $backupConfiguration;
376 11
            $parentContentObject->data = $backupRecord;
377
        }
378 23
        foreach ($values as &$value) {
379 23
            $value = $parentContentObject->stdWrap($value, $this->configuration) ?? '';
380
        }
381
382 23
        return $values;
383
    }
384
385
    /**
386
     * Return records via relation.
387
     *
388
     * @param string $foreignTable The table to fetch records from.
389
     * @param int ...$uids The uids to fetch from table.
390
     * @return array
391
     *
392
     * @throws \Doctrine\DBAL\DBALException
393
     */
394 39
    protected function getRelatedRecords(string $foreignTable, int ...$uids): array
395
    {
396
        /** @var QueryBuilder $queryBuilder */
397 39
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
398 39
        $queryBuilder->select('*')
399 39
            ->from($foreignTable)
400 39
            ->where(/** @scrutinizer ignore-type */ $queryBuilder->expr()->in('uid', $uids));
401 39
        if (isset($this->configuration['additionalWhereClause'])) {
402 2
            $queryBuilder->andWhere($this->configuration['additionalWhereClause']);
403
        }
404 39
        $statement = $queryBuilder->execute();
405
406 39
        return $this->sortByKeyInIN($statement, 'uid', ...$uids);
407
    }
408
409
    /**
410
     * Sorts the result set by key in array for IN values.
411
     *   Simulates MySqls ORDER BY FIELD(fieldname, COPY_OF_IN_FOR_WHERE)
412
     *   Example: SELECT * FROM a_table WHERE field_name IN (2, 3, 4) SORT BY FIELD(field_name, 2, 3, 4)
413
     *
414
     *
415
     * @param Statement $statement
416
     * @param string $columnName
417
     * @param array $arrayWithValuesForIN
418
     * @return array
419
     */
420 39
    protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN): array
421
    {
422 39
        $records = [];
423 39
        while ($record = $statement->fetchAssociative()) {
424 39
            $indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
425 39
            $records[$indexNumber] = $record;
426
        }
427 39
        ksort($records);
428 39
        return $records;
429
    }
430
431
    /**
432
     * Returns current language id fetched from object properties chain TSFE->context->language aspect->id.
433
     *
434
     * @return int
435
     * @throws AspectNotFoundException
436
     */
437 39
    protected function getLanguageUid(): int
438
    {
439 39
        return (int)$this->typoScriptFrontendController->/** @scrutinizer ignore-call */ getContext()->getPropertyFromAspect('language', 'id');
440
    }
441
442
    /**
443
     * Returns inaccessible {@link ContentObjectRenderer::typoScriptFrontendController} via reflection.
444
     *
445
     * The TypoScriptFrontendController object must be delegated to the whole object aggregation on indexing stack,
446
     * to be able to use Contexts properties and proceed indexing request.
447
     *
448
     * @param ContentObjectRenderer $cObj
449
     * @return TypoScriptFrontendController|null
450
     */
451 39
    protected function getProtectedTsfeObjectFromContentObjectRenderer(ContentObjectRenderer $cObj): ?TypoScriptFrontendController
452
    {
453 39
        $reflection = new ReflectionClass($cObj);
454 39
        $property = $reflection->getProperty('typoScriptFrontendController');
455 39
        $property->setAccessible(true);
456 39
        return $property->getValue($cObj);
457
    }
458
}
459