‪TYPO3CMS  ‪main
ReferenceIndex.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Log\LogLevel;
22 use TYPO3\CMS\Backend\Utility\BackendUtility;
33 
40 {
42  private const ‪HASH_VERSION = 1;
43 
51  private array ‪$excludedTables = [];
52 
57  private array ‪$tableRelationFieldCache = [];
58 
59  public function ‪__construct(
60  private readonly EventDispatcherInterface $eventDispatcher,
61  private readonly ‪SoftReferenceParserFactory $softReferenceParserFactory,
62  private readonly ‪ConnectionPool $connectionPool,
63  private readonly ‪Registry $registry,
64  private readonly ‪FlexFormTools $flexFormTools,
65  ) {}
66 
70  public function ‪getNumberOfReferencedRecords(string $tableName, int ‪$uid): int
71  {
72  ‪$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
73  return (int)‪$queryBuilder
74  ->count('*')->from('sys_refindex')
75  ->where(
76  ‪$queryBuilder->expr()->eq(
77  'ref_table',
78  ‪$queryBuilder->createNamedParameter($tableName)
79  ),
80  ‪$queryBuilder->expr()->eq(
81  'ref_uid',
82  ‪$queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
83  )
84  )->executeQuery()->fetchOne();
85  }
86 
94  public function ‪updateIndex(bool $testOnly, ?‪ProgressListenerInterface $progressListener = null): array
95  {
96  ‪$errors = [];
97  $numberOfHandledRecords = 0;
98 
99  $isWorkspacesLoaded = ‪ExtensionManagementUtility::isLoaded('workspaces');
100  $tcaTableNames = array_keys(‪$GLOBALS['TCA']);
101  sort($tcaTableNames);
102 
103  $progressListener?->log('Remember to create missing tables and columns before running this.', LogLevel::WARNING);
104 
105  // Remove dangling workspace sys_refindex rows
106  $listOfActiveWorkspaces = $this->‪getListOfActiveWorkspaces();
107  $numberOfUnusedWorkspaceRows = $testOnly
108  ? $this->‪getNumberOfUnusedWorkspaceRowsInReferenceIndex($listOfActiveWorkspaces)
109  : $this->‪removeUnusedWorkspaceRowsFromReferenceIndex($listOfActiveWorkspaces);
110  if ($numberOfUnusedWorkspaceRows > 0) {
111  $error = 'Index table hosted ' . $numberOfUnusedWorkspaceRows . ' indexes for non-existing or deleted workspaces, now removed.';
112  ‪$errors[] = $error;
113  $progressListener?->log($error, LogLevel::WARNING);
114  }
115 
116  // Remove sys_refindex rows of tables no longer defined in TCA
117  $numberOfRowsOfOldTables = $testOnly
118  ? $this->‪getNumberOfUnusedTablesInReferenceIndex($tcaTableNames)
120  if ($numberOfRowsOfOldTables > 0) {
121  $error = 'Index table hosted ' . $numberOfRowsOfOldTables . ' indexes for non-existing tables, now removed';
122  ‪$errors[] = $error;
123  $progressListener?->log($error, LogLevel::WARNING);
124  }
125 
126  // Main loop traverses all records of all TCA tables
127  foreach ($tcaTableNames as $tableName) {
128  $tableTca = ‪$GLOBALS['TCA'][$tableName];
129 
130  // Count number of records in table to have a correct $numberOfHandledRecords in the end
131  ‪$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
132  ‪$queryBuilder->getRestrictions()->removeAll();
133  $numberOfRecordsInTargetTable = ‪$queryBuilder
134  ->count('uid')
135  ->from($tableName)
136  ->executeQuery()
137  ->fetchOne();
138 
139  $progressListener?->start($numberOfRecordsInTargetTable, $tableName);
140 
141  if ($numberOfRecordsInTargetTable === 0 || $this->‪shouldExcludeTableFromReferenceIndex($tableName) || empty($this->‪getTableRelationFields($tableName))) {
142  // Table is empty, should be excluded, or can not have relations. Blindly remove any existing sys_refindex rows.
143  $numberOfHandledRecords += $numberOfRecordsInTargetTable;
144  ‪$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
145  ‪$queryBuilder->getRestrictions()->removeAll();
146  if ($testOnly) {
147  $countDeleted = ‪$queryBuilder
148  ->count('hash')
149  ->from('sys_refindex')
150  ->where(
151  ‪$queryBuilder->expr()->eq('tablename', ‪$queryBuilder->createNamedParameter($tableName))
152  )
153  ->executeQuery()
154  ->fetchOne();
155  } else {
156  $countDeleted = ‪$queryBuilder
157  ->delete('sys_refindex')
158  ->where(
159  ‪$queryBuilder->expr()->eq('tablename', ‪$queryBuilder->createNamedParameter($tableName))
160  )
161  ->executeStatement();
162  }
163  if ($countDeleted > 0) {
164  $error = 'Index table hosted ' . $countDeleted . ' ignored or outdated indexed, now removed.';
165  ‪$errors[] = $error;
166  $progressListener?->log($error, LogLevel::WARNING);
167  }
168  $progressListener?->finish();
169  continue;
170  }
171 
172  // Delete lost indexes of table: sys_refindex rows where the uid no longer exists in target table.
173  $subQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
174  $subQueryBuilder->getRestrictions()->removeAll();
175  $subQueryBuilder
176  ->select('uid')
177  ->from($tableName, 'sub_' . $tableName)
178  ->where(
179  $subQueryBuilder->expr()->eq('sub_' . $tableName . '.uid', $subQueryBuilder->quoteIdentifier('sys_refindex.recuid'))
180  );
181  ‪$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
182  ‪$queryBuilder->getRestrictions()->removeAll();
183  if ($testOnly) {
184  $numberOfRefindexRowsWithoutExistingTableRow = ‪$queryBuilder
185  ->count('hash')
186  ->from('sys_refindex')
187  ->where(
188  ‪$queryBuilder->expr()->eq('tablename', ‪$queryBuilder->createNamedParameter($tableName)),
189  'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')'
190  )
191  ->executeQuery()
192  ->fetchOne();
193  } else {
194  $numberOfRefindexRowsWithoutExistingTableRow = ‪$queryBuilder
195  ->delete('sys_refindex')
196  ->where(
197  ‪$queryBuilder->expr()->eq('tablename', ‪$queryBuilder->createNamedParameter($tableName)),
198  'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')'
199  )
200  ->executeStatement();
201  }
202  if ($numberOfRefindexRowsWithoutExistingTableRow > 0) {
203  $error = 'Table ' . $tableName . ' hosted ' . $numberOfRefindexRowsWithoutExistingTableRow . ' lost indexes, now removed.';
204  ‪$errors[] = $error;
205  $progressListener?->log($error, LogLevel::WARNING);
206  }
207 
208  // Delete rows in sys_refindex related to this table where the record is soft-deleted=1.
209  if (!empty($tableTca['ctrl']['delete'])) {
210  ‪$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
211  ‪$queryBuilder->getRestrictions()->removeAll();
212  $numberOfDeletedRecordsInTargetTable = ‪$queryBuilder
213  ->count('uid')
214  ->from($tableName)
215  ->where(‪$queryBuilder->expr()->eq($tableTca['ctrl']['delete'], 1))
216  ->executeQuery()
217  ->fetchOne();
218  if ($numberOfDeletedRecordsInTargetTable > 0) {
219  $numberOfHandledRecords += $numberOfDeletedRecordsInTargetTable;
220  // List of deleted=0 records in target table that have records in sys_refindex.
221  if ($testOnly) {
222  ‪$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
223  ‪$queryBuilder->getRestrictions()->removeAll();
224  // $subQueryBuilder actually fills parameter placeholders for the main $queryBuilder.
225  // The subQuery is never meant to be executed on its own, only used to be filled-in
226  // via $subQueryBuilder->getSQL().
227  $subQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
228  $subQueryBuilder->getRestrictions()->removeAll();
229  $subQueryBuilder
230  ->select('sub_' . $tableName . '.uid')
231  ->distinct()
232  ->from($tableName, 'sub_' . $tableName)
233  ->join(
234  'sub_' . $tableName,
235  'sys_refindex',
236  'sub_refindex',
237  ‪$queryBuilder->expr()->eq('sub_refindex.recuid', ‪$queryBuilder->quoteIdentifier('sub_' . $tableName . '.uid'))
238  )
239  ->where(
240  ‪$queryBuilder->expr()->eq('sub_refindex.tablename', ‪$queryBuilder->createNamedParameter($tableName)),
241  ‪$queryBuilder->expr()->eq('sub_' . $tableName . '.' . $tableTca['ctrl']['delete'], 1),
242  );
243  $numberOfRemovedIndexes = ‪$queryBuilder
244  ->count('hash')
245  ->from('sys_refindex')
246  ->where(
247  ‪$queryBuilder->expr()->eq('tablename', ‪$queryBuilder->createNamedParameter($tableName)),
248  ‪$queryBuilder->quoteIdentifier('recuid') . ' IN ( ' . $subQueryBuilder->getSQL() . ' )'
249  )
250  ->executeQuery()
251  ->fetchOne();
252  } else {
253  // MySQL is picky when using the same table in a sub-query and an outer delete query, if
254  // it is not materialized into a temporary table. Enforcing a temporary table would mitigate this
255  // MySQL limit, but we simply fetch the affected uid list instead and fire a chunked delete query.
256  // In contrast to $testOnly above, we execute the subQuery, named parameter placeholders need
257  // to be relative to its QueryBuilder.
258  $uidListQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
259  $uidListQueryBuilder->getRestrictions()->removeAll();
260  $uidListQueryBuilder
261  ->select('sub_' . $tableName . '.uid')
262  ->distinct()
263  ->from($tableName, 'sub_' . $tableName)
264  ->join(
265  'sub_' . $tableName,
266  'sys_refindex',
267  'sub_refindex',
268  $uidListQueryBuilder->expr()->eq('sub_refindex.recuid', $uidListQueryBuilder->quoteIdentifier('sub_' . $tableName . '.uid'))
269  )
270  ->where(
271  $uidListQueryBuilder->expr()->eq('sub_refindex.tablename', $uidListQueryBuilder->createNamedParameter($tableName)),
272  $uidListQueryBuilder->expr()->eq('sub_' . $tableName . '.' . $tableTca['ctrl']['delete'], 1),
273  );
274  $uidListOfRemovableIndexes = $uidListQueryBuilder->executeQuery()->fetchFirstColumn();
275  $numberOfRemovedIndexes = 0;
276  // Another variant to solve this would be a limit/offset query for the upper query, feeding delete.
277  // This would be more memory efficient. We however think there shouldn't be *that* many affected
278  // rows to delete in casual scenarios, so we skip that optimization for now since chunking isn't
279  // needed in most cases anyway.
280  // 10k is an arbitrary number. Reasoning: 1MB max query length with 10-char uids (9mio uid-range with comma)
281  // would allow ~10k uids. Combi tablename/recuid is indexed, so delete should be relatively quick even with
282  // larger sets, so delete-hard-locking on for instance innodb shouldn't be a huge issue here.
283  foreach (array_chunk($uidListOfRemovableIndexes, 10000) as $uidChunkOfRemovableIndexes) {
284  $chunkQueryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
285  $chunkQueryBuilder->getRestrictions()->removeAll();
286  $chunkedNumberOfRemovedIndexes = $chunkQueryBuilder
287  ->delete('sys_refindex')
288  ->where(
289  $chunkQueryBuilder->expr()->eq('tablename', $chunkQueryBuilder->createNamedParameter($tableName)),
290  $chunkQueryBuilder->expr()->in('recuid', $uidChunkOfRemovableIndexes)
291  )
292  ->executeStatement();
293  $numberOfRemovedIndexes += $chunkedNumberOfRemovedIndexes;
294  }
295  }
296  if ($numberOfRemovedIndexes > 0) {
297  $error = 'Table ' . $tableName . ' hosted ' . $numberOfRemovedIndexes . ' indexes from soft-deleted records, now removed.';
298  ‪$errors[] = $error;
299  $progressListener?->log($error, LogLevel::WARNING);
300  }
301  $progressListener?->advance($numberOfDeletedRecordsInTargetTable);
302  }
303  }
304 
305  // Some additional magic is needed if the table has a field that is the local side of
306  // a mm relation. See the variable usage below for details.
307  $tableHasLocalSideMmRelation = false;
308  foreach (($tableTca['columns'] ?? []) as $fieldConfig) {
309  if (!empty($fieldConfig['config']['MM'] ?? '')
310  && !empty($fieldConfig['config']['allowed'] ?? '')
311  && empty($fieldConfig['config']['MM_opposite_field'] ?? '')
312  ) {
313  $tableHasLocalSideMmRelation = true;
314  }
315  }
316 
317  // Traverse all records in table, not including soft-deleted records
318  ‪$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
319  ‪$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
321  ->select('*')
322  ->from($tableName)
323  ->orderBy('uid')
324  ->executeQuery();
325  while (‪$record = ‪$queryResult->fetchAssociative()) {
326  $progressListener?->advance();
327  if ($isWorkspacesLoaded && $tableHasLocalSideMmRelation && (int)(‪$record['t3ver_wsid'] ?? 0) === 0) {
328  // If we have a record that can be the local side of a workspace relation, workspace records
329  // may point to it, even though the record has no workspace overlay. See workspace ManyToMany
330  // Modify addCategoryRelation as example. In those cases, we need to iterate all active workspaces
331  // and update refindex for all foreign workspace records that point to it.
332  foreach ($listOfActiveWorkspaces as $workspaceId) {
333  $result = $this->‪updateRefIndexTable($tableName, (int)‪$record['uid'], $testOnly, $workspaceId, ‪$record);
334  $numberOfHandledRecords++;
335  if ($result['addedNodes'] || $result['deletedNodes']) {
336  $error = 'Record ' . $tableName . ':' . ‪$record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
337  ‪$errors[] = $error;
338  $progressListener?->log($error, LogLevel::WARNING);
339  }
340  }
341  } else {
342  $result = $this->‪updateRefIndexTable($tableName, (int)‪$record['uid'], $testOnly, (int)(‪$record['t3ver_wsid'] ?? 0), ‪$record);
343  $numberOfHandledRecords++;
344  if ($result['addedNodes'] || $result['deletedNodes']) {
345  $error = 'Record ' . $tableName . ':' . ‪$record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
346  ‪$errors[] = $error;
347  $progressListener?->log($error, LogLevel::WARNING);
348  }
349  }
350  }
351  $progressListener?->finish();
352  }
353 
354  $errorCount = count(‪$errors);
355  $recordsCheckedString = $numberOfHandledRecords . ' records from ' . count($tcaTableNames) . ' tables were checked/updated.';
356  if ($errorCount) {
357  $progressListener?->log($recordsCheckedString . ' Updates: ' . $errorCount, LogLevel::WARNING);
358  } else {
359  $progressListener?->log($recordsCheckedString . ' Index Integrity was perfect!');
360  }
361  if (!$testOnly) {
362  $this->registry->set('core', 'sys_refindex_lastUpdate', ‪$GLOBALS['EXEC_TIME']);
363  }
364  return ['resultText' => trim($recordsCheckedString), 'errors' => ‪$errors];
365  }
366 
376  public function ‪updateRefIndexTable(string $tableName, int ‪$uid, bool $testOnly = false, int $workspaceUid = 0, array $currentRecord = null): array
377  {
378  $result = [
379  'keptNodes' => 0,
380  'deletedNodes' => 0,
381  'addedNodes' => 0,
382  ];
383  if ($uid < 1 || $this->‪shouldExcludeTableFromReferenceIndex($tableName) || empty($this->‪getTableRelationFields($tableName))) {
384  // Not a valid uid, the table is excluded, or can not contain relations.
385  return $result;
386  }
387  if ($currentRecord === null) {
388  // Fetch record if not provided.
389  $currentRecord = BackendUtility::getRecord($tableName, ‪$uid);
390  }
391  ‪$currentRelationHashes = $this->getCurrentRelationHashes($tableName, ‪$uid, $workspaceUid);
392  if ($currentRecord === null) {
393  // If there is no record because it was hard or soft-deleted, remove any existing sys_refindex rows of it.
394  $numberOfLeftOverRelationHashes = count(‪$currentRelationHashes);
395  $result['deletedNodes'] = $numberOfLeftOverRelationHashes;
396  if ($numberOfLeftOverRelationHashes > 0 && !$testOnly) {
398  }
399  return $result;
400  }
401 
402  $relations = $this->‪compileReferenceIndexRowsForRecord($tableName, $currentRecord, $workspaceUid);
403  $connection = $this->connectionPool->getConnectionForTable('sys_refindex');
404  foreach ($relations as &$relation) {
405  if (!is_array($relation)) {
406  continue;
407  }
408  // Exclude any relations TO a specific table
409  if (($relation['ref_table'] ?? '') && $this->‪shouldExcludeTableFromReferenceIndex($relation['ref_table'])) {
410  continue;
411  }
412  $relation['hash'] = md5(implode('
413  // First, check if already indexed and if so, unset that row (so in the end we know which rows to remove!)
414  if (isset($currentRelationHashes[$relation['hash']])) {
415  unset($currentRelationHashes[$relation['hash']]);
416  $result['keptNodes']++;
417  $relation['_ACTION'] = 'KEPT';
418  } else {
419  // If new, add it:
420  if (!$testOnly) {
421  $connection->insert('sys_refindex', $relation);
422  }
423  $result['addedNodes']++;
424  $relation['_ACTION'] = 'ADDED';
425  }
426  }
427 
428  // If any existing are left, they are not in the current set anymore. Remove them.
429  $numberOfLeftOverRelationHashes = count($currentRelationHashes);
430  $result['deletedNodes'] = $numberOfLeftOverRelationHashes;
431  if ($numberOfLeftOverRelationHashes > 0 && !$testOnly) {
432  $this->removeRelationHashes($currentRelationHashes);
433  }
434 
435  return $result;
436  }
437 
444  public function getRelations(string $tableName, array $record, int $workspaceUid): array
445  {
446  $result = [];
447  $relationFields = $this->getTableRelationFields($tableName);
448  foreach ($relationFields as $field) {
449  $value = $record[$field] ?? null;
450  if (is_array($GLOBALS['TCA'][$tableName]['columns'][$field] ?? false)) {
451  $conf = $GLOBALS['TCA'][$tableName]['columns'][$field]['config'];
452  // Add a softref definition for link fields if the TCA does not specify one already
453  if ($conf['type'] === 'link' && empty($conf['softref'])) {
454  $conf['softref'] = 'typolink';
455  }
456  // Add a softref definition for email fields
457  if ($conf['type'] === 'email') {
458  $conf['softref'] = 'email[subst]';
459  }
460  $resultsFromDatabase = $this->getRelationsFromRelationField($tableName, $value, $conf, (int)$record['uid'], $workspaceUid, $record);
461  if (!empty($resultsFromDatabase)) {
462  // Create an entry for the field with all DB relations:
463  $result[$field] = [
464  'type' => 'db',
465  'itemArray' => $resultsFromDatabase,
466  ];
467  }
468  if ($conf['type'] === 'flex' && is_string($value) && $value !== '') {
469  // Traverse the flex data structure looking for db references for flex fields.
470  $flexFormRelations = $this->getRelationsFromFlexData($tableName, $field, $record, $workspaceUid);
471  if (!empty($flexFormRelations)) {
472  $result[$field] = [
473  'type' => 'flex',
474  'flexFormRels' => $flexFormRelations,
475  ];
476  }
477  }
478  if ((string)$value !== '') {
479  // Soft References
480  $softRefValue = $value;
481  if (!empty($conf['softref'])) {
482  foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($conf['softref']) as $softReferenceParser) {
483  $parserResult = $softReferenceParser->parse($tableName, $field, (int)$record['uid'], $softRefValue);
484  if ($parserResult->hasMatched()) {
485  $result[$field]['softrefs']['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements();
486  if ($parserResult->hasContent()) {
487  $softRefValue = $parserResult->getContent();
488  }
489  }
490  }
491  }
492  if (!empty($result[$field]['softrefs']) && (string)$value !== (string)$softRefValue && str_contains($softRefValue, '{softref:')) {
493  $result[$field]['softrefs']['tokenizedContent'] = $softRefValue;
494  }
495  }
496  }
497  }
498  return $result;
499  }
500 
506  private function getCurrentRelationHashes(string $tableName, int $uid, int $workspaceUid): array
507  {
508  $connection = $this->connectionPool->getConnectionForTable('sys_refindex');
509  $queryBuilder = $connection->createQueryBuilder();
510  $queryBuilder->getRestrictions()->removeAll();
511  $queryResult = $queryBuilder->select('hash')->from('sys_refindex')->where(
512  $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)),
513  $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)),
514  $queryBuilder->expr()->eq('workspace', $queryBuilder->createNamedParameter($workspaceUid, Connection::PARAM_INT))
515  )->executeQuery();
516  $currentRelationHashes = [];
517  while ($relation = $queryResult->fetchAssociative()) {
518  $currentRelationHashes[$relation['hash']] = true;
519  }
520  return $currentRelationHashes;
521  }
522 
528  private function removeRelationHashes(array $currentRelationHashes): void
529  {
530  $connection = $this->connectionPool->getConnectionForTable('sys_refindex');
531  $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
532  $chunks = array_chunk(array_keys($currentRelationHashes), $maxBindParameters - 10, true);
533  foreach ($chunks as $chunk) {
534  $queryBuilder = $connection->createQueryBuilder();
535  $queryBuilder
536  ->delete('sys_refindex')
537  ->where(
538  $queryBuilder->expr()->in('hash', $queryBuilder->createNamedParameter($chunk, Connection::PARAM_STR_ARRAY))
539  )
540  ->executeStatement();
541  }
542  }
543 
544  private function compileReferenceIndexRowsForRecord(string $tableName, array $record, int $workspaceUid): array
545  {
546  $relations = [];
547  $recordRelations = $this->getRelations($tableName, $record, $workspaceUid);
548  foreach ($recordRelations as $fieldName => $fieldRelations) {
549  if (BackendUtility::isTableWorkspaceEnabled($tableName)) {
550  $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
551  if (isset($record['t3ver_wsid']) && (int)$record['t3ver_wsid'] !== $workspaceUid && empty($fieldConfig['MM'])) {
552  // The given record is workspace-enabled but doesn't live in the selected workspace. Don't add index, it's not actually there.
553  // We still add those rows if the record is a local side live record of an MM relation and can be a target of a workspace record.
554  // See workspaces ManyToMany Modify addCategoryRelation for details on this case.
555  continue;
556  }
557  }
558  if (is_array($fieldRelations['itemArray'] ?? false)) {
559  // DB relations in a db field
560  foreach ($fieldRelations['itemArray'] as $sorting => $item) {
561  $relations[] = [
562  'tablename' => $tableName,
563  'recuid' => ‪$record['uid'],
564  'field' => $fieldName,
565  'flexpointer' => '',
566  'softref_key' => '',
567  'softref_id' => '',
568  'sorting' => $sorting,
569  'workspace' => $workspaceUid,
570  'ref_table' => $item['table'],
571  'ref_uid' => (int)$item['id'],
572  'ref_string' => '',
573  ];
574  }
575  }
576  if (is_array($fieldRelations['softrefs']['keys'] ?? false)) {
577  // Soft reference relations in a db field
578  foreach ($fieldRelations['softrefs']['keys'] as $softrefKey => $elements) {
579  if (!is_array($elements)) {
580  continue;
581  }
582  foreach ($elements as $softrefId => $element) {
583  if (!in_array($element['subst']['type'] ?? '', ['db', 'string'], true)) {
584  continue;
585  }
586  $referencedTable = '_STRING';
587  $referencedUid = 0;
588  $referencedString = '';
589  if ($element['subst']['type'] === 'db') {
590  [$referencedTable, $referencedUid] = explode(':', $element['subst']['recordRef']);
591  } else {
592  $referencedString = mb_substr($element['subst']['tokenValue'], 0, 1024);
593  }
594  $relations[] = [
595  'tablename' => $tableName,
596  'recuid' => ‪$record['uid'],
597  'field' => $fieldName,
598  'flexpointer' => '',
599  'softref_key' => $softrefKey,
600  'softref_id' => (string)$softrefId,
601  'sorting' => -1,
602  'workspace' => $workspaceUid,
603  'ref_table' => $referencedTable,
604  'ref_uid' => (int)$referencedUid,
605  'ref_string' => $referencedString,
606  ];
607  }
608  }
609  }
610  if (is_array($fieldRelations['flexFormRels']['db'] ?? false)) {
611  // DB relations in a flex field
612  foreach ($fieldRelations['flexFormRels']['db'] as $flexPointer => $subList) {
613  foreach ($subList as $sorting => $item) {
614  $relations[] = [
615  'tablename' => $tableName,
616  'recuid' => ‪$record['uid'],
617  'field' => $fieldName,
618  'flexpointer' => $flexPointer,
619  'softref_key' => '',
620  'softref_id' => '',
621  'sorting' => $sorting,
622  'workspace' => $workspaceUid,
623  'ref_table' => $item['table'],
624  'ref_uid' => (int)$item['id'],
625  'ref_string' => '',
626  ];
627  }
628  }
629  }
630  if (is_array($fieldRelations['flexFormRels']['softrefs'] ?? false)) {
631  // Soft reference relations in a flex field
632  foreach ($fieldRelations['flexFormRels']['softrefs'] as $flexPointer => $subList) {
633  foreach ($subList['keys'] as $softrefKey => $elements) {
634  if (!is_array($elements)) {
635  continue;
636  }
637  foreach ($elements as $softrefId => $element) {
638  if (!in_array($element['subst']['type'] ?? '', ['db', 'string'], true)) {
639  continue;
640  }
641  $referencedTable = '_STRING';
642  $referencedUid = 0;
643  $referencedString = '';
644  if ($element['subst']['type'] === 'db') {
645  [$referencedTable, $referencedUid] = explode(':', $element['subst']['recordRef']);
646  } else {
647  $referencedString = mb_substr($element['subst']['tokenValue'], 0, 1024);
648  }
649  $relations[] = [
650  'tablename' => $tableName,
651  'recuid' => ‪$record['uid'],
652  'field' => $fieldName,
653  'flexpointer' => $flexPointer,
654  'softref_key' => $softrefKey,
655  'softref_id' => (string)$softrefId,
656  'sorting' => -1,
657  'workspace' => $workspaceUid,
658  'ref_table' => $referencedTable,
659  'ref_uid' => (int)$referencedUid,
660  'ref_string' => $referencedString,
661  ];
662  }
663  }
664  }
665  }
666  }
667  return $relations;
668  }
669 
670  private function ‪getRelationsFromFlexData(string $tableName, string $fieldName, array $row, int $workspaceUid): array
671  {
672  $valueArray = ‪GeneralUtility::xml2array($row[$fieldName] ?? '');
673  if (!is_array($valueArray)) {
674  // Current flex form values can not be parsed to an array. No relations.
675  return [];
676  }
677  try {
678  $dataStructureArray = $this->flexFormTools->parseDataStructureByIdentifier(
679  $this->flexFormTools->getDataStructureIdentifier(‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName], $tableName, $fieldName, $row)
680  );
682  // Data structure can not be resolved or parsed. No relations.
683  return [];
684  }
685  if (!is_array($dataStructureArray['sheets'] ?? false)) {
686  // No sheet in DS. Shouldn't happen, though.
687  return [];
688  }
689  $flexRelations = [];
690  foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
691  foreach (($sheetData['ROOT']['el'] ?? []) as $sheetElementKey => $sheetElementTca) {
692  // For all elements allowed in Data Structure.
693  if (($sheetElementTca['type'] ?? '') === 'array') {
694  // This is a section.
695  if (!is_array($sheetElementTca['el'] ?? false) || !is_array($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] ?? false)) {
696  // No possible containers defined for this section in DS, or no values set for this section.
697  continue;
698  }
699  foreach ($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] as $valueSectionContainerKey => $valueSectionContainers) {
700  // We have containers for this section in values.
701  if (!is_array($valueSectionContainers ?? false)) {
702  // Values don't validate to an array, skip.
703  continue;
704  }
705  foreach ($valueSectionContainers as $valueContainerType => $valueContainerElements) {
706  // For all value containers in this section.
707  if (!is_array($sheetElementTca['el'][$valueContainerType]['el'] ?? false)) {
708  // There is no DS for this container type, skip.
709  continue;
710  }
711  foreach ($sheetElementTca['el'][$valueContainerType]['el'] as $containerElement => $containerElementTca) {
712  // Container type of this value container exists in DS. Iterate DS container to find value relations.
713  if (isset($valueContainerElements['el'][$containerElement]['vDEF'])) {
714  $fieldValue = $valueContainerElements['el'][$containerElement]['vDEF'];
715  $structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/el/' . $valueSectionContainerKey . '/' . $valueContainerType . '/el/' . $containerElement . '/vDEF/';
716  // Flex form container section elements can not have DB relations, those are not checked.
717  // Add a softref definition for link and email fields if the TCA does not specify one already
718  if (($containerElementTca['config']['type'] ?? '') === 'link' && empty($containerElementTca['config']['softref'])) {
719  $containerElementTca['config']['softref'] = 'typolink';
720  }
721  if (($containerElementTca['config']['type'] ?? '') === 'email') {
722  $containerElementTca['config']['softref'] = 'email[subst]';
723  }
724  if ($fieldValue !== '' && ($containerElementTca['config']['softref'] ?? '') !== '') {
725  $tokenizedContent = $fieldValue;
726  foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($containerElementTca['config']['softref']) as $softReferenceParser) {
727  $parserResult = $softReferenceParser->parse($tableName, $fieldName, (int)$row['uid'], $fieldValue, $structurePath);
728  if ($parserResult->hasMatched()) {
729  $flexRelations['softrefs'][$structurePath]['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements();
730  if ($parserResult->hasContent()) {
731  $tokenizedContent = $parserResult->getContent();
732  }
733  }
734  }
735  if (!empty($flexRelations['softrefs'][$structurePath]) && $fieldValue !== $tokenizedContent) {
736  $flexRelations['softrefs'][$structurePath]['tokenizedContent'] = $tokenizedContent;
737  }
738  }
739  }
740  }
741  }
742  }
743  } elseif (isset($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'])) {
744  // Not a section but a simple field. Get its relations.
745  $fieldValue = $valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'];
746  $structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/vDEF/';
747  $databaseRelations = $this->getRelationsFromRelationField($tableName, $fieldValue, $sheetElementTca['config'] ?? [], (int)$row['uid'], $workspaceUid);
748  if (!empty($databaseRelations)) {
749  $flexRelations['db'][$structurePath] = $databaseRelations;
750  }
751  // Add a softref definition for link and email fields if the TCA does not specify one already
752  if (($sheetElementTca['config']['type'] ?? '') === 'link' && empty($sheetElementTca['config']['softref'])) {
753  $sheetElementTca['config']['softref'] = 'typolink';
754  }
755  if (($sheetElementTca['config']['type'] ?? '') === 'email') {
756  $sheetElementTca['config']['softref'] = 'email[subst]';
757  }
758  if ($fieldValue !== '' && ($sheetElementTca['config']['softref'] ?? '') !== '') {
759  $tokenizedContent = $fieldValue;
760  foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($sheetElementTca['config']['softref']) as $softReferenceParser) {
761  $parserResult = $softReferenceParser->parse($tableName, $fieldName, (int)$row['uid'], $fieldValue, $structurePath);
762  if ($parserResult->hasMatched()) {
763  $flexRelations['softrefs'][$structurePath]['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements();
764  if ($parserResult->hasContent()) {
765  $tokenizedContent = $parserResult->getContent();
766  }
767  }
768  }
769  if (!empty($flexRelations['softrefs'][$structurePath]) && $fieldValue !== $tokenizedContent) {
770  $flexRelations['softrefs'][$structurePath]['tokenizedContent'] = $tokenizedContent;
771  }
772  }
773  }
774  }
775  }
776  return $flexRelations;
777  }
778 
782  private function ‪getRelationsFromRelationField(string $tableName, mixed $fieldValue, array $conf, int ‪$uid, int $workspaceUid, array $row = []): array
783  {
784  if (empty($conf)) {
785  return [];
786  }
787  if (($conf['type'] === 'inline' || $conf['type'] === 'file') && !empty($conf['foreign_table']) && empty($conf['MM'])) {
788  $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
789  $dbAnalysis->setUseLiveReferenceIds(false);
790  $dbAnalysis->setWorkspaceId($workspaceUid);
791  $dbAnalysis->start($fieldValue, $conf['foreign_table'], '', ‪$uid, $tableName, $conf);
792  return $dbAnalysis->itemArray;
793  }
794  if ($this->isDbReferenceField($conf)) {
795  $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
796  if ($conf['MM_opposite_field'] ?? false) {
797  // Never handle sys_refindex when looking at MM from foreign side
798  return [];
799  }
800  $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
801  $dbAnalysis->setWorkspaceId($workspaceUid);
802  $dbAnalysis->start($fieldValue, $allowedTables, $conf['MM'] ?? '', ‪$uid, $tableName, $conf);
803  $itemArray = $dbAnalysis->itemArray;
805  && $workspaceUid > 0
806  && !empty($conf['MM'] ?? '')
807  && !empty($conf['allowed'] ?? '')
808  && empty($conf['MM_opposite_field'] ?? '')
809  && (int)($row['t3ver_wsid'] ?? 0) === 0
810  ) {
811  // When dealing with local side mm relations in workspace 0, there may be workspace records on the foreign
812  // side, for instance when those got an additional category. See ManyToMany Modify addCategoryRelations test.
813  // In those cases, the full set of relations must be written to sys_refindex as workspace rows.
814  // But, if the relations in this workspace and live are identical, no sys_refindex workspace rows
815  // have to be added.
816  $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
817  $dbAnalysis->setWorkspaceId(0);
818  $dbAnalysis->start($fieldValue, $allowedTables, $conf['MM'], ‪$uid, $tableName, $conf);
819  $itemArrayLive = $dbAnalysis->itemArray;
820  if ($itemArrayLive === $itemArray) {
821  $itemArray = [];
822  }
823  }
824  return $itemArray;
825  }
826  return [];
827  }
828 
835  private function ‪isDbReferenceField(array $configuration): bool
836  {
837  return
838  $configuration['type'] === 'group'
839  || (
840  in_array($configuration['type'], ['select', 'category', 'inline', 'file'], true)
841  && !empty($configuration['foreign_table'])
842  );
843  }
844 
849  private function ‪isReferenceField(array $configuration): bool
850  {
851  return
852  $this->isDbReferenceField($configuration)
853  || $configuration['type'] === 'link'
854  || $configuration['type'] === 'email'
855  || $configuration['type'] === 'flex'
856  || isset($configuration['softref'])
857  ;
858  }
859 
866  private function ‪getTableRelationFields(string $tableName): array
867  {
868  if (isset($this->tableRelationFieldCache[$tableName])) {
869  return $this->tableRelationFieldCache[$tableName];
870  }
871  if (!is_array(‪$GLOBALS['TCA'][$tableName]['columns'] ?? false)) {
872  $this->tableRelationFieldCache[$tableName] = [];
873  return [];
874  }
875  $relationFields = [];
876  foreach (‪$GLOBALS['TCA'][$tableName]['columns'] as $fieldName => $fieldDefinition) {
877  if (!is_array($fieldDefinition['config'] ?? false)) {
878  continue;
879  }
880  if ($this->isReferenceField($fieldDefinition['config'])) {
881  $relationFields[] = $fieldName;
882  }
883  }
884  $this->tableRelationFieldCache[$tableName] = $relationFields;
885  return $relationFields;
886  }
887 
893  private function ‪getListOfActiveWorkspaces(): array
894  {
895  if (!‪ExtensionManagementUtility::isLoaded('workspaces')) {
896  // If ext:workspaces is not loaded, "0" is the only valid one.
897  return [0];
898  }
899  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_workspace');
900  // Workspaces can't be 'hidden', so we only use deleted restriction here.
901  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
902  $result = $queryBuilder->select('uid')->from('sys_workspace')->orderBy('uid')->executeQuery();
903  // "0", plus non-deleted workspaces are active
904  return array_merge([0 => 0], $result->fetchFirstColumn());
905  }
906 
913  private function ‪getNumberOfUnusedWorkspaceRowsInReferenceIndex(array $activeWorkspaces): int
914  {
915  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
916  $queryBuilder->getRestrictions()->removeAll();
917  $numberOfInvalidWorkspaceRecords = $queryBuilder
918  ->count('hash')
919  ->from('sys_refindex')
920  ->where(
921  $queryBuilder->expr()->notIn('workspace', $queryBuilder->createNamedParameter($activeWorkspaces, ‪Connection::PARAM_INT_ARRAY))
922  )
923  ->executeQuery()
924  ->fetchOne();
925  return (int)$numberOfInvalidWorkspaceRecords;
926  }
927 
931  private function ‪removeUnusedWorkspaceRowsFromReferenceIndex(array $activeWorkspaces): int
932  {
933  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
934  $queryBuilder->getRestrictions()->removeAll();
935  return $queryBuilder
936  ->delete('sys_refindex')
937  ->where(
938  $queryBuilder->expr()->notIn('workspace', $queryBuilder->createNamedParameter($activeWorkspaces, ‪Connection::PARAM_INT_ARRAY))
939  )
940  ->executeStatement();
941  }
942 
947  private function ‪getNumberOfUnusedTablesInReferenceIndex(array $tableNames): int
948  {
949  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
950  $queryBuilder->getRestrictions()->removeAll();
951  $numberOfRowsOfUnusedTables = $queryBuilder
952  ->count('hash')
953  ->from('sys_refindex')
954  ->where(
955  $queryBuilder->expr()->notIn('tablename', $queryBuilder->createNamedParameter($tableNames, ‪Connection::PARAM_STR_ARRAY))
956  )
957  ->executeQuery()
958  ->fetchOne();
959  return (int)$numberOfRowsOfUnusedTables;
960  }
961 
966  private function ‪removeReferenceIndexDataFromUnusedDatabaseTables(array $tableNames): int
967  {
968  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
969  $queryBuilder->getRestrictions()->removeAll();
970  return $queryBuilder
971  ->delete('sys_refindex')
972  ->where(
973  $queryBuilder->expr()->notIn('tablename', $queryBuilder->createNamedParameter($tableNames, ‪Connection::PARAM_STR_ARRAY))
974  )
975  ->executeStatement();
976  }
977 
981  private function ‪shouldExcludeTableFromReferenceIndex(string $tableName): bool
982  {
983  if (isset($this->excludedTables[$tableName])) {
984  return $this->excludedTables[$tableName];
985  }
986  $event = new ‪IsTableExcludedFromReferenceIndexEvent($tableName);
987  $event = $this->eventDispatcher->dispatch($event);
988  $this->excludedTables[$tableName] = $event->isTableExcluded();
989  return $this->excludedTables[$tableName];
990  }
991 }
‪TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent
Definition: IsTableExcludedFromReferenceIndexEvent.php:28
‪TYPO3\CMS\Core\Database\ReferenceIndex\getRelationsFromFlexData
‪getRelationsFromFlexData(string $tableName, string $fieldName, array $row, int $workspaceUid)
Definition: ReferenceIndex.php:670
‪TYPO3\CMS\Core\Database\ReferenceIndex\getNumberOfUnusedWorkspaceRowsInReferenceIndex
‪getNumberOfUnusedWorkspaceRowsInReferenceIndex(array $activeWorkspaces)
Definition: ReferenceIndex.php:913
‪TYPO3\CMS\Core\Database\ReferenceIndex\shouldExcludeTableFromReferenceIndex
‪shouldExcludeTableFromReferenceIndex(string $tableName)
Definition: ReferenceIndex.php:981
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Database\ReferenceIndex\updateIndex
‪array updateIndex(bool $testOnly, ?ProgressListenerInterface $progressListener=null)
Definition: ReferenceIndex.php:94
‪TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory
Definition: SoftReferenceParserFactory.php:28
‪TYPO3\CMS\Core\Database\ReferenceIndex
Definition: ReferenceIndex.php:40
‪TYPO3\CMS\Core\Database\ReferenceIndex\compileReferenceIndexRowsForRecord
‪compileReferenceIndexRowsForRecord(string $tableName, array $record, int $workspaceUid)
Definition: ReferenceIndex.php:544
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Core\Database\ReferenceIndex\$excludedTables
‪array $excludedTables
Definition: ReferenceIndex.php:51
‪TYPO3\CMS\Core\Database\ReferenceIndex\__construct
‪__construct(private readonly EventDispatcherInterface $eventDispatcher, private readonly SoftReferenceParserFactory $softReferenceParserFactory, private readonly ConnectionPool $connectionPool, private readonly Registry $registry, private readonly FlexFormTools $flexFormTools,)
Definition: ReferenceIndex.php:59
‪TYPO3\CMS\Core\Database\ReferenceIndex\removeReferenceIndexDataFromUnusedDatabaseTables
‪removeReferenceIndexDataFromUnusedDatabaseTables(array $tableNames)
Definition: ReferenceIndex.php:966
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\isLoaded
‪static isLoaded(string $key)
Definition: ExtensionManagementUtility.php:55
‪TYPO3\CMS\Core\Database\ReferenceIndex\$currentRelationHashes
‪$currentRelationHashes
Definition: ReferenceIndex.php:516
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility
Definition: ExtensionManagementUtility.php:32
‪TYPO3\CMS\Core\Database\ReferenceIndex\HASH_VERSION
‪const HASH_VERSION
Definition: ReferenceIndex.php:42
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException
Definition: InvalidIdentifierException.php:21
‪TYPO3\CMS\Core\Database\ReferenceIndex\getTableRelationFields
‪getTableRelationFields(string $tableName)
Definition: ReferenceIndex.php:866
‪TYPO3\CMS\Core\Database\ReferenceIndex\isDbReferenceField
‪bool isDbReferenceField(array $configuration)
Definition: ReferenceIndex.php:835
‪TYPO3\CMS\Core\Database\ReferenceIndex\isReferenceField
‪isReferenceField(array $configuration)
Definition: ReferenceIndex.php:849
‪TYPO3\CMS\Core\Database\ReferenceIndex\getListOfActiveWorkspaces
‪int[] getListOfActiveWorkspaces()
Definition: ReferenceIndex.php:893
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR_ARRAY
‪const PARAM_STR_ARRAY
Definition: Connection.php:77
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
Definition: FlexFormTools.php:40
‪TYPO3\CMS\Core\Database\ReferenceIndex\getNumberOfReferencedRecords
‪getNumberOfReferencedRecords(string $tableName, int $uid)
Definition: ReferenceIndex.php:70
‪TYPO3\CMS\Webhooks\Message\$record
‪identifier readonly int readonly array $record
Definition: PageModificationMessage.php:36
‪$errors
‪$errors
Definition: annotationChecker.php:116
‪TYPO3\CMS\Core\Database\ReferenceIndex\$queryResult
‪$queryResult
Definition: ReferenceIndex.php:511
‪TYPO3\CMS\Core\Database\ReferenceIndex\getRelationsFromRelationField
‪getRelationsFromRelationField(string $tableName, mixed $fieldValue, array $conf, int $uid, int $workspaceUid, array $row=[])
Definition: ReferenceIndex.php:782
‪TYPO3\CMS\Core\Database\ReferenceIndex\updateRefIndexTable
‪array updateRefIndexTable(string $tableName, int $uid, bool $testOnly=false, int $workspaceUid=0, array $currentRecord=null)
Definition: ReferenceIndex.php:376
‪TYPO3\CMS\Backend\View\ProgressListenerInterface
Definition: ProgressListenerInterface.php:31
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Database\ReferenceIndex\removeRelationHashes
‪removeRelationHashes(array $currentRelationHashes)
Definition: ReferenceIndex.php:528
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation
Definition: PlatformInformation.php:33
‪TYPO3\CMS\Core\Database\ReferenceIndex\removeUnusedWorkspaceRowsFromReferenceIndex
‪removeUnusedWorkspaceRowsFromReferenceIndex(array $activeWorkspaces)
Definition: ReferenceIndex.php:931
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Database\ReferenceIndex\getNumberOfUnusedTablesInReferenceIndex
‪getNumberOfUnusedTablesInReferenceIndex(array $tableNames)
Definition: ReferenceIndex.php:947
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT_ARRAY
‪const PARAM_INT_ARRAY
Definition: Connection.php:72
‪TYPO3\CMS\Core\Utility\GeneralUtility\xml2array
‪static array string xml2array(string $string, string $NSprefix='', bool $reportDocTag=false)
Definition: GeneralUtility.php:1265
‪TYPO3\CMS\Core\Database\ReferenceIndex\$tableRelationFieldCache
‪array $tableRelationFieldCache
Definition: ReferenceIndex.php:57
‪TYPO3\CMS\Core\Database\ReferenceIndex\$queryBuilder
‪$queryBuilder
Definition: ReferenceIndex.php:509
‪TYPO3\CMS\Core\Database
Definition: Connection.php:18