Passed
Push — master ( 449422...a50af9 )
by Timo
25:50
created

Relation   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 437
Duplicated Lines 0 %

Test Coverage

Coverage 89.35%

Importance

Changes 0
Metric Value
wmc 57
eloc 166
dl 0
loc 437
ccs 151
cts 169
cp 0.8935
rs 5.04
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A usePagesLanguageOverlayInsteadOfPagesIfPossible() 0 11 5
A getRelatedRecords() 0 13 2
A cObjGetSingleExt() 0 24 4
B resolveRelatedValue() 0 47 7
B getUidOfRecordOverlay() 0 39 8
A getRelatedItems() 0 22 4
A getTranslationOverlay() 0 7 2
A sortByKeyInIN() 0 9 2
A getLocalRecordUidFromOverlay() 0 12 4
B getRelatedItemsFromForeignTable() 0 39 6
A resolveForeignTableLabelField() 0 16 3
A __construct() 0 5 1
B getRelatedItemsFromMMTable() 0 48 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
namespace ApacheSolrForTypo3\Solr\ContentObject;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2011-2015 Ingo Renner <[email protected]>
8
 *  All rights reserved
9
 *
10
 *  This script is part of the TYPO3 project. The TYPO3 project is
11
 *  free software; you can redistribute it and/or modify
12
 *  it under the terms of the GNU General Public License as published by
13
 *  the Free Software Foundation; either version 3 of the License, or
14
 *  (at your option) any later version.
15
 *
16
 *  The GNU General Public License can be found at
17
 *  http://www.gnu.org/copyleft/gpl.html.
18
 *
19
 *  This script is distributed in the hope that it will be useful,
20
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 *  GNU General Public License for more details.
23
 *
24
 *  This copyright notice MUST APPEAR in all copies of the script!
25
 ***************************************************************/
26
27
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
28
use ApacheSolrForTypo3\Solr\Util;
29
use Doctrine\DBAL\Driver\Statement;
30
use TYPO3\CMS\Core\Database\ConnectionPool;
31
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32
use TYPO3\CMS\Core\Database\RelationHandler;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
35
36
/**
37
 * A content object (cObj) to resolve relations between database records
38
 *
39
 * Configuration options:
40
 *
41
 * localField: the record's field to use to resolve relations
42
 * 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
43
 * multiValue: whether to return related records suitable for a multi value field
44
 * 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.
45
 * relationTableSortingField: field in an mm relation table to sort by, usually "sorting"
46
 * enableRecursiveValueResolution: if the specified remote table's label field is a relation to another table, the value will be resolve by following the relation recursively.
47
 * removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
48
 * removeDuplicateValues: Removes duplicate values
49
 *
50
 * @author Ingo Renner <[email protected]>
51
 */
