Failed Conditions
Push — master ( 9606fb...a052b2 )
by Rafael
04:21
created

Relation::sortByKeyInIN()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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