Passed
Push — release-11.5.x ( 9b8416...f8d6dc )
by Markus
44:53
created

Relation   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 409
Duplicated Lines 0 %

Test Coverage

Coverage 89.23%

Importance

Changes 0
Metric Value
wmc 43
eloc 154
dl 0
loc 409
ccs 116
cts 130
cp 0.8923
rs 8.96
c 0
b 0
f 0

11 Methods

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

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 13
    public function __construct(
80
        ContentObjectRenderer $cObj,
81
        TCAService $tcaService = null,
82
        FrontendOverlayService $frontendOverlayService = null
83
    ) {
84 13
        parent::__construct($cObj);
85 13
        $this->configuration['enableRecursiveValueResolution'] = 1;
86 13
        $this->configuration['removeEmptyValues'] = 1;
87 13
        $this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
88 13
        $this->typoScriptFrontendController = $this->getProtectedTsfeObjectFromContentObjectRenderer($cObj);
89 13
        $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 13
    public function render($conf = [])
108
    {
109 13
        $this->configuration = array_merge($this->configuration, $conf);
110
111 13
        $relatedItems = $this->getRelatedItems($this->cObj);
112
113 13
        if (!empty($this->configuration['removeDuplicateValues'])) {
114
            $relatedItems = array_unique($relatedItems);
115
        }
116
117 13
        if (empty($conf['multiValue'])) {
118
            // single value, need to concatenate related items
119 2
            $singleValueGlue = !empty($conf['singleValueGlue']) ? trim($conf['singleValueGlue'], '|') : ', ';
120 2
            $result = implode($singleValueGlue, $relatedItems);
121
        } else {
122
            // multi value, need to serialize as content objects must return strings
123 11
            $result = serialize($relatedItems);
124
        }
125
126 13
        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 13
    protected function getRelatedItems(ContentObjectRenderer $parentContentObject): array
140
    {
141 13
        list($table, $uid) = explode(':', $parentContentObject->currentRecord);
142 13
        $uid = (int)$uid;
143 13
        $field = $this->configuration['localField'];
144
145 13
        if (!$this->tcaService->/** @scrutinizer ignore-call */ getHasConfigurationForField($table, $field)) {
146
            return [];
147
        }
148
149 13
        $overlayUid = $this->frontendOverlayService->/** @scrutinizer ignore-call */ getUidOfOverlay($table, $field, $uid);
150 13
        $fieldTCA = $this->tcaService->getConfigurationForField($table, $field);
151
152 13
        if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
153 10
            $relatedItems = $this->getRelatedItemsFromMMTable($table, $overlayUid, $fieldTCA);
154
        } else {
155 3
            $relatedItems = $this->getRelatedItemsFromForeignTable($table, $overlayUid, $fieldTCA, $parentContentObject);
156
        }
157
158 13
        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 10
    protected function getRelatedItemsFromMMTable(string $localTableName, int $localRecordUid, array $localFieldTca): array
173
    {
174 10
        $relatedItems = [];
175 10
        $foreignTableName = $localFieldTca['config']['foreign_table'];
176 10
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
177 10
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
178 10
        $mmTableName = $localFieldTca['config']['MM'];
179
180
        // Remove the first option of foreignLabelField for recursion
181 10
        if (strpos($this->configuration['foreignLabelField'] ?? '', '.') !== false) {
182
            $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
183
            unset($foreignTableLabelFieldArr[0]);
184
            $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
185
        }
186
187 10
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
188 10
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
189 10
        $selectUids = $relationHandler->tableArray[$foreignTableName];
190 10
        if (!is_array($selectUids) || count($selectUids) <= 0) {
191 2
            return $relatedItems;
192
        }
193
194 10
        $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
195 10
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
196 10
        foreach ($relatedRecords as $record) {
197 10
            $contentObject->start($record, $foreignTableName);
198
199
            if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
200
                && !empty($this->configuration['enableRecursiveValueResolution'])
201
            ) {
202
                $this->configuration['localField'] = $foreignTableLabelField;
203
                if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
204
                    $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
205
                    unset($foreignTableLabelFieldArr[0]);
206
                    $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
207
                } else {
208
                    unset($this->configuration['foreignLabelField']);
209
                }
210
211
                $relatedItems = array_merge($relatedItems, $this->getRelatedItems($contentObject));
212 10
                continue;
213 7
            }
214
            if ($this->getLanguageUid() > 0) {
215 10
                $record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
216
            }
217
218 10
            $relatedItems[] = $contentObject->stdWrap($record[$foreignTableLabelField] ?? '', $this->configuration) ?? '';
219
        }
220
221
        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 13
     *
230
     * @return string|null The field to use for the related item's label
231 13
     */
232
    protected function resolveForeignTableLabelField(array $foreignTableTca): ?string
233
    {
234 13
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'] ?? null;
235 11
236
        // when foreignLabelField is not enabled we can return directly
237
        if (empty($this->configuration['foreignLabelField'])) {
238 4
            return $foreignTableLabelField;
239 2
        }
240
241 4
        if (strpos($this->configuration['foreignLabelField'] ?? '', '.') !== false) {
242
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
243
        } else {
244 4
            $foreignTableLabelField = $this->configuration['foreignLabelField'];
245
        }
246
247
        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 3
     * @throws AspectNotFoundException
260
     * @throws DBALException
261
     */
262
    protected function getRelatedItemsFromForeignTable(
263
        string $localTableName,
264
        int $localRecordUid,
265 3
        array $localFieldTca,
266 3
        ContentObjectRenderer $parentContentObject
267 3
    ): array {
268 3
        $relatedItems = [];
269
        $foreignTableName = $localFieldTca['config']['foreign_table'];
270
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
271 3
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
272
        $localField = $this->configuration['localField'];
273 3
274
        /** @var $relationHandler RelationHandler */
275 3
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
276 3
        if (!empty($localFieldTca['config']['MM'] ?? '')) {
277
            $relationHandler->start(
278 3
                '',
279
                $foreignTableName,
280
                $localFieldTca['config']['MM'],
281
                $localRecordUid,
282 3
                $localTableName,
283
                $localFieldTca['config']
284 3
            );
285 3
        } else {
286
            $itemList = $parentContentObject->data[$localField] ?? '';
287
            $relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
288
        }
289
290
        $selectUids = $relationHandler->tableArray[$foreignTableName];
291
        if (!is_array($selectUids) || count($selectUids) <= 0) {
292 3
            return $relatedItems;
293 3
        }
294
295
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
296
        foreach ($relatedRecords as $relatedRecord) {
297 3
            $resolveRelatedValues = $this->resolveRelatedValue(
298
                $relatedRecord,
299
                $foreignTableTca,
300
                $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
                $parentContentObject,
302
                $foreignTableName
303
            );
304
305
            foreach ($resolveRelatedValues as $resolveRelatedValue) {
306
                if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
307
                    $relatedItems[] = $resolveRelatedValue;
308
                }
309
            }
310
        }
311
312
        return $relatedItems;
313
    }
314
315 3
    /**
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 3
     * @param ContentObjectRenderer $parentContentObject cObject
323 3
     * @param string $foreignTableName Related record table name
324
     *
325
     * @return array
326 3
     *
327
     * @throws AspectNotFoundException
328
     * @throws DBALException
329 3
     */
330 3
    protected function resolveRelatedValue(
331 3
        array $relatedRecord,
332
        array $foreignTableTca,
333
        string $foreignTableLabelField,
334 2
        ContentObjectRenderer $parentContentObject,
335 2
        string $foreignTableName = ''
336
    ): array {
337
        if ($this->getLanguageUid() > 0 && !empty($foreignTableName)) {
338 2
            $relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
339 2
        }
340 2
341 2
        $values = [$relatedRecord[$foreignTableLabelField]];
342
343 2
        if (
344
            !empty($foreignTableName)
345
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
346
            && $this->configuration['enableRecursiveValueResolution']
347 2
        ) {
348
            // backup
349
            $backupRecord = $parentContentObject->data;
350
            $backupConfiguration = $this->configuration;
351 2
352
            // adjust configuration for next level
353 2
            $this->configuration['localField'] = $foreignTableLabelField;
354 2
            $parentContentObject->data = $relatedRecord;
355
            if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
356
                list(, $this->configuration['foreignLabelField']) = explode(
357 2
                    '.',
358
                    $this->configuration['foreignLabelField'],
359
                    2
360 2
                );
361 2
            } else {
362
                $this->configuration['foreignLabelField'] = '';
363
            }
364 3
365
            // recursion
366
            $relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
367
                $foreignTableName,
368
                $relatedRecord['uid'],
369
                $foreignTableTca['columns'][$foreignTableLabelField],
370
                $parentContentObject
371
            );
372
            $values = $relatedItemsFromForeignTable;
373
374
            // restore
375
            $this->configuration = $backupConfiguration;
376 13
            $parentContentObject->data = $backupRecord;
377
        }
