Passed
Push — master ( 3cd8c6...9e747f )
by Timo
38:54 queued 16:17
created

Relation::getUidOfRecordOverlay()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.7458

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 10
cts 17
cp 0.5881
rs 8.5906
c 0
b 0
f 0
cc 5
eloc 12
nc 5
nop 2
crap 6.7458
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 TYPO3\CMS\Core\Database\RelationHandler;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
30
use TYPO3\CMS\Frontend\Page\PageRepository;
31
32
/**
33
 * A content object (cObj) to resolve relations between database records
34
 *
35
 * Configuration options:
36
 *
37
 * localField: the record's field to use to resolve relations
38
 * 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
39
 * multiValue: whether to return related records suitable for a multi value field
40
 * 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.
41
 * relationTableSortingField: field in an mm relation table to sort by, usually "sorting"
42
 * 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.
43
 * removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
44
 * removeDuplicateValues: Removes duplicate values
45
 *
46
 * @author Ingo Renner <[email protected]>
47
 */
48
class Relation
49
{
50
    const CONTENT_OBJECT_NAME = 'SOLR_RELATION';
51
52
    /**
53
     * Content object configuration
54
     *
55
     * @var array
56
     */
57
    protected $configuration = [];
58
59
    /**
60
     * Constructor.
61
     *
62
     */
63 7
    public function __construct()
64
    {
65 7
        $this->configuration['enableRecursiveValueResolution'] = 1;
66 7
        $this->configuration['removeEmptyValues'] = 1;
67 7
    }
68
69
    /**
70
     * Executes the SOLR_RELATION content object.
71
     *
72
     * Resolves relations between records. Currently supported relations are
73
     * TYPO3-style m:n relations.
74
     * May resolve single value and multi value relations.
75
     *
76
     * @param string $name content object name 'SOLR_RELATION'
77
     * @param array $configuration for the content object
78
     * @param string $TyposcriptKey not used
79
     * @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $parentContentObject parent content object
80
     * @return string serialized array representation of the given list
81
     */
82 7
    public function cObjGetSingleExt(
83
        /** @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...
84
        array $configuration,
85
        /** @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...
86
        $parentContentObject
87
    ) {
88 7
        $this->configuration = array_merge($this->configuration, $configuration);
89
90 7
        $relatedItems = $this->getRelatedItems($parentContentObject);
91
92 7
        if (!empty($this->configuration['removeDuplicateValues'])) {
93
            $relatedItems = array_unique($relatedItems);
94
        }
95
96 7
        if (empty($configuration['multiValue'])) {
97
            // single value, need to concatenate related items
98
            $singleValueGlue = ', ';
99
100
            if (!empty($configuration['singleValueGlue'])) {
101
                $singleValueGlue = trim($configuration['singleValueGlue'], '|');
102
            }
103
104
            $result = implode($singleValueGlue, $relatedItems);
105
        } else {
106
            // multi value, need to serialize as content objects must return strings
107 7
            $result = serialize($relatedItems);
108
        }
109
110 7
        return $result;
111
    }
112
113
    /**
114
     * Gets the related items of the current record's configured field.
115
     *
116
     * @param ContentObjectRenderer $parentContentObject parent content object
117
     * @return array Array of related items, values already resolved from related records
118
     */
119 7
    protected function getRelatedItems(
120
        ContentObjectRenderer $parentContentObject
121
    ) {
122 7
        $relatedItems = [];
123
124 7
        list($localTableName, $localRecordUid) = explode(':',
125 7
            $parentContentObject->currentRecord);
126
127 7
        $localTableNameOrg = $localTableName;
128
        // pages has a special overlay table constriction
129 7
        if ($GLOBALS['TSFE']->sys_language_uid > 0 && $localTableName === 'pages') {
130 1
            $localTableName = 'pages_language_overlay';
131
        }
132
133 7
        $localTableTca = $GLOBALS['TCA'][$localTableName];
134 7
        $localFieldName = $this->configuration['localField'];
135
136 7
        if (isset($localTableTca['columns'][$localFieldName])) {
137 7
            $localFieldTca = $localTableTca['columns'][$localFieldName];
138 7
            $localRecordUid = $this->getUidOfRecordOverlay($localTableNameOrg, $localRecordUid);
139 7
            if (isset($localFieldTca['config']['MM']) && trim($localFieldTca['config']['MM']) !== '') {
140 5
                $relatedItems = $this->getRelatedItemsFromMMTable($localTableName,
141
                    $localRecordUid, $localFieldTca);
142
            } else {
143 2
                $relatedItems = $this->getRelatedItemsFromForeignTable($localTableName,
144
                    $localRecordUid, $localFieldTca, $parentContentObject);
145
            }
146
        }
147
148 7
        return $relatedItems;
149
    }
150
151
    /**
152
     * Gets the related items from a table using a n:m relation.
153
     *
154
     * @param string $localTableName Local table name
155
     * @param int $localRecordUid Local record uid
156
     * @param array $localFieldTca The local table's TCA
157
     * @return array Array of related items, values already resolved from related records
158
     */
159 5
    protected function getRelatedItemsFromMMTable(
160
        $localTableName,
161
        $localRecordUid,
162
        array $localFieldTca
163
    ) {
164 5
        $relatedItems = [];
165 5
        $foreignTableName = $localFieldTca['config']['foreign_table'];
166 5
        $foreignTableTca = $GLOBALS['TCA'][$foreignTableName];
167 5
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
168 5
        $mmTableName = $localFieldTca['config']['MM'];
169
170
        // Remove the first option of foreignLabelField for recursion
171 5
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
172
            $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
173
            unset($foreignTableLabelFieldArr[0]);
174
            $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
175
        }
176
177 5
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
178 5
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
179 5
        $selectUids = $relationHandler->tableArray[$foreignTableName];
180 5
        if (!is_array($selectUids) || count($selectUids) <= 0) {
181 2
            return $relatedItems;
182
        }
183
184 5
        $pageSelector = GeneralUtility::makeInstance(PageRepository::class);
185 5
        $whereClause = $pageSelector->enableFields($foreignTableName);
186 5
        $relatedRecords = $this->getRelatedRecords($foreignTableName, $selectUids, $whereClause);
187 5
        foreach ($relatedRecords as $record) {
188 5
            if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
189 5
                && $this->configuration['enableRecursiveValueResolution']
190
            ) {
191
                if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
192
                    $foreignLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
193
                    unset($foreignLabelFieldArr[0]);
194
                    $this->configuration['foreignLabelField'] = implode('.', $foreignLabelFieldArr);
195
                }
196
197
                $this->configuration['localField'] = $foreignTableLabelField;
198
199
                $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
200
                $contentObject->start($record, $foreignTableName);
201
202
                return $this->getRelatedItems($contentObject);
203
            } else {
204 5
                if ($GLOBALS['TSFE']->sys_language_uid > 0) {
205 3
                    $record = $this->getTranslationOverlay($foreignTableName, $record);
206
                }
207 5
                $relatedItems[] = $record[$foreignTableLabelField];
208
            }
209
        }
