Passed
Push — master ( 6d83d0...d2efa3 )
by Timo
45:45 queued 16:20
created

Relation::getRelatedItemsFromMMTable()   B

Complexity

Conditions 9
Paths 12

Size

Total Lines 48
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 12.6176

Importance

Changes 0
Metric Value
eloc 31
dl 0
loc 48
ccs 20
cts 31
cp 0.6452
rs 8.0555
c 0
b 0
f 0
cc 9
nc 12
nop 3
crap 12.6176
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 ApacheSolrForTypo3\Solr\Util;
30
use Doctrine\DBAL\Driver\Statement;
31
use TYPO3\CMS\Core\Database\ConnectionPool;
32
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
33
use TYPO3\CMS\Core\Database\RelationHandler;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
36
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
37
38
/**
39
 * A content object (cObj) to resolve relations between database records
40
 *
41
 * Configuration options:
42
 *
43
 * localField: the record's field to use to resolve relations
44
 * 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
45
 * multiValue: whether to return related records suitable for a multi value field
46
 * 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.
47
 * relationTableSortingField: field in an mm relation table to sort by, usually "sorting"
48
 * 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.
49
 * removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
50
 * removeDuplicateValues: Removes duplicate values
51
 *
52
 * @author Ingo Renner <[email protected]>
53
 */
54
class Relation extends AbstractContentObject
55
{
56
    const CONTENT_OBJECT_NAME = 'SOLR_RELATION';
57
58
    /**
59
     * Content object configuration
60
     *
61
     * @var array
62
     */
63
    protected $configuration = [];
64
65
    /**
66
     * @var TCAService
67
     */
68
    protected $tcaService = null;
69
70
    /**
71
     * @var FrontendOverlayService
72
     */
73
    protected $frontendOverlayService = null;
74
75
    /**
76
     * Relation constructor.
77
     * @param TCAService|null $tcaService
78
     * @param FrontendOverlayService|null $frontendOverlayService
79
     */
80 47
    public function __construct(ContentObjectRenderer $cObj, TCAService $tcaService = null, FrontendOverlayService $frontendOverlayService = null)
81
    {
82 47
        $this->cObj = $cObj;
83 47
        $this->configuration['enableRecursiveValueResolution'] = 1;
84 47
        $this->configuration['removeEmptyValues'] = 1;
85 47
        $this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
86 47
        $this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class);
87 47
    }
88
89
    /**
90
     * Executes the SOLR_RELATION content object.
91
     *
92
     * Resolves relations between records. Currently supported relations are
93
     * TYPO3-style m:n relations.
94
     * May resolve single value and multi value relations.
95
     *
96
     * @inheritDoc
97
     */
98 47
    public function render($conf = [])
99
    {
100 47
        $this->configuration = array_merge($this->configuration, $conf);
101
102 47
        $relatedItems = $this->getRelatedItems($this->cObj);
103
104 47
        if (!empty($this->configuration['removeDuplicateValues'])) {
105
            $relatedItems = array_unique($relatedItems);
106
        }
107
108 47
        if (empty($conf['multiValue'])) {
109
            // single value, need to concatenate related items
110 2
            $singleValueGlue = !empty($conf['singleValueGlue']) ? trim($conf['singleValueGlue'], '|') : ', ';
111 2
            $result = implode($singleValueGlue, $relatedItems);
112
        } else {
113
            // multi value, need to serialize as content objects must return strings
114 45
            $result = serialize($relatedItems);
115
        }
116
117 47
        return $result;
118
    }
119
120
    /**
121
     * Gets the related items of the current record's configured field.
122
     *
123
     * @param ContentObjectRenderer $parentContentObject parent content object
124
     * @return array Array of related items, values already resolved from related records
125
     */
126 47
    protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
127
    {
128 47
        list($table, $uid) = explode(':', $parentContentObject->currentRecord);
129 47
        $uid = (int) $uid;
130 47
        $field = $this->configuration['localField'];
131
132 47
        if (!$this->tcaService->getHasConfigurationForField($table, $field)) {
133
            return [];
134
        }
135
136 47
        $overlayUid = $this->frontendOverlayService->getUidOfOverlay($table, $field, $uid);
137 47
        $fieldTCA = $this->tcaService->getConfigurationForField($table, $field);
138
139 47
        if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
140 44
            $relatedItems = $this->getRelatedItemsFromMMTable($table, $overlayUid, $fieldTCA);
141
        } else {
142 3
            $relatedItems = $this->getRelatedItemsFromForeignTable($table, $overlayUid, $fieldTCA, $parentContentObject);
143
        }
