Passed
Push — master ( cc3f84...4a930e )
by Timo
23:43
created

usePagesLanguageOverlayInsteadOfPagesIfPossible()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.6111
c 0
b 0
f 0
cc 5
nc 2
nop 2
crap 5
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\Language\FrontendOverlayService;
28
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
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
     * @var FrontendOverlayService
70
     */
71
    protected $frontendOverlayService = null;
72
73
    /**
74
     * Relation constructor.
75
     * @param TCAService|null $tcaService
76
     * @param FrontendOverlayService|null $frontendOverlayService
77
     */
78 46
    public function __construct(TCAService $tcaService = null, FrontendOverlayService $frontendOverlayService = null)
79
    {
80 46
        $this->configuration['enableRecursiveValueResolution'] = 1;
81 46
        $this->configuration['removeEmptyValues'] = 1;
82 46
        $this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
83 46
        $this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class);
84 46
    }
85
86
    /**
87
     * Executes the SOLR_RELATION content object.
88
     *
89
     * Resolves relations between records. Currently supported relations are
90
     * TYPO3-style m:n relations.
91
     * May resolve single value and multi value relations.
92
     *
93
     * @param string $name content object name 'SOLR_RELATION'
94
     * @param array $configuration for the content object
95
     * @param string $TyposcriptKey not used
96
     * @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $parentContentObject parent content object
97
     * @return string serialized array representation of the given list
98
     */
99 46
    public function cObjGetSingleExt(
100
        /** @noinspection PhpUnusedParameterInspection */ $name,
101
        array $configuration,
102
        /** @noinspection PhpUnusedParameterInspection */ $TyposcriptKey,
103
        $parentContentObject
104
    ) {
105 46
        $this->configuration = array_merge($this->configuration, $configuration);
106
107 46
        $relatedItems = $this->getRelatedItems($parentContentObject);
108
109 46
        if (!empty($this->configuration['removeDuplicateValues'])) {
110
            $relatedItems = array_unique($relatedItems);
111
        }
112
113 46
        if (empty($configuration['multiValue'])) {
114
            // single value, need to concatenate related items
115 2
            $singleValueGlue = !empty($configuration['singleValueGlue']) ? trim($configuration['singleValueGlue'], '|') : ', ';
116 2
            $result = implode($singleValueGlue, $relatedItems);
117
        } else {
118
            // multi value, need to serialize as content objects must return strings
119 44
            $result = serialize($relatedItems);
120
        }
121
122 46
        return $result;
123
    }
124
125
    /**
126
     * Gets the related items of the current record's configured field.
127
     *
128
     * @param ContentObjectRenderer $parentContentObject parent content object
129
     * @return array Array of related items, values already resolved from related records
130
     */
131 46
    protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
132
    {
133 46
        list($table, $uid) = explode(':', $parentContentObject->currentRecord);
134 46
        $uid = (int) $uid;
135 46
        $field = $this->configuration['localField'];
136
137 46
        if (!$this->tcaService->getHasConfigurationForField($table, $field)) {
138
            return [];
139
        }
140
141
        /**
142
         * @todo this can be removed when TYPO3 8 support is dropped since overlays are then also stored in the same
143
         * table for pages
144
         */
145 46
        $overlayTable = $this->frontendOverlayService->getOverlayTable($table, $field);
146 46
        $overlayUid = $this->frontendOverlayService->getUidOfOverlay($table, $field, $uid);
147 46
        $fieldTCA = $this->tcaService->getConfigurationForField($overlayTable, $field);
148
149 46
        if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
150 43
            $relatedItems = $this->getRelatedItemsFromMMTable($overlayTable, $overlayUid, $fieldTCA);
151
        } else {
152 3
            $relatedItems = $this->getRelatedItemsFromForeignTable($overlayTable, $overlayUid, $fieldTCA, $parentContentObject);
153
        }
154
155 46
        return $relatedItems;
156
    }
157
158
    /**
159
     * Gets the related items from a table using a n:m relation.
160
     *
161
     * @param string $localTableName Local table name
162
     * @param int $localRecordUid Local record uid
163
     * @param array $localFieldTca The local table's TCA
164
     * @return array Array of related items, values already resolved from related records
165
     */