210
211 5
        return $relatedItems;
212
    }
213
214
    /**
215
     * Resolves the field to use as the related item's label depending on TCA
216
     * and TypoScript configuration
217
     *
218
     * @param array $foreignTableTca The foreign table's TCA
219
     * @return string The field to use for the related item's label
220
     */
221 7
    protected function resolveForeignTableLabelField(array $foreignTableTca)
222
    {
223 7
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'];
224
225
        // when foreignLabelField is not enabled we can return directly
226 7
        if (empty($this->configuration['foreignLabelField'])) {
227 6
            return $foreignTableLabelField;
228
        }
229
230 2
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
231 1
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
232
        } else {
233 2
            $foreignTableLabelField = $this->configuration['foreignLabelField'];
234
        }
235
236 2
        return $foreignTableLabelField;
237
    }
238
239
    /**
240
     * Return the translated record
241
     *
242
     * @param string $tableName
243
     * @param array $record
244
     * @return array
245
     */
246 3
    protected function getTranslationOverlay($tableName, $record)
247
    {
248 3
        if ($tableName === 'pages') {
249 2
            return $GLOBALS['TSFE']->sys_page->getPageOverlay($record, $GLOBALS['TSFE']->sys_language_uid);
250
        }
251
252 2
        return $GLOBALS['TSFE']->sys_page->getRecordOverlay($tableName, $record, $GLOBALS['TSFE']->sys_language_uid);
253
    }
