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