Issues (216)

Classes/ContentObject/Relation.php (1 issue)

Labels
Severity
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\ContentObjectRenderer;
36
37
/**
38
 * A content object (cObj) to resolve relations between database records
39
 *
40
 * Configuration options:
41
 *
42
 * localField: the record's field to use to resolve relations
43
 * 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
44
 * multiValue: whether to return related records suitable for a multi value field
45
 * 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.
46
 * relationTableSortingField: field in an mm relation table to sort by, usually "sorting"
47
 * 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.
48
 * removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
49
 * removeDuplicateValues: Removes duplicate values
50
 *
51
 * @author Ingo Renner <[email protected]>
52
 */
53
class Relation
54
{
55
    const CONTENT_OBJECT_NAME = 'SOLR_RELATION';
56
57
    /**
58
     * Content object configuration
59
     *
60
     * @var array
61
     */
62
    protected $configuration = [];
63
64
    /**
65
     * @var TCAService
66
     */
67
    protected $tcaService = null;
68
69
    /**
70
     * @var FrontendOverlayService
71
     */
72
    protected $frontendOverlayService = null;
73
74
    /**
75
     * Relation constructor.
76
     * @param TCAService|null $tcaService
77
     * @param FrontendOverlayService|null $frontendOverlayService
78 47
     */
79
    public function __construct(TCAService $tcaService = null, FrontendOverlayService $frontendOverlayService = null)
80 47
    {
81 47
        $this->configuration['enableRecursiveValueResolution'] = 1;
82 47
        $this->configuration['removeEmptyValues'] = 1;
83 47
        $this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
84 47
        $this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class);
85
    }
86
87
    /**
88
     * Executes the SOLR_RELATION content object.
89
     *
90
     * Resolves relations between records. Currently supported relations are
91
     * TYPO3-style m:n relations.
92
     * May resolve single value and multi value relations.
93
     *
94
     * @param string $name content object name 'SOLR_RELATION'
95
     * @param array $configuration for the content object
96
     * @param string $TyposcriptKey not used
97
     * @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $parentContentObject parent content object
98
     * @return string serialized array representation of the given list
99 47
     */
100
    public function cObjGetSingleExt(
101
        /** @noinspection PhpUnusedParameterInspection */ $name,
102
        array $configuration,
103
        /** @noinspection PhpUnusedParameterInspection */ $TyposcriptKey,
104
        $parentContentObject
105 47
    ) {
106
        $this->configuration = array_merge($this->configuration, $configuration);
107 47
108
        $relatedItems = $this->getRelatedItems($parentContentObject);
109 47
110
        if (!empty($this->configuration['removeDuplicateValues'])) {
111
            $relatedItems = array_unique($relatedItems);
112
        }
113 47
114
        if (empty($configuration['multiValue'])) {
115 2
            // single value, need to concatenate related items
116 2
            $singleValueGlue = !empty($configuration['singleValueGlue']) ? trim($configuration['singleValueGlue'], '|') : ', ';
117
            $result = implode($singleValueGlue, $relatedItems);
118
        } else {
119 45
            // multi value, need to serialize as content objects must return strings
120
            $result = serialize($relatedItems);
121
        }
122 47
123
        return $result;
124
    }
125
126
    /**
127
     * Gets the related items of the current record's configured field.
128
     *
129
     * @param ContentObjectRenderer $parentContentObject parent content object
130
     * @return array Array of related items, values already resolved from related records
131 47
     */
132
    protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
133 47
    {
134 47
        list($table, $uid) = explode(':', $parentContentObject->currentRecord);
135 47
        $uid = (int) $uid;
136
        $field = $this->configuration['localField'];
137 47
138
        if (!$this->tcaService->getHasConfigurationForField($table, $field)) {
139
            return [];
140
        }
141
142
        $overlayUid = $this->frontendOverlayService->getUidOfOverlay($table, $field, $uid);
143
        $fieldTCA = $this->tcaService->getConfigurationForField($table, $field);
144
145 47
        if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
146 47
            $relatedItems = $this->getRelatedItemsFromMMTable($table, $overlayUid, $fieldTCA);
147 47
        } else {
148
            $relatedItems = $this->getRelatedItemsFromForeignTable($table, $overlayUid, $fieldTCA, $parentContentObject);
149 47
        }
150 44
151
        return $relatedItems;
152 3
    }