52
class Relation
53
{
54
    const CONTENT_OBJECT_NAME = 'SOLR_RELATION';
55
56
    /**
57
     * Content object configuration
58
     *
59
     * @var array
60
     */
61
    protected $configuration = [];
62
63
    /**
64
     * @var TCAService
65
     */
66
    protected $tcaService = null;
67
68
    /**
69
     * Relation constructor.
70
     * @param TCAService|null $tcaService
71
     */
72 46
    public function __construct(TCAService $tcaService = null)
73
    {
74 46
        $this->configuration['enableRecursiveValueResolution'] = 1;
75 46
        $this->configuration['removeEmptyValues'] = 1;
76 46
        $this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
77 46
    }
78
79
    /**
80
     * Executes the SOLR_RELATION content object.
81
     *
82
     * Resolves relations between records. Currently supported relations are
83
     * TYPO3-style m:n relations.
84
     * May resolve single value and multi value relations.
85
     *
86
     * @param string $name content object name 'SOLR_RELATION'
87
     * @param array $configuration for the content object
88
     * @param string $TyposcriptKey not used
89
     * @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $parentContentObject parent content object
90
     * @return string serialized array representation of the given list
91
     */
92 46
    public function cObjGetSingleExt(
93
        /** @noinspection PhpUnusedParameterInspection */ $name,
94
        array $configuration,
95
        /** @noinspection PhpUnusedParameterInspection */ $TyposcriptKey,
96
        $parentContentObject
97
    ) {
98 46
        $this->configuration = array_merge($this->configuration, $configuration);
99
100 46
        $relatedItems = $this->getRelatedItems($parentContentObject);
101
102 46
        if (!empty($this->configuration['removeDuplicateValues'])) {
103
            $relatedItems = array_unique($relatedItems);
104
        }
105
106 46
        if (empty($configuration['multiValue'])) {
107
            // single value, need to concatenate related items
108 2
            $singleValueGlue = !empty($configuration['singleValueGlue']) ? trim($configuration['singleValueGlue'], '|') : ', ';
109 2
            $result = implode($singleValueGlue, $relatedItems);
110
        } else {
111
            // multi value, need to serialize as content objects must return strings
112 44
            $result = serialize($relatedItems);
113
        }
114
115 46
        return $result;
116
    }
117
118
    /**
119
     * Gets the related items of the current record's configured field.
120
     *
121
     * @param ContentObjectRenderer $parentContentObject parent content object
122
     * @return array Array of related items, values already resolved from related records
123
     */
124 46
    protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
125
    {
126
127 46
        list($localTableNameOrg, $localRecordUid) = explode(':', $parentContentObject->currentRecord);
128 46
        $localFieldName = $this->configuration['localField'];
129
130 46
        if (!$this->tcaService->getHasConfigurationForField($localTableNameOrg, $localFieldName)) {
131
            return [];
132
        }
133
134 46
        $localTableName = $this->usePagesLanguageOverlayInsteadOfPagesIfPossible($localTableNameOrg, $localFieldName);
135 46
        $localRecordUid = $this->getUidOfRecordOverlay($localTableNameOrg, $localFieldName, $localRecordUid);
0 ignored issues
show
Bug introduced by
$localRecordUid of type string is incompatible with the type integer expected by parameter $localRecordUid of ApacheSolrForTypo3\Solr\...getUidOfRecordOverlay(). ( Ignorable by Annotation )

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

135
        $localRecordUid = $this->getUidOfRecordOverlay($localTableNameOrg, $localFieldName, /** @scrutinizer ignore-type */ $localRecordUid);
Loading history...
136 46
        $localFieldTca = $this->tcaService->getConfigurationForField($localTableName, $localFieldName);
137
138 46
        if (isset($localFieldTca['config']['MM']) && trim($localFieldTca['config']['MM']) !== '') {
139 43
            $relatedItems = $this->getRelatedItemsFromMMTable($localTableName, $localRecordUid, $localFieldTca);
140
        } else {
141 3
            $relatedItems = $this->getRelatedItemsFromForeignTable($localTableName,
142 3
                $localRecordUid, $localFieldTca, $parentContentObject);
143
        }
144
145 46
        return $relatedItems;
146
    }
147
148
    /**
149
     * @param string $localTableName
150
     * @param string $localFieldName
151
     * @return string
152
     * @todo this can be removed when TYPO3 8 support is dropped since pages translations are in pages then as well
153
     */
154 46
    protected function usePagesLanguageOverlayInsteadOfPagesIfPossible(string $localTableName, string $localFieldName) : string
155
    {
156
        // pages has a special overlay table constriction
157 46
        if ($GLOBALS['TSFE']->sys_language_uid > 0
158 46
            && $localTableName === 'pages'
159 46
            && $this->tcaService->getHasConfigurationForField('pages_language_overlay', $localFieldName)
160 46
            && Util::getIsTYPO3VersionBelow9()) {
161 1
            return 'pages_language_overlay';
162
        }
163
164 46
        return $localTableName;
165
    }
166
167
    /**
168
     * Gets the related items from a table using a n:m relation.
169
     *
170
     * @param string $localTableName Local table name
171
     * @param int $localRecordUid Local record uid
172
     * @param array $localFieldTca The local table's TCA
173
     * @return array Array of related items, values already resolved from related records
174
     */
175 43
    protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid, array $localFieldTca)