254
255
    /**
256
     * Gets the related items from a table using a 1:n relation.
257
     *
258
     * @param string $localTableName Local table name
259
     * @param int $localRecordUid Local record uid
260
     * @param array $localFieldTca The local table's TCA
261
     * @param ContentObjectRenderer $parentContentObject parent content object
262
     * @return array Array of related items, values already resolved from related records
263
     */
264 2
    protected function getRelatedItemsFromForeignTable(
265
        $localTableName,
266
        $localRecordUid,
267
        array $localFieldTca,
268
        ContentObjectRenderer $parentContentObject
269
    ) {
270 2
        $relatedItems = [];
271 2
        $foreignTableName = $localFieldTca['config']['foreign_table'];
272 2
        $foreignTableTca = $GLOBALS['TCA'][$foreignTableName];
273 2
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
274
275
            /** @var $relationHandler RelationHandler */
276 2
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
277
278 2
        $itemList = isset($parentContentObject->data[$this->configuration['localField']]) ?
279 2
                        $parentContentObject->data[$this->configuration['localField']] : '';
280
281 2
        $relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
282 2
        $selectUids = $relationHandler->tableArray[$foreignTableName];
283
284 2
        if (!is_array($selectUids) || count($selectUids) <= 0) {
285
            return $relatedItems;
286
        }
287
288 2
        $pageSelector = GeneralUtility::makeInstance(PageRepository::class);
289 2
        $whereClause  = $pageSelector->enableFields($foreignTableName);
290 2
        $relatedRecords = $this->getRelatedRecords($foreignTableName, $selectUids, $whereClause);
291
292 2
        foreach ($relatedRecords as $relatedRecord) {
293 2
            $resolveRelatedValue = $this->resolveRelatedValue(
294
                $relatedRecord,
295
                $foreignTableTca,
296
                $foreignTableLabelField,
297
                $parentContentObject,
298
                $foreignTableName
299
            );
300 2
            if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
301 2
                $relatedItems[] = $resolveRelatedValue;
302
            }
303
        }
304
305 2
        return $relatedItems;
306
    }
307
308
    /**
309
     * Resolves the value of the related field. If the related field's value is
310
     * a relation itself, this method takes care of resolving it recursively.
311
     *
312
     * @param array $relatedRecord Related record as array
313
     * @param array $foreignTableTca TCA of the related table
314
     * @param string $foreignTableLabelField Field name of the foreign label field
315
     * @param ContentObjectRenderer $parentContentObject cObject
316
     * @param string $foreignTableName Related record table name
317
     *
318
     * @return string
319
     */
320 2
    protected function resolveRelatedValue(
321
        array $relatedRecord,
322
        $foreignTableTca,
323
        $foreignTableLabelField,
324
        ContentObjectRenderer $parentContentObject,
325
        $foreignTableName = ''
326
    ) {
327 2
        if ($GLOBALS['TSFE']->sys_language_uid > 0 && !empty($foreignTableName)) {
328
            $relatedRecord = $this->getTranslationOverlay($foreignTableName, $relatedRecord);
329
        }
330
331 2
        $value = $relatedRecord[$foreignTableLabelField];
332
333
        if (
334 2
            !empty($foreignTableName)
335 2
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
336 2
            && $this->configuration['enableRecursiveValueResolution']
337
        ) {
338
            // backup
339 1
            $backupRecord = $parentContentObject->data;
340 1
            $backupConfiguration = $this->configuration;
341
342
            // adjust configuration for next level
343 1
            $this->configuration['localField'] = $foreignTableLabelField;
344 1
            $parentContentObject->data = $relatedRecord;
345 1
            if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
346 1
                list(, $this->configuration['foreignLabelField']) = explode('.',
347 1
                    $this->configuration['foreignLabelField'], 2);
348
            } else {
349 1
                $this->configuration['foreignLabelField'] = '';
350
            }
351
352
            // recursion
353 1
            $relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
354
                $foreignTableName,
355 1
                $relatedRecord['uid'],
356 1
                $foreignTableTca['columns'][$foreignTableLabelField],
357
                $parentContentObject
358
            );
