Relation::getRelatedItemsFromMMTable()   B
last analyzed

Complexity

Conditions 9
Paths 12

Size

Total Lines 48
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 9.2733

Importance

Changes 0
Metric Value
eloc 31
c 0
b 0
f 0
dl 0
loc 48
ccs 17
cts 20
cp 0.85
rs 8.0555
cc 9
nc 12
nop 3
crap 9.2733
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 45
     * @var TCAService
67
     */
68 45
    protected $tcaService = null;
69 45
70 45
    /**
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
    public function __construct(ContentObjectRenderer $cObj, TCAService $tcaService = null, FrontendOverlayService $frontendOverlayService = null)
81
    {
82
        $this->cObj = $cObj;
83
        $this->configuration['enableRecursiveValueResolution'] = 1;
84
        $this->configuration['removeEmptyValues'] = 1;
85 45
        $this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
86
        $this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class);
87
    }
88
89
    /**
90
     * Executes the SOLR_RELATION content object.
91 45
     *
92
     * Resolves relations between records. Currently supported relations are
93 45
     * TYPO3-style m:n relations.
94
     * May resolve single value and multi value relations.
95 45
     *
96
     * @inheritDoc
97
     */
98
    public function render($conf = [])
99 45
    {
100
        $this->configuration = array_merge($this->configuration, $conf);
101 2
102
        $relatedItems = $this->getRelatedItems($this->cObj);
103 2
104
        if (!empty($this->configuration['removeDuplicateValues'])) {
105
            $relatedItems = array_unique($relatedItems);
106
        }
107 2
108
        if (empty($conf['multiValue'])) {
109
            // single value, need to concatenate related items
110 43
            $singleValueGlue = !empty($conf['singleValueGlue']) ? trim($conf['singleValueGlue'], '|') : ', ';
111
            $result = implode($singleValueGlue, $relatedItems);
112
        } else {
113 45
            // multi value, need to serialize as content objects must return strings
114
            $result = serialize($relatedItems);
115
        }
116
117
        return $result;
118
    }
119
120
    /**
121
     * Gets the related items of the current record's configured field.
122 45
     *
123
     * @param ContentObjectRenderer $parentContentObject parent content object
124
     * @return array Array of related items, values already resolved from related records
125
     */
126 45
    protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
127 45
    {
128
        list($table, $uid) = explode(':', $parentContentObject->currentRecord);
129 45
        $uid = (int) $uid;
130
        $field = $this->configuration['localField'];
131
132
        if (!$this->tcaService->getHasConfigurationForField($table, $field)) {
133 45
            return [];
134 45
        }
135
136 45
        $overlayUid = $this->frontendOverlayService->getUidOfOverlay($table, $field, $uid);
137 45
        $fieldTCA = $this->tcaService->getConfigurationForField($table, $field);
138 42
139 42
        if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
140
            $relatedItems = $this->getRelatedItemsFromMMTable($table, $overlayUid, $fieldTCA);
141 3
        } else {
142 3
            $relatedItems = $this->getRelatedItemsFromForeignTable($table, $overlayUid, $fieldTCA, $parentContentObject);
143
        }
144
145 45
        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 45
     * @return array Array of related items, values already resolved from related records
155
     */
156
    protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid, array $localFieldTca)
157 45
    {
158 45
        $relatedItems = [];
159 45
        $foreignTableName = $localFieldTca['config']['foreign_table'];
160 45
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
161 1
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
162
        $mmTableName = $localFieldTca['config']['MM'];
163
164 45
        // Remove the first option of foreignLabelField for recursion
165
        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
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
172
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
173
        $selectUids = $relationHandler->tableArray[$foreignTableName];
174 45
        if (!is_array($selectUids) || count($selectUids) <= 0) {
175
            return $relatedItems;
176 45
        }
177
178
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
179
        foreach ($relatedRecords as $record) {
180
            if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
181
                && $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 42
                }
188
189 42
                $this->configuration['localField'] = $foreignTableLabelField;
190 42
191 42
                $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
192 42
                $contentObject->start($record, $foreignTableName);
193 42
194
                return $this->getRelatedItems($contentObject);
195
            } else {
196 42
                if (Util::getLanguageUid() > 0) {
197
                    $record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
198
                }
199
                $relatedItems[] = $record[$foreignTableLabelField];
200
            }
201
        }
202 42
203 42
        return $relatedItems;
204 42
    }
205 42
206 34
    /**
207
     * Resolves the field to use as the related item's label depending on TCA
208
     * and TypoScript configuration
209 10
     *
210 10
     * @param array $foreignTableTca The foreign table's TCA
211 10
     * @return string The field to use for the related item's label
212 10
     */
213
    protected function resolveForeignTableLabelField(array $foreignTableTca)