144
145 47
        return $relatedItems;
146
    }
147
148
    /**
149
     * Gets the related items from a table using a n:m relation.
150
     *
151
     * @param string $localTableName Local table name
152
     * @param int $localRecordUid Local record uid
153
     * @param array $localFieldTca The local table's TCA
154
     * @return array Array of related items, values already resolved from related records
155
     */
156 44
    protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid, array $localFieldTca)
157
    {
158 44
        $relatedItems = [];
159 44
        $foreignTableName = $localFieldTca['config']['foreign_table'];
160 44
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
161 44
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
162 44
        $mmTableName = $localFieldTca['config']['MM'];
163
164
        // Remove the first option of foreignLabelField for recursion
165 44
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
166
            $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
167
            unset($foreignTableLabelFieldArr[0]);
168
            $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
169
        }
170
171 44
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
172 44
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
173 44
        $selectUids = $relationHandler->tableArray[$foreignTableName];
174 44
        if (!is_array($selectUids) || count($selectUids) <= 0) {
175 36
            return $relatedItems;
176
        }
177
178 10
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
179 10
        foreach ($relatedRecords as $record) {
180 10
            if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
181 10
                && $this->configuration['enableRecursiveValueResolution']
182
            ) {
183
                if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
184
                    $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
185
                    unset($foreignTableLabelFieldArr[0]);
186
                    $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
187
                }
188
189
                $this->configuration['localField'] = $foreignTableLabelField;
190
191
                $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
192
                $contentObject->start($record, $foreignTableName);
193
194
                return $this->getRelatedItems($contentObject);
195
            } else {
196 10
                if (Util::getLanguageUid() > 0) {
197 7
                    $record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
198
                }
199 10
                $relatedItems[] = $record[$foreignTableLabelField];
200
            }
201
        }
202
203 10
        return $relatedItems;
204
    }
205
206
    /**
207
     * Resolves the field to use as the related item's label depending on TCA
208
     * and TypoScript configuration
209
     *
210
     * @param array $foreignTableTca The foreign table's TCA
211
     * @return string The field to use for the related item's label
212
     */
213 47
    protected function resolveForeignTableLabelField(array $foreignTableTca)
214
    {
215 47
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'];
216
217
        // when foreignLabelField is not enabled we can return directly
218 47
        if (empty($this->configuration['foreignLabelField'])) {
219 11
            return $foreignTableLabelField;
220
        }
221
222 38
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
223 2
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
224
        } else {
225 38
            $foreignTableLabelField = $this->configuration['foreignLabelField'];
226
        }
227
228 38
        return $foreignTableLabelField;
229
    }
230
231
    /**
232
     * Gets the related items from a table using a 1:n relation.
233
     *
234
     * @param string $localTableName Local table name
235
     * @param int $localRecordUid Local record uid
236
     * @param array $localFieldTca The local table's TCA
237
     * @param ContentObjectRenderer $parentContentObject parent content object
238
     * @return array Array of related items, values already resolved from related records
239
     */
240 3
    protected function getRelatedItemsFromForeignTable(
241
        $localTableName,
242
        $localRecordUid,
243
        array $localFieldTca,
244
        ContentObjectRenderer $parentContentObject
245
    ) {
246 3
        $relatedItems = [];
247 3
        $foreignTableName = $localFieldTca['config']['foreign_table'];
248 3
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
249 3
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
250
251
            /** @var $relationHandler RelationHandler */
252 3
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
253
254 3
        $itemList = $parentContentObject->data[$this->configuration['localField']] ?? '';
255
256 3
        $relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
257 3
        $selectUids = $relationHandler->tableArray[$foreignTableName];
258
259 3
        if (!is_array($selectUids) || count($selectUids) <= 0) {
260
            return $relatedItems;
261
        }
262
263 3
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
264
265 3
        foreach ($relatedRecords as $relatedRecord) {
266 3
            $resolveRelatedValue = $this->resolveRelatedValue(
267 3
                $relatedRecord,
268
                $foreignTableTca,
269
                $foreignTableLabelField,
270
                $parentContentObject,
271
                $foreignTableName
272
            );
273 3
            if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
274 3
                $relatedItems[] = $resolveRelatedValue;
275
            }
276
        }