176
    {
177 43
        $relatedItems = [];
178 43
        $foreignTableName = $localFieldTca['config']['foreign_table'];
179 43
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
180 43
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
181 43
        $mmTableName = $localFieldTca['config']['MM'];
182
183
        // Remove the first option of foreignLabelField for recursion
184 43
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
185
            $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
186
            unset($foreignTableLabelFieldArr[0]);
187
            $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
188
        }
189
190 43
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
191 43
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
192 43
        $selectUids = $relationHandler->tableArray[$foreignTableName];
193 43
        if (!is_array($selectUids) || count($selectUids) <= 0) {
194 35
            return $relatedItems;
195
        }
196
197 10
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
198 10
        foreach ($relatedRecords as $record) {
199 10
            if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
200 10
                && $this->configuration['enableRecursiveValueResolution']
201
            ) {
202
                if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
203
                    $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
204
                    unset($foreignTableLabelFieldArr[0]);
205
                    $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
206
                }
207
208
                $this->configuration['localField'] = $foreignTableLabelField;
209
210
                $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
211
                $contentObject->start($record, $foreignTableName);
212
213
                return $this->getRelatedItems($contentObject);
214
            } else {
215 10
                if ($GLOBALS['TSFE']->sys_language_uid > 0) {
216 5
                    $record = $this->getTranslationOverlay($foreignTableName, $record);
217
                }
218 10
                $relatedItems[] = $record[$foreignTableLabelField];
219
            }
220
        }
221
222 10
        return $relatedItems;
223
    }
224
225
    /**
226
     * Resolves the field to use as the related item's label depending on TCA
227
     * and TypoScript configuration
228
     *
229
     * @param array $foreignTableTca The foreign table's TCA
230
     * @return string The field to use for the related item's label
231
     */
232 46
    protected function resolveForeignTableLabelField(array $foreignTableTca)
233
    {
234 46
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'];
235
236
        // when foreignLabelField is not enabled we can return directly
237 46
        if (empty($this->configuration['foreignLabelField'])) {
238 11
            return $foreignTableLabelField;
239
        }
240
241 37
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
242 2
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
243
        } else {
244 37
            $foreignTableLabelField = $this->configuration['foreignLabelField'];
245
        }
246
247 37
        return $foreignTableLabelField;
248
    }
249
250
    /**
251
     * Return the translated record
252
     *
253
     * @param string $tableName
254
     * @param array $record
255
     * @return array
256
     */
257 5
    protected function getTranslationOverlay($tableName, $record)
258
    {
259 5
        if ($tableName === 'pages') {
260 2
            return $GLOBALS['TSFE']->sys_page->getPageOverlay($record, $GLOBALS['TSFE']->sys_language_uid);
261
        }
262
263 4
        return $GLOBALS['TSFE']->sys_page->getRecordOverlay($tableName, $record, $GLOBALS['TSFE']->sys_language_uid);
264
    }
265
266
    /**
267
     * Gets the related items from a table using a 1:n relation.
268
     *
269
     * @param string $localTableName Local table name
270
     * @param int $localRecordUid Local record uid
271
     * @param array $localFieldTca The local table's TCA
272
     * @param ContentObjectRenderer $parentContentObject parent content object
273
     * @return array Array of related items, values already resolved from related records
274
     */
275 3
    protected function getRelatedItemsFromForeignTable(
276
        $localTableName,
277
        $localRecordUid,
278
        array $localFieldTca,
279
        ContentObjectRenderer $parentContentObject
280
    ) {
281 3
        $relatedItems = [];
282 3
        $foreignTableName = $localFieldTca['config']['foreign_table'];
283 3
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
284 3
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
285
286
            /** @var $relationHandler RelationHandler */
287 3
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
288
289 3
        $itemList = $parentContentObject->data[$this->configuration['localField']] ?? '';
290
291 3
        $relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
292 3
        $selectUids = $relationHandler->tableArray[$foreignTableName];
293
294 3
        if (!is_array($selectUids) || count($selectUids) <= 0) {
295
            return $relatedItems;
296
        }
297
298 3
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
299
300 3
        foreach ($relatedRecords as $relatedRecord) {
301 3
            $resolveRelatedValue = $this->resolveRelatedValue(
302 3
                $relatedRecord,
303 3
                $foreignTableTca,
304 3
                $foreignTableLabelField,
305 3
                $parentContentObject,
306 3
                $foreignTableName
307
            );
308 3
            if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
309 3
                $relatedItems[] = $resolveRelatedValue;
310
            }
311
        }