153
154
    /**
155 47
     * Gets the related items from a table using a n:m relation.
156
     *
157
     * @param string $localTableName Local table name
158
     * @param int $localRecordUid Local record uid
159
     * @param array $localFieldTca The local table's TCA
160
     * @return array Array of related items, values already resolved from related records
161
     */
162
    protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid, array $localFieldTca)
163
    {
164
        $relatedItems = [];
165
        $foreignTableName = $localFieldTca['config']['foreign_table'];
166 44
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
167
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
168 44
        $mmTableName = $localFieldTca['config']['MM'];
169 44
170 44
        // Remove the first option of foreignLabelField for recursion
171 44
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
172 44
            $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
173
            unset($foreignTableLabelFieldArr[0]);
174
            $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
175 44
        }
176
177
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
178
        $relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
179
        $selectUids = $relationHandler->tableArray[$foreignTableName];
180
        if (!is_array($selectUids) || count($selectUids) <= 0) {
181 44
            return $relatedItems;
182 44
        }
183 44
184 44
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
185 36
        foreach ($relatedRecords as $record) {
186
            if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
187
                && $this->configuration['enableRecursiveValueResolution']
188 10
            ) {
189 10
                if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
190 10
                    $foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
191 10
                    unset($foreignTableLabelFieldArr[0]);
192
                    $this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
193
                }
194
195
                $this->configuration['localField'] = $foreignTableLabelField;
196
197
                $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
198
                $contentObject->start($record, $foreignTableName);
199
200
                return $this->getRelatedItems($contentObject);
201
            } else {
202
                if (Util::getLanguageUid() > 0) {
203
                    $record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
204
                }
205
                $relatedItems[] = $record[$foreignTableLabelField];
206 10
            }
207 5
        }
208
209 10
        return $relatedItems;
210
    }
211
212
    /**
213 10
     * Resolves the field to use as the related item's label depending on TCA
214
     * and TypoScript configuration
215
     *
216
     * @param array $foreignTableTca The foreign table's TCA
217
     * @return string The field to use for the related item's label
218
     */
219
    protected function resolveForeignTableLabelField(array $foreignTableTca)
220
    {
221
        $foreignTableLabelField = $foreignTableTca['ctrl']['label'];
222
223 47
        // when foreignLabelField is not enabled we can return directly
224
        if (empty($this->configuration['foreignLabelField'])) {
225 47
            return $foreignTableLabelField;
226
        }
227
228 47
        if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
229 11
            list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
230
        } else {
231
            $foreignTableLabelField = $this->configuration['foreignLabelField'];
232 38
        }
233 2
234
        return $foreignTableLabelField;
235 38
    }
236
237
    /**
238 38
     * Gets the related items from a table using a 1:n relation.
239
     *
240
     * @param string $localTableName Local table name
241
     * @param int $localRecordUid Local record uid
242
     * @param array $localFieldTca The local table's TCA
243
     * @param ContentObjectRenderer $parentContentObject parent content object
244
     * @return array Array of related items, values already resolved from related records
245
     */
246
    protected function getRelatedItemsFromForeignTable(
247
        $localTableName,
248
        $localRecordUid,
249
        array $localFieldTca,
250 3
        ContentObjectRenderer $parentContentObject
251
    ) {
252
        $relatedItems = [];
253
        $foreignTableName = $localFieldTca['config']['foreign_table'];
254
        $foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
255
        $foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
256 3
257 3
            /** @var $relationHandler RelationHandler */
258 3
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
259 3
260
        $itemList = $parentContentObject->data[$this->configuration['localField']] ?? '';
261
262 3
        $relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
263
        $selectUids = $relationHandler->tableArray[$foreignTableName];
264 3
265
        if (!is_array($selectUids) || count($selectUids) <= 0) {
266 3
            return $relatedItems;
267 3
        }
268
269 3
        $relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
270
271
        foreach ($relatedRecords as $relatedRecord) {
272
            $resolveRelatedValue = $this->resolveRelatedValue(
273 3
                $relatedRecord,
274
                $foreignTableTca,
275 3
                $foreignTableLabelField,
276 3
                $parentContentObject,
277 3
                $foreignTableName
278 3
            );
279 3
            if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
280 3
                $relatedItems[] = $resolveRelatedValue;
281 3
            }
282
        }
283 3
284 3
        return $relatedItems;
285
    }