166 43
    protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid, array $localFieldTca)
167
    {
168 43
        $relatedItems = [];
169 43
        $foreignTableName = $localFieldTca['config']['foreign_table'];
170 43
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
171 43
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
172 43
        $mmTableName = $localFieldTca['config']['MM'];
173
174
        // Remove the first option of foreignLabelField for recursion
175 43
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
176
            $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
177
            unset($foreignTableLabelFieldArr[0]);
178
            $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
179
        }
180
181 43
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
182 43
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
183 43
        $selectUids = $relationHandler->tableArray[$foreignTableName];
184 43
        if (!is_array($selectUids) || count($selectUids) <= 0) {
185 35
            return $relatedItems;
186
        }
187
188 10
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
189 10
        foreach ($relatedRecords as $record) {
190 10
            if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
191 10
                && $this->configuration['enableRecursiveValueResolution']
192
            ) {
193
                if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
194
                    $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
195
                    unset($foreignTableLabelFieldArr[0]);
196
                    $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
197
                }
198
199
                $this->configuration['localField'] = $foreignTableLabelField;
200
201
                $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
202
                $contentObject->start($record, $foreignTableName);
203
204
                return $this->getRelatedItems($contentObject);
205
            } else {
206 10
                if ($GLOBALS['TSFE']->sys_language_uid > 0) {
207 5
                    $record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
208
                }
209 10
                $relatedItems[] = $record[$foreignTableLabelField];
210
            }
211
        }
212
213 10
        return $relatedItems;
214
    }
215
216
    /**
217
     * Resolves the field to use as the related item's label depending on TCA
218
     * and TypoScript configuration
219
     *
220
     * @param array $foreignTableTca The foreign table's TCA
221
     * @return string The field to use for the related item's label
222
     */
223 46
    protected function resolveForeignTableLabelField(array $foreignTableTca)
224
    {
225 46
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'];
226
227
        // when foreignLabelField is not enabled we can return directly
228 46
        if (empty($this->configuration['foreignLabelField'])) {
229 11
            return $foreignTableLabelField;
230
        }
231
232 37
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
233 2
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
234
        } else {
235 37
            $foreignTableLabelField = $this->configuration['foreignLabelField'];
236
        }
237
238 37
        return $foreignTableLabelField;
239
    }
240
241
    /**
242
     * Gets the related items from a table using a 1:n relation.
243
     *
244
     * @param string $localTableName Local table name
245
     * @param int $localRecordUid Local record uid
246
     * @param array $localFieldTca The local table's TCA
247
     * @param ContentObjectRenderer $parentContentObject parent content object
248
     * @return array Array of related items, values already resolved from related records
249
     */
250 3
    protected function getRelatedItemsFromForeignTable(
251
        $localTableName,
252
        $localRecordUid,
253
        array $localFieldTca,
254
        ContentObjectRenderer $parentContentObject
255
    ) {
256 3
        $relatedItems = [];
257 3
        $foreignTableName = $localFieldTca['config']['foreign_table'];
258 3
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
259 3
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
260
261
            /** @var $relationHandler RelationHandler */
262 3
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
263
264 3
        $itemList = $parentContentObject->data[$this->configuration['localField']] ?? '';
265
266 3
        $relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
267 3
        $selectUids = $relationHandler->tableArray[$foreignTableName];
268
269 3
        if (!is_array($selectUids) || count($selectUids) <= 0) {
270
            return $relatedItems;
271
        }
272
273 3
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
274
275 3
        foreach ($relatedRecords as $relatedRecord) {
276 3
            $resolveRelatedValue = $this->resolveRelatedValue(
277 3
                $relatedRecord,
278 3
                $foreignTableTca,
279 3
                $foreignTableLabelField,
280 3
                $parentContentObject,
281 3
                $foreignTableName
282
            );
283 3
            if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
284 3
                $relatedItems[] = $resolveRelatedValue;
285
            }
286
        }