312
313 3
        return $relatedItems;
314
    }
315
316
    /**
317
     * Resolves the value of the related field. If the related field's value is
318
     * a relation itself, this method takes care of resolving it recursively.
319
     *
320
     * @param array $relatedRecord Related record as array
321
     * @param array $foreignTableTca TCA of the related table
322
     * @param string $foreignTableLabelField Field name of the foreign label field
323
     * @param ContentObjectRenderer $parentContentObject cObject
324
     * @param string $foreignTableName Related record table name
325
     *
326
     * @return string
327
     */
328 3
    protected function resolveRelatedValue(
329
        array $relatedRecord,
330
        $foreignTableTca,
331
        $foreignTableLabelField,
332
        ContentObjectRenderer $parentContentObject,
333
        $foreignTableName = ''
334
    ) {
335 3
        if ($GLOBALS['TSFE']->sys_language_uid > 0 && !empty($foreignTableName)) {
336
            $relatedRecord = $this->getTranslationOverlay($foreignTableName, $relatedRecord);
337
        }
338
339 3
        $value = $relatedRecord[$foreignTableLabelField];
340
341
        if (
342 3
            !empty($foreignTableName)
343 3
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
344 3
            && $this->configuration['enableRecursiveValueResolution']
345
        ) {
346
            // backup
347 2
            $backupRecord = $parentContentObject->data;
348 2
            $backupConfiguration = $this->configuration;
349
350
            // adjust configuration for next level
351 2
            $this->configuration['localField'] = $foreignTableLabelField;
352 2
            $parentContentObject->data = $relatedRecord;
353 2
            if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
354 2
                list(, $this->configuration['foreignLabelField']) = explode('.',
355 2
                    $this->configuration['foreignLabelField'], 2);
356
            } else {
357 2
                $this->configuration['foreignLabelField'] = '';
358
            }
359
360
            // recursion
361 2
            $relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
362 2
                $foreignTableName,
363 2
                $relatedRecord['uid'],
364 2
                $foreignTableTca['columns'][$foreignTableLabelField],
365 2
                $parentContentObject
366
            );
367 2
            $value = array_pop($relatedItemsFromForeignTable);
368
369
            // restore
370 2
            $this->configuration = $backupConfiguration;
371 2
            $parentContentObject->data = $backupRecord;
372
        }
373
374 3
        return $parentContentObject->stdWrap($value, $this->configuration);
375
    }
376
377
    /**
378
     * When the record has an overlay we retrieve the uid of the translated record,
379
     * to resolve the relations from the translation.
380
     *
381
     * @param string $localTableName
382
     * @param int $localRecordUid
383
     * @return int
384
     */
385 46
    protected function getUidOfRecordOverlay($localTableName, $localFieldName, $localRecordUid)