359 1
            $value = array_pop($relatedItemsFromForeignTable);
360
361
            // restore
362 1
            $this->configuration = $backupConfiguration;
363 1
            $parentContentObject->data = $backupRecord;
364
        }
365
366 2
        return $parentContentObject->stdWrap($value, $this->configuration);
367
    }
368
369
    /**
370
     * When the record has an overlay we retrieve the uid of the translated record,
371
     * to resolve the relations from the translation.
372
     *
373
     * @param string $localTableName
374
     * @param int $localRecordUid
375
     * @return int
376
     */
377 7
    protected function getUidOfRecordOverlay($localTableName, $localRecordUid)
378
    {
379
        // when no language is set at all we do not need to overlay
380 7
        if (!isset($GLOBALS['TSFE']->sys_language_uid)) {
381
            return $localRecordUid;
382
        }
383
        // when no language is set we can return the passed recordUid
384 7
        if (!$GLOBALS['TSFE']->sys_language_uid > 0) {
385 7
            return $localRecordUid;
386
        }
387
        /** @var  $db  \TYPO3\CMS\Core\Database\DatabaseConnection */
388 3
        $db = $GLOBALS['TYPO3_DB'];
389 3
        $record = $db->exec_SELECTgetSingleRow('*', $localTableName, 'uid = ' . $localRecordUid);
390
391
        // when the overlay is not an array, we return the localRecordUid
392 3
        if (!is_array($record)) {
393
            return $localRecordUid;
394
        }
395
396 3
        $overlayUid = $this->getLocalRecordUidFromOverlay($localTableName, $record);
397 3
        $localRecordUid = ($overlayUid !== 0) ? $overlayUid : $localRecordUid;
398 3
        return $localRecordUid;
399
    }
400
401
    /**
402
     * This method retrieves the _PAGES_OVERLAY_UID or _LOCALIZED_UID from the localized record.
403
     *
404
     * @param string $localTableName
405
     * @param array $overlayRecord
406
     * @return int
407
     */
408 3
    protected function getLocalRecordUidFromOverlay($localTableName, $overlayRecord)
409
    {
410 3
        $overlayRecord = $this->getTranslationOverlay($localTableName, $overlayRecord);
411
412
        // when there is a _PAGES_OVERLAY_UID | _LOCALIZED_UID in the overlay, we return it
413 3
        if ($localTableName === 'pages' && isset($overlayRecord['_PAGES_OVERLAY_UID'])) {
414 1
            return (int)$overlayRecord['_PAGES_OVERLAY_UID'];
415 2
        } elseif (isset($overlayRecord['_LOCALIZED_UID'])) {
416 2
            return (int)$overlayRecord['_LOCALIZED_UID'];
417
        }
418
419
        return 0;
420
    }
421
422
    /**
423
     * Return records via relation.
424
     *
425
     * @param string $foreignTable The table to fetch records from.
426
     * @param array $uids The uids to fetch from table.
427
     * @param string $whereClause The where clause to append.
428
     *
429
     * @return array
430
     */
431 7
    protected function getRelatedRecords($foreignTable, array $uids, $whereClause)
432
    {
433 7
        if (isset($this->configuration['additionalWhereClause'])) {
434 2
            $whereClause .= ' AND ' . $this->configuration['additionalWhereClause'];
435
        }
436
437 7
        return $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
438 7
            '*',
439
            $foreignTable,
440 7
            'uid IN (' . implode(',', $uids) . ')'
441 7
            . $whereClause,
442 7
            '',
443 7
            'FIELD(uid, ' . implode(',', $uids) . ')'
444
        );
445
    }
446
}
447