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