286
287
    /**
288 3
     * Resolves the value of the related field. If the related field's value is
289
     * a relation itself, this method takes care of resolving it recursively.
290
     *
291
     * @param array $relatedRecord Related record as array
292
     * @param array $foreignTableTca TCA of the related table
293
     * @param string $foreignTableLabelField Field name of the foreign label field
294
     * @param ContentObjectRenderer $parentContentObject cObject
295
     * @param string $foreignTableName Related record table name
296
     *
297
     * @return string
298
     */
299
    protected function resolveRelatedValue(
300
        array $relatedRecord,
301
        $foreignTableTca,
302
        $foreignTableLabelField,
303 3
        ContentObjectRenderer $parentContentObject,
304
        $foreignTableName = ''
305
    ) {
306
        if (Util::getLanguageUid() > 0 && !empty($foreignTableName)) {
307
            $relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
308
        }
309
310 3
        $value = $relatedRecord[$foreignTableLabelField];
311
312
        if (
313
            !empty($foreignTableName)
314 3
            && isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
315
            && $this->configuration['enableRecursiveValueResolution']
316
        ) {
317 3
            // backup
318 3
            $backupRecord = $parentContentObject->data;
319 3
            $backupConfiguration = $this->configuration;
320
321
            // adjust configuration for next level
322 2
            $this->configuration['localField'] = $foreignTableLabelField;
323 2
            $parentContentObject->data = $relatedRecord;
324
            if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
325
                list(, $this->configuration['foreignLabelField']) = explode('.',
326 2
                    $this->configuration['foreignLabelField'], 2);
327 2
            } else {
328 2
                $this->configuration['foreignLabelField'] = '';
329 2
            }
330 2
331
            // recursion
332 2
            $relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
333
                $foreignTableName,
334
                $relatedRecord['uid'],
335
                $foreignTableTca['columns'][$foreignTableLabelField],
336 2
                $parentContentObject
337 2
            );
338 2
            $value = array_pop($relatedItemsFromForeignTable);
339 2
340 2
            // restore
341
            $this->configuration = $backupConfiguration;
342 2
            $parentContentObject->data = $backupRecord;
343
        }
344
345 2
        return $parentContentObject->stdWrap($value, $this->configuration);
346 2
    }
347
348
    /**
349 3
     * Return records via relation.
350
     *
351
     * @param string $foreignTable The table to fetch records from.
352
     * @param int[] ...$uids The uids to fetch from table.
353
     * @return array
354
     */
355
    protected function getRelatedRecords($foreignTable, int ...$uids): array
356
    {
357
        /** @var QueryBuilder $queryBuilder */
358
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
359 13
        $queryBuilder->select('*')
360
            ->from($foreignTable)
361
            ->where($queryBuilder->expr()->in('uid', $uids));
362 13
        if (isset($this->configuration['additionalWhereClause'])) {
363 13
            $queryBuilder->andWhere($this->configuration['additionalWhereClause']);
364 13
        }
365 13
        $statement = $queryBuilder->execute();
366 13
367 2
        return $this->sortByKeyInIN($statement, 'uid', ...$uids);
0 ignored issues
show
It seems like $statement can also be of type integer; however, parameter $statement of ApacheSolrForTypo3\Solr\...lation::sortByKeyInIN() does only seem to accept Doctrine\DBAL\Driver\Statement, maybe add an additional type check? ( Ignorable by Annotation )

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

367
        return $this->sortByKeyInIN(/** @scrutinizer ignore-type */ $statement, 'uid', ...$uids);
Loading history...
368
    }
369 13
370
    /**
371 13
     * Sorts the result set by key in array for IN values.
372
     *   Simulates MySqls ORDER BY FIELD(fieldname, COPY_OF_IN_FOR_WHERE)
373
     *   Example: SELECT * FROM a_table WHERE field_name IN (2, 3, 4) SORT BY FIELD(field_name, 2, 3, 4)
374
     *
375
     *
376
     * @param Statement $statement
377
     * @param string $columnName
378
     * @param array $arrayWithValuesForIN
379
     * @return array
380
     */
381
    protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN) : array
382
    {
383
        $records = [];
384
        while ($record = $statement->fetch()) {
385 13
            $indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
386
            $records[$indexNumber] = $record;
387 13
        }
388 13
        ksort($records);
389 13
        return $records;
390 13
    }
391
}
392