Passed
Push — master ( 498335...52b5ef )
by Rafael
42:19
created

Relation::getRelatedItemsFromForeignTable()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6.0033

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 39
ccs 21
cts 22
cp 0.9545
rs 8.9617
c 0
b 0
f 0
cc 6
nc 4
nop 4
crap 6.0033
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 3
                $foreignTableTca,
269 3
                $foreignTableLabelField,
270 3
                $parentContentObject,
271 3
                $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 2
                $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()) {
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Driver\ResultStatement::fetch() has been deprecated: Use fetchNumeric(), fetchAssociative() or fetchOne() instead. ( Ignorable by Annotation )

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

378
        while ($record = /** @scrutinizer ignore-deprecated */ $statement->fetch()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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