Passed
Push — master ( 8f9ec7...80523f )
by Timo
23:39
created

Relation::resolveForeignTableLabelField()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 3
nop 1
crap 3
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 2 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\Util;
28
use Doctrine\DBAL\Driver\Statement;
29
use TYPO3\CMS\Core\Database\ConnectionPool;
30
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
31
use TYPO3\CMS\Core\Database\RelationHandler;
32
use TYPO3\CMS\Core\Utility\GeneralUtility;
33
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
34
35
/**
36
 * A content object (cObj) to resolve relations between database records
37
 *
38
 * Configuration options:
39
 *
40
 * localField: the record's field to use to resolve relations
41
 * 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
42
 * multiValue: whether to return related records suitable for a multi value field
43
 * 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.
44
 * relationTableSortingField: field in an mm relation table to sort by, usually "sorting"
45
 * 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.
46
 * removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
47
 * removeDuplicateValues: Removes duplicate values
48
 *
49
 * @author Ingo Renner <[email protected]>
50
 */
51
class Relation
52
{
53
    const CONTENT_OBJECT_NAME = 'SOLR_RELATION';
54
55
    /**
56
     * Content object configuration
57
     *
58
     * @var array
59
     */
60
    protected $configuration = [];
61
62
    /**
63
     * Constructor.
64
     *
65
     */
66 11
    public function __construct()
67
    {
68 11
        $this->configuration['enableRecursiveValueResolution'] = 1;
69 11
        $this->configuration['removeEmptyValues'] = 1;
70 11
    }
71
72
    /**
73
     * Executes the SOLR_RELATION content object.
74
     *
75
     * Resolves relations between records. Currently supported relations are
76
     * TYPO3-style m:n relations.
77
     * May resolve single value and multi value relations.
78
     *
79
     * @param string $name content object name 'SOLR_RELATION'
80
     * @param array $configuration for the content object
81
     * @param string $TyposcriptKey not used
82
     * @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $parentContentObject parent content object
83
     * @return string serialized array representation of the given list
84
     */
85 11
    public function cObjGetSingleExt(
86
        /** @noinspection PhpUnusedParameterInspection */ $name,
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
87
        array $configuration,
88
        /** @noinspection PhpUnusedParameterInspection */ $TyposcriptKey,
0 ignored issues
show
Unused Code introduced by
The parameter $TyposcriptKey is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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