Passed
Push — task/3376-TYPO3_12_compatibili... ( b15a4c...d33ba8 )
by Rafael
45:27 queued 49s
created

Relation   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 410
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 43
eloc 154
c 0
b 0
f 0
dl 0
loc 410
rs 8.96

11 Methods

Rating   Name   Duplication   Size   Complexity  
A render() 0 20 4
A getRelatedItems() 0 20 4
A __construct() 0 11 1
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\Exception as DBALException;
21
use Doctrine\DBAL\Result;
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
    public function __construct(
80
        ContentObjectRenderer $cObj,
81
        TCAService $tcaService = null,
82
        FrontendOverlayService $frontendOverlayService = null
83
    ) {
84
        parent::__construct($cObj);
0 ignored issues
show
Bug introduced by
The method __construct() does not exist on TYPO3\CMS\Frontend\Conte...t\AbstractContentObject. It seems like you code against a sub-type of TYPO3\CMS\Frontend\Conte...t\AbstractContentObject such as ApacheSolrForTypo3\Solr\ContentObject\Relation or TYPO3\CMS\Frontend\Conte...ject\ImageContentObject or TYPO3\CMS\Frontend\Conte...idTemplateContentObject. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

84
        parent::/** @scrutinizer ignore-call */ 
85
                __construct($cObj);
Loading history...
85
        $this->configuration['enableRecursiveValueResolution'] = 1;
86
        $this->configuration['removeEmptyValues'] = 1;
87
        $this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
88
        $this->typoScriptFrontendController = $this->getProtectedTsfeObjectFromContentObjectRenderer($cObj);
89
        $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
    public function render($conf = [])
108
    {
109
        $this->configuration = array_merge($this->configuration, $conf);
110
111
        $relatedItems = $this->getRelatedItems($this->cObj);
0 ignored issues
show
Bug introduced by
It seems like $this->cObj can also be of type null; however, parameter $parentContentObject of ApacheSolrForTypo3\Solr\...tion::getRelatedItems() does only seem to accept TYPO3\CMS\Frontend\Conte...t\ContentObjectRenderer, 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

111
        $relatedItems = $this->getRelatedItems(/** @scrutinizer ignore-type */ $this->cObj);
Loading history...
112
113
        if (!empty($this->configuration['removeDuplicateValues'])) {
114
            $relatedItems = array_unique($relatedItems);
115
        }
116
117
        if (empty($conf['multiValue'])) {
118
            // single value, need to concatenate related items
119
            $singleValueGlue = !empty($conf['singleValueGlue']) ? trim($conf['singleValueGlue'], '|') : ', ';
120
            $result = implode($singleValueGlue, $relatedItems);
121
        } else {
122
            // multi value, need to serialize as content objects must return strings
123
            $result = serialize($relatedItems);
124
        }
125
126
        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
    protected function getRelatedItems(ContentObjectRenderer $parentContentObject): array
140
    {
141
        list($table, $uid) = explode(':', $parentContentObject->currentRecord);
142
        $uid = (int)$uid;
143
        $field = $this->configuration['localField'];
144
145
        if (!$this->tcaService->/** @scrutinizer ignore-call */ getHasConfigurationForField($table, $field)) {
146
            return [];
147
        }
148
149
        $overlayUid = $this->frontendOverlayService->/** @scrutinizer ignore-call */ getUidOfOverlay($table, $field, $uid);
150
        $fieldTCA = $this->tcaService->getConfigurationForField($table, $field);
151
152
        if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
153
            $relatedItems = $this->getRelatedItemsFromMMTable($table, $overlayUid, $fieldTCA);
154
        } else {
155
            $relatedItems = $this->getRelatedItemsFromForeignTable($table, $overlayUid, $fieldTCA, $parentContentObject);
156
        }
157
158
        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
    protected function getRelatedItemsFromMMTable(string $localTableName, int $localRecordUid, array $localFieldTca): array
173
    {
174
        $relatedItems = [];
175
        $foreignTableName = $localFieldTca['config']['foreign_table'];
176
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
177
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
178
        $mmTableName = $localFieldTca['config']['MM'];
179
180
        // Remove the first option of foreignLabelField for recursion
181
        if (str_contains($this->configuration['foreignLabelField'] ?? '', '.')) {
182
            $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
183
            unset($foreignTableLabelFieldArr[0]);
184
            $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
185
        }
186
187
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
188
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
189
        $selectUids = $relationHandler->tableArray[$foreignTableName];
190
        if (!is_array($selectUids) || count($selectUids) <= 0) {
191
            return $relatedItems;
192
        }
193
194
        $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
195
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
196
        foreach ($relatedRecords as $record) {
197
            $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 (str_contains($this->configuration['foreignLabelField'], '.')) {
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
                continue;
213
            }
214
            if ($this->getLanguageUid() > 0) {
215
                $record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
216
            }
217
218
            $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
     *
230
     * @return string|null The field to use for the related item's label
231
     */
232
    protected function resolveForeignTableLabelField(array $foreignTableTca): ?string
233
    {
234
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'] ?? null;
235
236
        // when foreignLabelField is not enabled we can return directly
237
        if (empty($this->configuration['foreignLabelField'])) {
238
            return $foreignTableLabelField;
239
        }
240
241
        if (str_contains($this->configuration['foreignLabelField'] ?? '', '.')) {
242
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
243
        } else {
244
            $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
     * @throws AspectNotFoundException
260
     * @throws DBALException
261
     */
262
    protected function getRelatedItemsFromForeignTable(
263
        string $localTableName,
264
        int $localRecordUid,
265
        array $localFieldTca,
266
        ContentObjectRenderer $parentContentObject
267
    ): array {
268
        $relatedItems = [];
269
        $foreignTableName = $localFieldTca['config']['foreign_table'];
270
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
271
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
272
        $localField = $this->configuration['localField'];
273
274
        /** @var $relationHandler RelationHandler */
275
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
276
        if (!empty($localFieldTca['config']['MM'] ?? '')) {
277
            $relationHandler->start(
278
                '',
279
                $foreignTableName,
280
                $localFieldTca['config']['MM'],
281
                $localRecordUid,
282
                $localTableName,
283
                $localFieldTca['config']
284
            );
285
        } 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
            return $relatedItems;
293
        }
294
295
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
296
        foreach ($relatedRecords as $relatedRecord) {
297
            $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
    /**
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
    protected function resolveRelatedValue(
331
        array $relatedRecord,
332
        array $foreignTableTca,
333
        string $foreignTableLabelField,
334
        ContentObjectRenderer $parentContentObject,
335
        string $foreignTableName = ''
336
    ): array {
337
        if ($this->getLanguageUid() > 0 && !empty($foreignTableName)) {
338
            $relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
339
        }
340
341
        $values = [$relatedRecord[$foreignTableLabelField]];
342
343
        if (
344
            !empty($foreignTableName)
345
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
346
            && $this->configuration['enableRecursiveValueResolution']
347
        ) {
348
            // backup
349
            $backupRecord = $parentContentObject->data;
350
            $backupConfiguration = $this->configuration;
351
352
            // adjust configuration for next level
353
            $this->configuration['localField'] = $foreignTableLabelField;
354
            $parentContentObject->data = $relatedRecord;
355
            if (str_contains($this->configuration['foreignLabelField'], '.')) {
356
                list(, $this->configuration['foreignLabelField']) = explode(
357
                    '.',
358
                    $this->configuration['foreignLabelField'],
359
                    2
360
                );
361
            } else {
362
                $this->configuration['foreignLabelField'] = '';
363
            }
364
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
            $parentContentObject->data = $backupRecord;
377
        }
378
        foreach ($values as &$value) {
379
            $value = $parentContentObject->stdWrap($value, $this->configuration) ?? '';
380
        }
381
382
        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
    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
            $queryBuilder->andWhere($this->configuration['additionalWhereClause']);
403
        }
404
        $queryResult = $queryBuilder->executeQuery();
405
406
        return $this->sortByKeyInIN($queryResult, '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 Result $statement
416
     * @param string $columnName
417
     * @param array $arrayWithValuesForIN
418
     * @return array
419
     * @throws DBALException
420
     */
421
    protected function sortByKeyInIN(Result $statement, string $columnName, ...$arrayWithValuesForIN): array
422
    {
423
        $records = [];
424
        while ($record = $statement->fetchAssociative()) {
425
            $indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
426
            $records[$indexNumber] = $record;
427
        }
428
        ksort($records);
429
        return $records;
430
    }
431
432
    /**
433
     * Returns current language id fetched from object properties chain TSFE->context->language aspect->id.
434
     *
435
     * @return int
436
     * @throws AspectNotFoundException
437
     */
438
    protected function getLanguageUid(): int
439
    {
440
        return (int)$this->typoScriptFrontendController->/** @scrutinizer ignore-call */ getContext()->getPropertyFromAspect('language', 'id');
441
    }
442
443
    /**
444
     * Returns inaccessible {@link ContentObjectRenderer::typoScriptFrontendController} via reflection.
445
     *
446
     * The TypoScriptFrontendController object must be delegated to the whole object aggregation on indexing stack,
447
     * to be able to use Contexts properties and proceed indexing request.
448
     *
449
     * @param ContentObjectRenderer $cObj
450
     * @return TypoScriptFrontendController|null
451
     */
452
    protected function getProtectedTsfeObjectFromContentObjectRenderer(ContentObjectRenderer $cObj): ?TypoScriptFrontendController
453
    {
454
        $reflection = new ReflectionClass($cObj);
455
        $property = $reflection->getProperty('typoScriptFrontendController');
456
        $property->setAccessible(true);
457
        return $property->getValue($cObj);
458
    }
459
}
460