214
    {
215
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'];
216
217
        // when foreignLabelField is not enabled we can return directly
218
        if (empty($this->configuration['foreignLabelField'])) {
219
            return $foreignTableLabelField;
220
        }
221
222
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
223
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
224
        } else {
225
            $foreignTableLabelField = $this->configuration['foreignLabelField'];
226
        }
227 10
228 5
        return $foreignTableLabelField;
229
    }
230 10
231
    /**
232
     * Gets the related items from a table using a 1:n relation.
233
     *
234 10
     * @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
    protected function getRelatedItemsFromForeignTable(
241
        $localTableName,
242
        $localRecordUid,
243
        array $localFieldTca,
244 45
        ContentObjectRenderer $parentContentObject
245
    ) {
246 45
        $relatedItems = [];
247
        $foreignTableName = $localFieldTca['config']['foreign_table'];
248
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
249 45
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
250 11
251
            /** @var $relationHandler RelationHandler */
252
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
253 36
254 2
        $itemList = $parentContentObject->data[$this->configuration['localField']] ?? '';
255
256 36
        $relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
257
        $selectUids = $relationHandler->tableArray[$foreignTableName];
258
259 36
        if (!is_array($selectUids) || count($selectUids) <= 0) {
260
            return $relatedItems;
261
        }
262
263
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
264
265
        foreach ($relatedRecords as $relatedRecord) {
266
            $resolveRelatedValue = $this->resolveRelatedValue(
267
                $relatedRecord,
268
                $foreignTableTca,
269 5
                $foreignTableLabelField,
270
                $parentContentObject,
271 5
                $foreignTableName
272 2
            );
273
            if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
274
                $relatedItems[] = $resolveRelatedValue;
275 4
            }
276
        }
277
278
        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 3
     * @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 3
        array $relatedRecord,
295 3
        $foreignTableTca,
296 3
        $foreignTableLabelField,
297
        ContentObjectRenderer $parentContentObject,
298
        $foreignTableName = ''
299 3
    ) {
300
        if (Util::getLanguageUid() > 0 && !empty($foreignTableName)) {
301 3
            $relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
302 3
        }
303
304 3
        $value = $relatedRecord[$foreignTableLabelField];
305 3
306
        if (
307 3
            !empty($foreignTableName)
308
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
309
            && $this->configuration['enableRecursiveValueResolution']
310
        ) {
311 3
            // backup
312
            $backupRecord = $parentContentObject->data;
313 3
            $backupConfiguration = $this->configuration;
314 3
315 3
            // adjust configuration for next level
316 3
            $this->configuration['localField'] = $foreignTableLabelField;
317 3
            $parentContentObject->data = $relatedRecord;
318 3
            if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
319 3
                list(, $this->configuration['foreignLabelField']) = explode('.',
320
                    $this->configuration['foreignLabelField'], 2);
321 3
            } else {
322 3
                $this->configuration['foreignLabelField'] = '';
323
            }
324
325
            // recursion
326 3
            $relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
327
                $foreignTableName,
328
                $relatedRecord['uid'],
329
                $foreignTableTca['columns'][$foreignTableLabelField],
330
                $parentContentObject
331
            );
332
            $value = array_pop($relatedItemsFromForeignTable);
333
334
            // restore
335
            $this->configuration = $backupConfiguration;
336
            $parentContentObject->data = $backupRecord;
337
        }
338
339
        return $parentContentObject->stdWrap($value, $this->configuration);
340
    }
341 3
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 3
     */
349
    protected function getRelatedRecords($foreignTable, int ...$uids): array
350
    {
351
        /** @var QueryBuilder $queryBuilder */
352 3
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
353
        $queryBuilder->select('*')
354
            ->from($foreignTable)
355 3
            ->where($queryBuilder->expr()->in('uid', $uids));
356 3
        if (isset($this->configuration['additionalWhereClause'])) {
357 3
            $queryBuilder->andWhere($this->configuration['additionalWhereClause']);
358
        }
359
        $statement = $queryBuilder->execute();
360 2
361 2
        return $this->sortByKeyInIN($statement, 'uid', ...$uids);
362
    }
363
364 2
    /**
365 2
     * Sorts the result set by key in array for IN values.
366 2
     *   Simulates MySqls ORDER BY FIELD(fieldname, COPY_OF_IN_FOR_WHERE)
367 2
     *   Example: SELECT * FROM a_table WHERE field_name IN (2, 3, 4) SORT BY FIELD(field_name, 2, 3, 4)
368 2
     *
369
     *
370 2
     * @param Statement $statement
371
     * @param string $columnName
372
     * @param array $arrayWithValuesForIN
373
     * @return array
374 2
     */
375 2
    protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN) : array
376 2
    {
377 2
        $records = [];
378 2
        while ($record = $statement->fetch()) {
379
            $indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
380 2
            $records[$indexNumber] = $record;
381
        }
382
        ksort($records);
383 2
        return $records;
384 2
    }
385
}
386