378
        foreach ($values as &$value) {
379 13
            $value = $parentContentObject->stdWrap($value, $this->configuration) ?? '';
380 13
        }
381 13
382 13
        return $values;
383 13
    }
384 2
385
    /**
386 13
     * Return records via relation.
387
     *
388 13
     * @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
    protected function getRelatedRecords(string $foreignTable, int ...$uids): array
395
    {
396
        /** @var QueryBuilder $queryBuilder */
397
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
398
        $queryBuilder->select('*')
399
            ->from($foreignTable)
400
            ->where(/** @scrutinizer ignore-type */ $queryBuilder->expr()->in('uid', $uids));
401
        if (isset($this->configuration['additionalWhereClause'])) {
402 13
            $queryBuilder->andWhere($this->configuration['additionalWhereClause']);
403
        }
404 13
        $statement = $queryBuilder->execute();
405 13
406 13
        return $this->sortByKeyInIN($statement, 'uid', ...$uids);
407 13
    }
408
409 13
    /**
410 13
     * 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 13
     */
420
    protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN): array
421 13
    {
422
        $records = [];
423
        while ($record = $statement->fetchAssociative()) {
424
            $indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
425
            $records[$indexNumber] = $record;
426
        }
427
        ksort($records);
428
        return $records;
429
    }
430
431
    /**
432
     * Returns current language id fetched from object properties chain TSFE->context->language aspect->id.
433 13
     *
434
     * @return int
435 13
     * @throws AspectNotFoundException
436 13
     */
437 13
    protected function getLanguageUid(): int
438 13
    {
439
        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
    protected function getProtectedTsfeObjectFromContentObjectRenderer(ContentObjectRenderer $cObj): ?TypoScriptFrontendController
452
    {
453
        $reflection = new ReflectionClass($cObj);
454
        $property = $reflection->getProperty('typoScriptFrontendController');
455
        $property->setAccessible(true);
456
        return $property->getValue($cObj);
457
    }
458
}
459