277
278 3
        return $relatedItems;
279
    }
280
281
    /**
282
     * Resolves the value of the related field. If the related field's value is
283
     * a relation itself, this method takes care of resolving it recursively.
284
     *
285
     * @param array $relatedRecord Related record as array
286
     * @param array $foreignTableTca TCA of the related table
287
     * @param string $foreignTableLabelField Field name of the foreign label field
288
     * @param ContentObjectRenderer $parentContentObject cObject
289
     * @param string $foreignTableName Related record table name
290
     *
291
     * @return string
292
     */
293 3
    protected function resolveRelatedValue(
294
        array $relatedRecord,
295
        $foreignTableTca,
296
        $foreignTableLabelField,
297
        ContentObjectRenderer $parentContentObject,
298
        $foreignTableName = ''
299
    ) {
300 3
        if (Util::getLanguageUid() > 0 && !empty($foreignTableName)) {
301 3
            $relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
302
        }
303
304 3
        $value = $relatedRecord[$foreignTableLabelField];
305
306
        if (
307 3
            !empty($foreignTableName)
308 3
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
309 3
            && $this->configuration['enableRecursiveValueResolution']
310
        ) {
311
            // backup
312 2
            $backupRecord = $parentContentObject->data;
313 2
            $backupConfiguration = $this->configuration;
314
315
            // adjust configuration for next level
316 2
            $this->configuration['localField'] = $foreignTableLabelField;
317 2
            $parentContentObject->data = $relatedRecord;
318 2
            if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
319 2
                list(, $this->configuration['foreignLabelField']) = explode('.',
320 2
                    $this->configuration['foreignLabelField'], 2);
321
            } else {
322 2
                $this->configuration['foreignLabelField'] = '';
323
            }
324
325
            // recursion
326 2
            $relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
327 2
                $foreignTableName,
328 2
                $relatedRecord['uid'],
329 2
                $foreignTableTca['columns'][$foreignTableLabelField],
330
                $parentContentObject
331
            );
332 2
            $value = array_pop($relatedItemsFromForeignTable);
333
334
            // restore
335 2
            $this->configuration = $backupConfiguration;
336 2
            $parentContentObject->data = $backupRecord;
337
        }
338
339 3
        return $parentContentObject->stdWrap($value, $this->configuration);
340
    }
341
342
    /**
343
     * Return records via relation.
344
     *
345
     * @param string $foreignTable The table to fetch records from.
346
     * @param int[] ...$uids The uids to fetch from table.
347
     * @return array
348
     */
349 13
    protected function getRelatedRecords($foreignTable, int ...$uids): array
350
    {
351
        /** @var QueryBuilder $queryBuilder */
352 13
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
353 13
        $queryBuilder->select('*')
354 13
            ->from($foreignTable)
355 13
            ->where($queryBuilder->expr()->in('uid', $uids));
356 13
        if (isset($this->configuration['additionalWhereClause'])) {
357 2
            $queryBuilder->andWhere($this->configuration['additionalWhereClause']);
358
        }
359 13
        $statement = $queryBuilder->execute();
360
361 13
        return $this->sortByKeyInIN($statement, 'uid', ...$uids);
362
    }
363
364
    /**
365
     * Sorts the result set by key in array for IN values.
366
     *   Simulates MySqls ORDER BY FIELD(fieldname, COPY_OF_IN_FOR_WHERE)
367
     *   Example: SELECT * FROM a_table WHERE field_name IN (2, 3, 4) SORT BY FIELD(field_name, 2, 3, 4)
368
     *
369
     *
370
     * @param Statement $statement
371
     * @param string $columnName
372
     * @param array $arrayWithValuesForIN
373
     * @return array
374
     */
375 13
    protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN) : array
376
    {
377 13
        $records = [];
378 13
        while ($record = $statement->fetch()) {
379 13
            $indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
380 13
            $records[$indexNumber] = $record;
381
        }
382 13
        ksort($records);
383 13
        return $records;
384
    }
385
}
386