287
288 3
        return $relatedItems;
289
    }
290
291
    /**
292
     * Resolves the value of the related field. If the related field's value is
293
     * a relation itself, this method takes care of resolving it recursively.
294
     *
295
     * @param array $relatedRecord Related record as array
296
     * @param array $foreignTableTca TCA of the related table
297
     * @param string $foreignTableLabelField Field name of the foreign label field
298
     * @param ContentObjectRenderer $parentContentObject cObject
299
     * @param string $foreignTableName Related record table name
300
     *
301
     * @return string
302
     */
303 3
    protected function resolveRelatedValue(
304
        array $relatedRecord,
305
        $foreignTableTca,
306
        $foreignTableLabelField,
307
        ContentObjectRenderer $parentContentObject,
308
        $foreignTableName = ''
309
    ) {
310 3
        if ($GLOBALS['TSFE']->sys_language_uid > 0 && !empty($foreignTableName)) {
311
            $relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
312
        }
313
314 3
        $value = $relatedRecord[$foreignTableLabelField];
315
316
        if (
317 3
            !empty($foreignTableName)
318 3
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
319 3
            && $this->configuration['enableRecursiveValueResolution']
320
        ) {
321
            // backup
322 2
            $backupRecord = $parentContentObject->data;
323 2
            $backupConfiguration = $this->configuration;
324
325
            // adjust configuration for next level
326 2
            $this->configuration['localField'] = $foreignTableLabelField;
327 2
            $parentContentObject->data = $relatedRecord;
328 2
            if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
329 2
                list(, $this->configuration['foreignLabelField']) = explode('.',
330 2
                    $this->configuration['foreignLabelField'], 2);
331
            } else {
332 2
                $this->configuration['foreignLabelField'] = '';
333
            }
334
335
            // recursion
336 2
            $relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
337 2
                $foreignTableName,
338 2
                $relatedRecord['uid'],
339 2
                $foreignTableTca['columns'][$foreignTableLabelField],
340 2
                $parentContentObject
341
            );
342 2
            $value = array_pop($relatedItemsFromForeignTable);
343
344
            // restore
345 2
            $this->configuration = $backupConfiguration;
346 2
            $parentContentObject->data = $backupRecord;
347
        }
348
349 3
        return $parentContentObject->stdWrap($value, $this->configuration);
350
    }
351
352
    /**
353
     * Return records via relation.
354
     *
355
     * @param string $foreignTable The table to fetch records from.
356
     * @param int[] ...$uids The uids to fetch from table.
357
     * @return array
358
     */
359 13
    protected function getRelatedRecords($foreignTable, int ...$uids): array
360
    {
361
        /** @var QueryBuilder $queryBuilder */
362 13
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
363 13
        $queryBuilder->select('*')
364 13
            ->from($foreignTable)
365 13
            ->where($queryBuilder->expr()->in('uid', $uids));
366 13
        if (isset($this->configuration['additionalWhereClause'])) {
367 2
            $queryBuilder->andWhere($this->configuration['additionalWhereClause']);
368
        }
369 13
        $statement = $queryBuilder->execute();
370
371 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

371
        return $this->sortByKeyInIN(/** @scrutinizer ignore-type */ $statement, 'uid', ...$uids);
Loading history...
372
    }
373
374
    /**
375
     * Sorts the result set by key in array for IN values.
376
     *   Simulates MySqls ORDER BY FIELD(fieldname, COPY_OF_IN_FOR_WHERE)
377
     *   Example: SELECT * FROM a_table WHERE field_name IN (2, 3, 4) SORT BY FIELD(field_name, 2, 3, 4)
378
     *
379
     *
380
     * @param Statement $statement
381
     * @param string $columnName
382
     * @param array $arrayWithValuesForIN
383
     * @return array
384
     */
385 13
    protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN) : array
386
    {
387 13
        $records = [];
388 13
        while ($record = $statement->fetch()) {
389 13
            $indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
390 13
            $records[$indexNumber] = $record;
391
        }
392 13
        ksort($records);
393 13
        return $records;
394
    }
395
}
396