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
Bug
introduced
by
![]() |
|||
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 |