386
    {
387
        // when no language is set at all we do not need to overlay
388 46
        if (!isset($GLOBALS['TSFE']->sys_language_uid)) {
389
            return $localRecordUid;
390
        }
391
        // when no language is set we can return the passed recordUid
392 46
        if (!$GLOBALS['TSFE']->sys_language_uid > 0) {
393 44
            return $localRecordUid;
394
        }
395
        // when no TCA configured for pages_language_overlay's field, then use original record Uid
396
        // @todo this can be dropped when TYPO3 8 compatibility is dropped
397 5
        $translatedInPagesLanguageOverlayAndNoTCAPresent = Util::getIsTYPO3VersionBelow9() &&
398 5
            !$this->tcaService->getHasConfigurationForField('pages_language_overlay', $localFieldName);
399 5
        if ($localTableName === 'pages' && $translatedInPagesLanguageOverlayAndNoTCAPresent) {
400 2
            return $localRecordUid;
401
        }
402
403
        /** @var QueryBuilder $queryBuilder */
404 3
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
405 3
            ->getQueryBuilderForTable($localTableName);
406
407
        $record = $queryBuilder
408 3
            ->select('*')
409 3
            ->from($localTableName)
410 3
            ->where(
411 3
                $queryBuilder->expr()->eq('uid', $localRecordUid)
412
            )
413 3
            ->execute()
414 3
            ->fetch();
415
416
        // when the overlay is not an array, we return the localRecordUid
417 3
        if (!is_array($record)) {
418
            return $localRecordUid;
419
        }
420
421 3
        $overlayUid = $this->getLocalRecordUidFromOverlay($localTableName, $record);
422 3
        $localRecordUid = ($overlayUid !== 0) ? $overlayUid : $localRecordUid;
423 3
        return $localRecordUid;
424
    }
425
426
    /**
427
     * This method retrieves the _PAGES_OVERLAY_UID or _LOCALIZED_UID from the localized record.
428
     *
429
     * @param string $localTableName
430
     * @param array $overlayRecord
431
     * @return int
432
     */
433 3
    protected function getLocalRecordUidFromOverlay($localTableName, $overlayRecord)
434
    {
435 3
        $overlayRecord = $this->getTranslationOverlay($localTableName, $overlayRecord);
436
437
        // when there is a _PAGES_OVERLAY_UID | _LOCALIZED_UID in the overlay, we return it
438 3
        if ($localTableName === 'pages' && isset($overlayRecord['_PAGES_OVERLAY_UID'])) {
439 1
            return (int)$overlayRecord['_PAGES_OVERLAY_UID'];
440 2
        } elseif (isset($overlayRecord['_LOCALIZED_UID'])) {
441 2
            return (int)$overlayRecord['_LOCALIZED_UID'];
442
        }
443
444
        return 0;
445
    }
446
447
    /**
448
     * Return records via relation.
449
     *
450
     * @param string $foreignTable The table to fetch records from.
451
     * @param int[] ...$uids The uids to fetch from table.
452
     * @return array
453
     */
454 13
    protected function getRelatedRecords($foreignTable, int ...$uids): array
455
    {
456
        /** @var QueryBuilder $queryBuilder */
457 13
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
458 13
        $queryBuilder->select('*')
459 13
            ->from($foreignTable)
460 13
            ->where($queryBuilder->expr()->in('uid', $uids));
461 13
        if (isset($this->configuration['additionalWhereClause'])) {
462 2
            $queryBuilder->andWhere($this->configuration['additionalWhereClause']);
463
        }
464 13
        $statement = $queryBuilder->execute();
465
466 13
        return $this->sortByKeyInIN($statement, 'uid', ...$uids);
0 ignored issues
show
Bug introduced by
It seems like $statement can also be of type integer; however, parameter $statement of ApacheSolrForTypo3\Solr\...lation::sortByKeyInIN() does only seem to accept Doctrine\DBAL\Driver\Statement, 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

466
        return $this->sortByKeyInIN(/** @scrutinizer ignore-type */ $statement, 'uid', ...$uids);
Loading history...
467
    }
468
469
    /**
470
     * Sorts the result set by key in array for IN values.
471
     *   Simulates MySqls ORDER BY FIELD(fieldname, COPY_OF_IN_FOR_WHERE)
472
     *   Example: SELECT * FROM a_table WHERE field_name IN (2, 3, 4) SORT BY FIELD(field_name, 2, 3, 4)
473
     *
474
     *
475
     * @param Statement $statement
476
     * @param string $columnName
477
     * @param array $arrayWithValuesForIN
478
     * @return array
479
     */
480 13
    protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN) : array
481
    {
482 13
        $records = [];
483 13
        while ($record = $statement->fetch()) {
484 13
            $indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
485 13
            $records[$indexNumber] = $record;
486
        }
487 13
        ksort($records);
488 13
        return $records;
489
    }
490
}
491