connectionPool->getQueryBuilderForTable('sys_refindex'); return (int)$queryBuilder ->count('*')->from('sys_refindex') ->where( $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($tableName)), $queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)) )->executeQuery()->fetchOne(); } /** * Update full refindex. Used by 'referenceindex:update' CLI and ext:lowlevel BE UI. * * @param ProgressListenerInterface|null $progressListener If set, the current progress is added to the listener * @return array Header and body status content * @internal */ public function updateIndex(bool $testOnly, ?ProgressListenerInterface $progressListener = null): array { $errors = []; $numberOfHandledRecords = 0; $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces'); $tcaTableNames = $this->tcaSchemaFactory->all()->getNames(); // @todo: Ensure tcaSchemaFactory->all() always sorts alphabetically (or add test to verify), then remove this sort() sort($tcaTableNames); $progressListener?->log('Remember to create missing tables and columns before running this.', LogLevel::WARNING); // Remove dangling workspace sys_refindex rows $listOfActiveWorkspaces = $this->getListOfActiveWorkspaces(); $numberOfUnusedWorkspaceRows = $testOnly ? $this->getNumberOfUnusedWorkspaceRowsInReferenceIndex($listOfActiveWorkspaces) : $this->removeUnusedWorkspaceRowsFromReferenceIndex($listOfActiveWorkspaces); if ($numberOfUnusedWorkspaceRows > 0) { $error = 'Index table hosted ' . $numberOfUnusedWorkspaceRows . ' indexes for non-existing or deleted workspaces, now removed.'; $errors[] = $error; $progressListener?->log($error, LogLevel::WARNING); } // Remove sys_refindex rows of tables no longer defined in TCA $numberOfRowsOfOldTables = $testOnly ? $this->getNumberOfUnusedTablesInReferenceIndex($tcaTableNames) : $this->removeReferenceIndexDataFromUnusedDatabaseTables($tcaTableNames); if ($numberOfRowsOfOldTables > 0) { $error = 'Index table hosted ' . $numberOfRowsOfOldTables . ' indexes for non-existing tables, now removed'; $errors[] = $error; $progressListener?->log($error, LogLevel::WARNING); } // Main loop traverses all records of all TCA tables foreach ($tcaTableNames as $tableName) { $tableTcaSchema = $this->tcaSchemaFactory->get($tableName); // Count number of records in table to have a correct $numberOfHandledRecords in the end $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); $queryBuilder->getRestrictions()->removeAll(); $numberOfRecordsInTargetTable = $queryBuilder ->count('uid') ->from($tableName) ->executeQuery() ->fetchOne(); $progressListener?->start($numberOfRecordsInTargetTable, $tableName); if ($numberOfRecordsInTargetTable === 0 || $this->shouldExcludeTableFromReferenceIndex($tableName) || empty($this->getTableRelationFields($tableName))) { // Table is empty, should be excluded, or can not have relations. Blindly remove any existing sys_refindex rows. $numberOfHandledRecords += $numberOfRecordsInTargetTable; $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex'); $queryBuilder->getRestrictions()->removeAll(); if ($testOnly) { $countDeleted = $queryBuilder ->count('hash') ->from('sys_refindex') ->where($queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName))) ->executeQuery() ->fetchOne(); } else { $countDeleted = $queryBuilder ->delete('sys_refindex') ->where($queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName))) ->executeStatement(); } if ($countDeleted > 0) { $error = 'Index table hosted ' . $countDeleted . ' ignored or outdated indexed, now removed.'; $errors[] = $error; $progressListener?->log($error, LogLevel::WARNING); } $progressListener?->finish(); continue; } // Delete lost indexes of table: sys_refindex rows where the uid no longer exists in target table. $subQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); $subQueryBuilder->getRestrictions()->removeAll(); $subQueryBuilder ->select('uid') ->from($tableName, 'sub_' . $tableName) ->where( $subQueryBuilder->expr()->eq('sub_' . $tableName . '.uid', $subQueryBuilder->quoteIdentifier('sys_refindex.recuid')) ); $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex'); $queryBuilder->getRestrictions()->removeAll(); if ($testOnly) { $numberOfRefindexRowsWithoutExistingTableRow = $queryBuilder ->count('hash') ->from('sys_refindex') ->where( $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)), 'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')' ) ->executeQuery() ->fetchOne(); } else { $numberOfRefindexRowsWithoutExistingTableRow = $queryBuilder ->delete('sys_refindex') ->where( $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)), 'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')' ) ->executeStatement(); } if ($numberOfRefindexRowsWithoutExistingTableRow > 0) { $error = 'Table ' . $tableName . ' hosted ' . $numberOfRefindexRowsWithoutExistingTableRow . ' lost indexes, now removed.'; $errors[] = $error; $progressListener?->log($error, LogLevel::WARNING); } // Delete rows in sys_refindex related to this table where the record is soft-deleted=1. if ($tableTcaSchema->hasCapability(TcaSchemaCapability::SoftDelete)) { $softDeleteFieldName = $tableTcaSchema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName(); $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); $queryBuilder->getRestrictions()->removeAll(); $numberOfDeletedRecordsInTargetTable = $queryBuilder ->count('uid') ->from($tableName) ->where($queryBuilder->expr()->eq($softDeleteFieldName, 1)) ->executeQuery() ->fetchOne(); if ($numberOfDeletedRecordsInTargetTable > 0) { $numberOfHandledRecords += $numberOfDeletedRecordsInTargetTable; // List of deleted=0 records in target table that have records in sys_refindex. if ($testOnly) { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex'); $queryBuilder->getRestrictions()->removeAll(); // $subQueryBuilder actually fills parameter placeholders for the main $queryBuilder. // The subQuery is never meant to be executed on its own, only used to be filled-in // via $subQueryBuilder->getSQL(). $subQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); $subQueryBuilder->getRestrictions()->removeAll(); $subQueryBuilder ->select('sub_' . $tableName . '.uid') ->distinct() ->from($tableName, 'sub_' . $tableName) ->join( 'sub_' . $tableName, 'sys_refindex', 'sub_refindex', $queryBuilder->expr()->eq('sub_refindex.recuid', $queryBuilder->quoteIdentifier('sub_' . $tableName . '.uid')) ) ->where( $queryBuilder->expr()->eq('sub_refindex.tablename', $queryBuilder->createNamedParameter($tableName)), $queryBuilder->expr()->eq('sub_' . $tableName . '.' . $softDeleteFieldName, 1), ); $numberOfRemovedIndexes = $queryBuilder ->count('hash') ->from('sys_refindex') ->where( $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)), $queryBuilder->quoteIdentifier('recuid') . ' IN ( ' . $subQueryBuilder->getSQL() . ' )' ) ->executeQuery() ->fetchOne(); } else { // MySQL is picky when using the same table in a sub-query and an outer delete query, if // it is not materialized into a temporary table. Enforcing a temporary table would mitigate this // MySQL limit, but we simply fetch the affected uid list instead and fire a chunked delete query. // In contrast to $testOnly above, we execute the subQuery, named parameter placeholders need // to be relative to its QueryBuilder. $uidListQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); $uidListQueryBuilder->getRestrictions()->removeAll(); $uidListQueryBuilder ->select('sub_' . $tableName . '.uid') ->distinct() ->from($tableName, 'sub_' . $tableName) ->join( 'sub_' . $tableName, 'sys_refindex', 'sub_refindex', $uidListQueryBuilder->expr()->eq('sub_refindex.recuid', $uidListQueryBuilder->quoteIdentifier('sub_' . $tableName . '.uid')) ) ->where( $uidListQueryBuilder->expr()->eq('sub_refindex.tablename', $uidListQueryBuilder->createNamedParameter($tableName)), $uidListQueryBuilder->expr()->eq('sub_' . $tableName . '.' . $softDeleteFieldName, 1), ); $uidListOfRemovableIndexes = $uidListQueryBuilder->executeQuery()->fetchFirstColumn(); $numberOfRemovedIndexes = 0; // Another variant to solve this would be a limit/offset query for the upper query, feeding delete. // This would be more memory efficient. We however think there shouldn't be *that* many affected // rows to delete in casual scenarios, so we skip that optimization for now since chunking isn't // needed in most cases anyway. // 10k is an arbitrary number. Reasoning: 1MB max query length with 10-char uids (9mio uid-range with comma) // would allow ~10k uids. Combi tablename/recuid is indexed, so delete should be relatively quick even with // larger sets, so delete-hard-locking on for instance innodb shouldn't be a huge issue here. foreach (array_chunk($uidListOfRemovableIndexes, 10000) as $uidChunkOfRemovableIndexes) { $chunkQueryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex'); $chunkQueryBuilder->getRestrictions()->removeAll(); $chunkedNumberOfRemovedIndexes = $chunkQueryBuilder ->delete('sys_refindex') ->where( $chunkQueryBuilder->expr()->eq('tablename', $chunkQueryBuilder->createNamedParameter($tableName)), $chunkQueryBuilder->expr()->in('recuid', $uidChunkOfRemovableIndexes) ) ->executeStatement(); $numberOfRemovedIndexes += $chunkedNumberOfRemovedIndexes; } } if ($numberOfRemovedIndexes > 0) { $error = 'Table ' . $tableName . ' hosted ' . $numberOfRemovedIndexes . ' indexes from soft-deleted records, now removed.'; $errors[] = $error; $progressListener?->log($error, LogLevel::WARNING); } $progressListener?->advance($numberOfDeletedRecordsInTargetTable); } } // Some additional magic is needed if the table has a field that is the local side of // a mm relation. See the variable usage below for details. $tableHasLocalSideMmRelation = false; foreach ($tableTcaSchema->getFields() as $field) { $fieldConfig = $field->getConfiguration(); if (!empty($fieldConfig['MM'] ?? '') // Catch type=group 'allowed' and type=select 'foreign_table' MM scenarios && (!empty($fieldConfig['allowed'] ?? '') || !empty($fieldConfig['foreign_table'] ?? '')) && empty($fieldConfig['MM_opposite_field'] ?? '') ) { $tableHasLocalSideMmRelation = true; } } // Traverse all records in table, not including soft-deleted records $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $queryResult = $queryBuilder ->select('*') ->from($tableName) ->orderBy('uid') ->executeQuery(); while ($record = $queryResult->fetchAssociative()) { $progressListener?->advance(); if ($isWorkspacesLoaded && $tableHasLocalSideMmRelation && (int)($record['t3ver_wsid'] ?? 0) === 0) { // If we have a record that can be the local side of a workspace relation, workspace records // may point to it, even though the record has no workspace overlay. See workspace ManyToMany // Modify addCategoryRelation as example. In those cases, we need to iterate all active workspaces // and update refindex for all foreign workspace records that point to it. foreach ($listOfActiveWorkspaces as $workspaceId) { $result = $this->updateRefIndexTable($tableName, (int)$record['uid'], $testOnly, $workspaceId, $record); $numberOfHandledRecords++; if ($result['addedNodes'] || $result['deletedNodes']) { $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes'; $errors[] = $error; $progressListener?->log($error, LogLevel::WARNING); } } } else { $result = $this->updateRefIndexTable($tableName, (int)$record['uid'], $testOnly, (int)($record['t3ver_wsid'] ?? 0), $record); $numberOfHandledRecords++; if ($result['addedNodes'] || $result['deletedNodes']) { $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes'; $errors[] = $error; $progressListener?->log($error, LogLevel::WARNING); } } } $progressListener?->finish(); } $errorCount = count($errors); $recordsCheckedString = $numberOfHandledRecords . ' records from ' . count($tcaTableNames) . ' tables were checked/updated.'; if ($errorCount) { $progressListener?->log($recordsCheckedString . ' Updates: ' . $errorCount, LogLevel::WARNING); } else { $progressListener?->log($recordsCheckedString . ' Index Integrity was perfect.'); } if (!$testOnly) { $this->registry->set('core', 'sys_refindex_lastUpdate', $GLOBALS['EXEC_TIME']); } return ['resultText' => trim($recordsCheckedString), 'errors' => $errors]; } /** * Update the sys_refindex table for a record, even one just deleted. * This is used by DataHandler ReferenceIndexUpdater as entry method to take care of single records. * It is also used internally via updateIndex() by CLI "referenceindex:update" and lowlevel BE module. * * @param array|null $currentRecord Current full (select *) record from DB. Optimization for updateIndex(). * @return array Statistics about how many index records were added, deleted and not altered. * @internal */ public function updateRefIndexTable(string $tableName, int $uid, bool $testOnly = false, int $workspaceUid = 0, ?array $currentRecord = null): array { $result = [ 'keptNodes' => 0, 'deletedNodes' => 0, 'addedNodes' => 0, ]; if ($uid < 1 || $this->shouldExcludeTableFromReferenceIndex($tableName) || empty($this->getTableRelationFields($tableName))) { // Not a valid uid, the table is excluded, or can not contain relations. return $result; } if ($currentRecord === null) { // Fetch record if not provided. $currentRecord = BackendUtility::getRecord($tableName, $uid); } $currentRelationHashes = $this->getCurrentRelationHashes($tableName, $uid, $workspaceUid); if ($currentRecord === null) { // If there is no record because it was hard or soft-deleted, remove any existing sys_refindex rows of it. $numberOfLeftOverRelationHashes = count($currentRelationHashes); $result['deletedNodes'] = $numberOfLeftOverRelationHashes; if ($numberOfLeftOverRelationHashes > 0 && !$testOnly) { $this->removeRelationHashes($currentRelationHashes); } return $result; } $relations = $this->compileReferenceIndexRowsForRecord($tableName, $currentRecord, $workspaceUid); $connection = $this->connectionPool->getConnectionForTable('sys_refindex'); $relationsToInsert = []; foreach ($relations as $relation) { if (!is_array($relation)) { continue; } // Exclude any relations TO a specific table if (($relation['ref_table'] ?? '') && $this->shouldExcludeTableFromReferenceIndex($relation['ref_table'])) { continue; } $relation['hash'] = hash(algo: 'xxh128', data: implode(',', $relation), options: ['seed' => self::HASH_VERSION]); // First, check if already indexed and if so, unset that row (so in the end we know which rows to remove!) if (isset($currentRelationHashes[$relation['hash']])) { unset($currentRelationHashes[$relation['hash']]); $result['keptNodes']++; } else { // If new, register for bulk add: if (!$testOnly) { $relationsToInsert[] = $relation; } $result['addedNodes']++; } } if (!$testOnly && !empty($relationsToInsert)) { try { $connection->bulkInsert('sys_refindex', $relationsToInsert, array_keys(current($relationsToInsert))); } catch (\Exception $e) { // Do nothing for the time being } } // If any existing are left, they are not in the current set anymore. Remove them. $numberOfLeftOverRelationHashes = count($currentRelationHashes); $result['deletedNodes'] = $numberOfLeftOverRelationHashes; if ($numberOfLeftOverRelationHashes > 0 && !$testOnly) { $this->removeRelationHashes($currentRelationHashes); } return $result; } /** * Returns relation information for a record from a TCA table. * * @return array Array with information about relations * @internal */ public function getRelations(string $tableName, array $record, int $workspaceUid): array { $result = []; $relationFields = $this->getTableRelationFields($tableName); $tableTcaSchema = $this->tcaSchemaFactory->get($tableName); foreach ($relationFields as $fieldName) { $value = $record[$fieldName] ?? null; if (!$tableTcaSchema->hasField($fieldName)) { continue; } $field = $tableTcaSchema->getField($fieldName); $fieldConfig = $field->getConfiguration(); $resultsFromDatabase = $this->getRelationsFromRelationField($tableName, $value, $fieldConfig, (int)$record['uid'], $workspaceUid, $record); if (!empty($resultsFromDatabase)) { // Create an entry for the field with all DB relations: $result[$fieldName] = [ 'type' => 'db', 'itemArray' => $resultsFromDatabase, ]; } if ($field->isType(TableColumnType::FLEX) && is_string($value) && $value !== '') { // Traverse the flex data structure looking for db references for flex fields. $flexFormRelations = $this->getRelationsFromFlexData($tableName, $fieldName, $record, $workspaceUid); if (!empty($flexFormRelations)) { $result[$fieldName] = [ 'type' => 'flex', 'flexFormRels' => $flexFormRelations, ]; } } if ((string)$value !== '') { // Soft References $softRefValue = $value; $softReferenceKeys = $field->getSoftReferenceKeys(); if ($softReferenceKeys !== false) { foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList(implode(',', $softReferenceKeys)) as $softReferenceParser) { $parserResult = $softReferenceParser->parse($tableName, $fieldName, (int)$record['uid'], $softRefValue); if ($parserResult->hasMatched()) { $result[$fieldName]['softrefs']['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements(); if ($parserResult->hasContent()) { $softRefValue = $parserResult->getContent(); } } } } if (!empty($result[$fieldName]['softrefs']) && (string)$value !== (string)$softRefValue && str_contains($softRefValue, '{softref:')) { $result[$fieldName]['softrefs']['tokenizedContent'] = $softRefValue; } } } return $result; } /** * Get current sys_refindex rows of table:uid from database with hash as index. * * @return array */ private function getCurrentRelationHashes(string $tableName, int $uid, int $workspaceUid): array { $connection = $this->connectionPool->getConnectionForTable('sys_refindex'); $queryBuilder = $connection->createQueryBuilder(); $queryBuilder->getRestrictions()->removeAll(); $queryResult = $queryBuilder->select('hash')->from('sys_refindex')->where( $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)), $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)), $queryBuilder->expr()->eq('workspace', $queryBuilder->createNamedParameter($workspaceUid, Connection::PARAM_INT)) )->executeQuery(); $currentRelationHashes = []; while ($relation = $queryResult->fetchAssociative()) { $currentRelationHashes[$relation['hash']] = true; } return $currentRelationHashes; } /** * Remove sys_refindex rows by hash. * * @param array $currentRelationHashes */ private function removeRelationHashes(array $currentRelationHashes): void { $connection = $this->connectionPool->getConnectionForTable('sys_refindex'); $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform()); $chunks = array_chunk(array_keys($currentRelationHashes), $maxBindParameters - 10, true); foreach ($chunks as $chunk) { $queryBuilder = $connection->createQueryBuilder(); $queryBuilder ->delete('sys_refindex') ->where( $queryBuilder->expr()->in('hash', $queryBuilder->createNamedParameter($chunk, Connection::PARAM_STR_ARRAY)) ) ->executeStatement(); } } private function compileReferenceIndexRowsForRecord(string $tableName, array $record, int $workspaceUid): array { $relations = []; $tableTcaSchema = $this->tcaSchemaFactory->get($tableName); $hiddenFieldValue = $tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField) ? (int)$record[$tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()] : 0; $starttimeFieldValue = $tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime) ? (int)$record[$tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()] : 0; $endtimeFieldValue = $tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime) ? (int)($record[$tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()] ?: 2147483647) : 2147483647; // @todo: 2^31-1 (year 2038) and not 2^32-1 since postgres 32-bit int is always signed $recordRelations = $this->getRelations($tableName, $record, $workspaceUid); foreach ($recordRelations as $fieldName => $fieldRelations) { $field = $tableTcaSchema->getField($fieldName); if ($tableTcaSchema->isWorkspaceAware() && isset($record['t3ver_wsid']) && (int)$record['t3ver_wsid'] !== $workspaceUid && $field instanceof RelationalFieldTypeInterface && $field->getRelationshipType() !== RelationshipType::ManyToMany ) { // The given record is workspace-enabled but doesn't live in the selected workspace. Don't add index, it's not actually there. // 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. // See workspaces ManyToMany Modify addCategoryRelation for details on this case. continue; } if (is_array($fieldRelations['itemArray'] ?? false) && !empty($fieldRelations['itemArray'])) { // DB relations in a db field $itemArray = $fieldRelations['itemArray']; if ($field->isType(TableColumnType::INLINE, TableColumnType::FILE) && $field instanceof RelationalFieldTypeInterface && $field->getRelationshipType()->isSingularRelationship() ) { // RelationHandler does not return info on hidden, starttime, endtime for inline non-MM, yet. Add this now. // @todo: Refactor RelationHandler / PlainDataResolver to (optionally?) return full child row record $itemArray = $this->enrichInlineRelations(current($itemArray)['table'], $fieldRelations['itemArray']); } if ($field->isType(TableColumnType::INLINE, TableColumnType::GROUP, TableColumnType::SELECT) && $field instanceof RelationalFieldTypeInterface && $field->getRelationshipType() === RelationshipType::ManyToMany ) { foreach ($itemArray as $itemKey => $item) { // Get rid of soft-deleted foreign records, those should not create refindex entries // @todo: It would be better if RelationHandler would not return soft-deleted MM rows. Unsure // how to do that since RH only works on the MM table, but it could then also return // the full foreign row along the way. // @todo: Expensive. This code could be optimized to fetch multiple records at once per foreign // table, or make RH return full foreign rows in some hopefully efficient way. $foreignSideRecord = BackendUtility::getRecord($item['table'], (int)$item['id']); if ($foreignSideRecord === null) { // @todo: This mixes up ref_sorting when rows are removed here. Shouldn't be // very problematic, though. unset($itemArray[$itemKey]); continue; } $itemTableSchema = $this->tcaSchemaFactory->get((string)$item['table']); if ($itemTableSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) { $disabledFieldName = $itemTableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(); $itemArray[$itemKey][$disabledFieldName] = $foreignSideRecord[$disabledFieldName]; } if ($itemTableSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) { $starttimeFieldName = $itemTableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName(); $itemArray[$itemKey][$starttimeFieldName] = $foreignSideRecord[$starttimeFieldName]; } if ($itemTableSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) { $endtimeFieldName = $itemTableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName(); $itemArray[$itemKey][$endtimeFieldName] = $foreignSideRecord[$endtimeFieldName]; } if ($itemTableSchema->hasCapability(TcaSchemaCapability::Workspace)) { $itemArray[$itemKey]['t3ver_state'] = $foreignSideRecord['t3ver_state']; } } } $sorting = 0; foreach ($itemArray as $refRecord) { $refTable = (string)$refRecord['table']; $refTcaTableSchema = $this->tcaSchemaFactory->get($refTable); $relations[] = [ 'tablename' => $tableName, 'recuid' => (int)$record['uid'], 'field' => $fieldName, 'hidden' => $hiddenFieldValue, 'starttime' => $starttimeFieldValue, 'endtime' => $endtimeFieldValue, 't3ver_state' => (int)($record['t3ver_state'] ?? 0), 'flexpointer' => '', 'softref_key' => '', 'softref_id' => '', 'sorting' => $sorting, 'workspace' => $workspaceUid, 'ref_table' => $refTable, 'ref_uid' => (int)$refRecord['id'], 'ref_field' => (string)($refRecord['fieldname'] ?? ''), 'ref_hidden' => $refTcaTableSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField) ? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()] ?? 0) : 0, 'ref_starttime' => $refTcaTableSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime) ? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()] ?? 0) : 0, 'ref_endtime' => $refTcaTableSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime) ? (int)(($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()] ?? 0) ?: 2147483647) : 2147483647, 'ref_t3ver_state' => (int)($refRecord['t3ver_state'] ?? 0), 'ref_sorting' => (int)($refRecord['sorting_foreign'] ?? 0), 'ref_string' => '', ]; $sorting++; } } if (is_array($fieldRelations['softrefs']['keys'] ?? false)) { // Soft reference relations in a db field foreach ($fieldRelations['softrefs']['keys'] as $softrefKey => $elements) { if (!is_array($elements)) { continue; } foreach ($elements as $softrefId => $element) { if (!in_array($element['subst']['type'] ?? '', ['db', 'string'], true)) { continue; } $refTable = '_STRING'; $refUid = 0; $refString = ''; $refRecord = []; $refTcaTableSchema = null; if ($element['subst']['type'] === 'db') { $explodedRefTableUid = explode(':', $element['subst']['recordRef']); $refTable = $explodedRefTableUid[0]; $refUid = (int)$explodedRefTableUid[1]; if ($refRecord = BackendUtility::getRecord($refTable, $refUid)) { // @todo: It would be great to refactor the softref parser mess "data structure" // and let it return the reference record along the way - "db" type softrefs // fetch those already. $refTcaTableSchema = $this->tcaSchemaFactory->get($refTable); } else { // Sanitize the target record: If it does not exist, we should not create a softref // entry to it. Also ref_uid is the uid of the target record. If a softref parser // goes rogue and misinterprets for instance a huge number ("telephone") as uid, // creating this as softref would try to insert a "bigger than 2^31 int" for ref_uid, // leading to an out of bound insert. // @todo: This late validation of softref stuff should probably be relocated to the // parsers directly. Refactor this together with the above comment. // @todo: It is fishy that DataHandler does not sanitize fields with attached softref // parsers, either. Example: tt_content header_link with manual editor edit // "+49 123456789" which looks like a phone number, but the internal phone // syntax is "tel:49123456789". DH still happily writes the invalid thing into DB. // This should be rethought. continue; } } else { $refString = mb_substr($element['subst']['tokenValue'], 0, 1024); } $relations[] = [ 'tablename' => $tableName, 'recuid' => (int)$record['uid'], 'field' => $fieldName, 'hidden' => $hiddenFieldValue, 'starttime' => $starttimeFieldValue, 'endtime' => $endtimeFieldValue, 't3ver_state' => (int)($record['t3ver_state'] ?? 0), 'flexpointer' => '', 'softref_key' => (string)$softrefKey, 'softref_id' => (string)$softrefId, 'sorting' => 0, 'workspace' => $workspaceUid, 'ref_table' => $refTable, 'ref_uid' => $refUid, 'ref_field' => '', 'ref_hidden' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionDisabledField) ? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()] ?? 0) : 0, 'ref_starttime' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionStartTime) ? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()] ?? 0) : 0, 'ref_endtime' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionEndTime) ? (int)(($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()] ?? 0) ?: 2147483647) : 2147483647, 'ref_t3ver_state' => (int)($refRecord['t3ver_state'] ?? 0), 'ref_sorting' => 0, 'ref_string' => $refString, ]; } } } if (is_array($fieldRelations['flexFormRels']['db'] ?? false)) { // DB relations in a flex field foreach ($fieldRelations['flexFormRels']['db'] as $flexPointer => $subList) { $sorting = 0; foreach ($subList as $refRecord) { // @todo: This has no proper test setup in ReferenceIndexTest and ReferenceIndexWorkspaceLoadedTest. // We probably need to fetch the target record for inline relations here, as done with // $fieldRelations['itemArray'] enrichInlineRelations() above, to set ref_ fields hidden, starttime, // endtime and t3ver_state. Additionally, a test based on categories should verify MM details. $relations[] = [ 'tablename' => $tableName, 'recuid' => (int)$record['uid'], 'field' => $fieldName, 'hidden' => $hiddenFieldValue, 'starttime' => $starttimeFieldValue, 'endtime' => $endtimeFieldValue, 't3ver_state' => (int)($record['t3ver_state'] ?? 0), 'flexpointer' => (string)$flexPointer, 'softref_key' => '', 'softref_id' => '', 'sorting' => $sorting, 'workspace' => $workspaceUid, 'ref_table' => $refRecord['table'], 'ref_uid' => (int)$refRecord['id'], 'ref_field' => (string)($refRecord['fieldname'] ?? ''), // @todo: ref_hidden, ref_starttime, ref_endtime, ref_t3ver_state, ref_t3ver_state and ref_sorting need coverage and handling. 'ref_hidden' => 0, 'ref_starttime' => 0, 'ref_endtime' => 2147483647, 'ref_t3ver_state' => 0, 'ref_sorting' => 0, 'ref_string' => '', ]; $sorting++; } } } if (is_array($fieldRelations['flexFormRels']['softrefs'] ?? false)) { // Soft reference relations in a flex field foreach ($fieldRelations['flexFormRels']['softrefs'] as $flexPointer => $subList) { foreach ($subList['keys'] as $softrefKey => $elements) { if (!is_array($elements)) { continue; } foreach ($elements as $softrefId => $element) { if (!in_array($element['subst']['type'] ?? '', ['db', 'string'], true)) { continue; } $refTable = '_STRING'; $refUid = 0; $refString = ''; $refRecord = []; $refTcaTableSchema = null; if ($element['subst']['type'] === 'db') { $explodedRefTableUid = explode(':', $element['subst']['recordRef']); $refTable = $explodedRefTableUid[0]; $refUid = (int)$explodedRefTableUid[1]; if ($refRecord = BackendUtility::getRecord($refTable, $refUid)) { // @todo: It would be great to refactor the softref parser mess "data structure" // and let it return the reference record along the way - "db" type softrefs // fetch those already. $refTcaTableSchema = $this->tcaSchemaFactory->get($refTable); } } else { $refString = mb_substr($element['subst']['tokenValue'], 0, 1024); } $relations[] = [ 'tablename' => $tableName, 'recuid' => (int)$record['uid'], 'field' => $fieldName, 'hidden' => $hiddenFieldValue, 'starttime' => $starttimeFieldValue, 'endtime' => $endtimeFieldValue, 't3ver_state' => (int)($record['t3ver_state'] ?? 0), 'flexpointer' => $flexPointer, 'softref_key' => (string)$softrefKey, 'softref_id' => (string)$softrefId, 'sorting' => 0, 'workspace' => $workspaceUid, 'ref_table' => $refTable, 'ref_uid' => $refUid, 'ref_field' => '', 'ref_hidden' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionDisabledField) ? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()] ?? 0) : 0, 'ref_starttime' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionStartTime) ? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()] ?? 0) : 0, 'ref_endtime' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionEndTime) ? (int)(($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()] ?? 0) ?: 2147483647) : 2147483647, 'ref_t3ver_state' => (int)($refRecord['t3ver_state'] ?? 0), 'ref_sorting' => 0, 'ref_string' => $refString, ]; } } } } } return $relations; } /** * RelationHandler does not return relation record details when dealing with * inline foreign_table relations. We need fields like hidden and starrtime, * though. Fetch them now. */ private function enrichInlineRelations(string $tableName, array $itemArray): array { $selectFields = ['uid']; $tableTcaSchema = $this->tcaSchemaFactory->get($tableName); if ($tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) { $selectFields[] = $tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(); } if ($tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) { $selectFields[] = $tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName(); } if ($tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) { $selectFields[] = $tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName(); } if ($tableTcaSchema->isWorkspaceAware()) { $selectFields[] = 't3ver_state'; } if (count($selectFields) === 1) { return $itemArray; } $connection = $this->connectionPool->getConnectionForTable($tableName); $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform()); $queryBuilder = $connection->createQueryBuilder(); $queryBuilder->getRestrictions()->removeAll(); $rows = []; $uidList = array_column($itemArray, 'id'); foreach (array_chunk($uidList, $maxBindParameters - 10, true) as $chunk) { $result = $queryBuilder->select(...$selectFields)->from($tableName) ->where( $queryBuilder->expr()->in( 'uid', $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY) ) ) ->orderBy('uid', 'ASC')->executeQuery(); while ($row = $result->fetchAssociative()) { $rows[(int)$row['uid']] = $row; } } foreach ($itemArray as &$item) { if (isset($rows[$item['id']])) { // @todo: The isset() prevents a PHP array access warning here. It seems this can happen with // inline CSV since RelationHandler->realList() does not verify if attached records // really exist. There is probably a deeper issue with CSV lists here, see #106428 for // more information and reproduce. This area should have a closer look, it looks as if // "count" value instead of uid fields are hand over here - at least with tx_styleguide_inline_11. $item = array_merge($item, $rows[$item['id']]); } } return $itemArray; } private function getRelationsFromFlexData(string $tableName, string $fieldName, array $row, int $workspaceUid): array { $valueArray = GeneralUtility::xml2array($row[$fieldName] ?? ''); if (!is_array($valueArray)) { // Current flex form values can not be parsed to an array. No relations. return []; } try { $tableTcaSchema = $this->tcaSchemaFactory->get($tableName); $fieldConfig['config'] = $tableTcaSchema->getField($fieldName)->getConfiguration(); $dataStructureArray = $this->flexFormTools->parseDataStructureByIdentifier( $this->flexFormTools->getDataStructureIdentifier($fieldConfig, $tableName, $fieldName, $row, $tableTcaSchema), $tableTcaSchema ); } catch (InvalidIdentifierException) { // Data structure can not be resolved or parsed. No relations. return []; } if (!is_array($dataStructureArray['sheets'] ?? false)) { // No sheet in DS. Shouldn't happen, though. return []; } $flexRelations = []; foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) { foreach (($sheetData['ROOT']['el'] ?? []) as $sheetElementKey => $sheetElementTca) { // For all elements allowed in Data Structure. if (($sheetElementTca['type'] ?? '') === 'array') { // This is a section. if (!is_array($sheetElementTca['el'] ?? false) || !is_array($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] ?? false)) { // No possible containers defined for this section in DS, or no values set for this section. continue; } foreach ($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] as $valueSectionContainerKey => $valueSectionContainers) { // We have containers for this section in values. if (!is_array($valueSectionContainers ?? false)) { // Values don't validate to an array, skip. continue; } foreach ($valueSectionContainers as $valueContainerType => $valueContainerElements) { // For all value containers in this section. if (!is_array($sheetElementTca['el'][$valueContainerType]['el'] ?? false)) { // There is no DS for this container type, skip. continue; } foreach ($sheetElementTca['el'][$valueContainerType]['el'] as $containerElement => $containerElementTca) { // Container type of this value container exists in DS. Iterate DS container to find value relations. if (isset($valueContainerElements['el'][$containerElement]['vDEF'])) { $fieldValue = $valueContainerElements['el'][$containerElement]['vDEF']; $structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/el/' . $valueSectionContainerKey . '/' . $valueContainerType . '/el/' . $containerElement . '/vDEF/'; if ($fieldValue !== '' && ($containerElementTca['config']['softref'] ?? '') !== '') { $tokenizedContent = $fieldValue; foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($containerElementTca['config']['softref']) as $softReferenceParser) { $parserResult = $softReferenceParser->parse($tableName, $fieldName, (int)$row['uid'], $fieldValue, $structurePath); if ($parserResult->hasMatched()) { $flexRelations['softrefs'][$structurePath]['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements(); if ($parserResult->hasContent()) { $tokenizedContent = $parserResult->getContent(); } } } if (!empty($flexRelations['softrefs'][$structurePath]) && $fieldValue !== $tokenizedContent) { $flexRelations['softrefs'][$structurePath]['tokenizedContent'] = $tokenizedContent; } } } } } } } elseif (isset($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'])) { // Not a section but a simple field. Get its relations. $fieldValue = $valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF']; $structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/vDEF/'; $databaseRelations = $this->getRelationsFromRelationField($tableName, $fieldValue, $sheetElementTca['config'] ?? [], (int)$row['uid'], $workspaceUid, $row); if (!empty($databaseRelations)) { $flexRelations['db'][$structurePath] = $databaseRelations; } if ($fieldValue !== '' && ($sheetElementTca['config']['softref'] ?? '') !== '') { $tokenizedContent = $fieldValue; foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($sheetElementTca['config']['softref']) as $softReferenceParser) { $parserResult = $softReferenceParser->parse($tableName, $fieldName, (int)$row['uid'], $fieldValue, $structurePath); if ($parserResult->hasMatched()) { $flexRelations['softrefs'][$structurePath]['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements(); if ($parserResult->hasContent()) { $tokenizedContent = $parserResult->getContent(); } } } if (!empty($flexRelations['softrefs'][$structurePath]) && $fieldValue !== $tokenizedContent) { $flexRelations['softrefs'][$structurePath]['tokenizedContent'] = $tokenizedContent; } } } } } return $flexRelations; } /** * Check field configuration if it is a DB relation field and extract DB relations if any */ private function getRelationsFromRelationField(string $tableName, mixed $fieldValue, array $conf, int $uid, int $workspaceUid, array $row): array { if (empty($conf)) { return []; } if (($conf['type'] === 'inline' || $conf['type'] === 'file') && !empty($conf['foreign_table']) && empty($conf['MM'])) { $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class); $dbAnalysis->setUseLiveReferenceIds(false); $dbAnalysis->setWorkspaceId($workspaceUid); $dbAnalysis->start($fieldValue, $conf['foreign_table'], '', $uid, $tableName, $conf); return $dbAnalysis->itemArray; } if ($this->isDbReferenceField($conf)) { $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table']; if ($conf['MM_opposite_field'] ?? false) { // Never handle sys_refindex when looking at MM from foreign side return []; } $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class); $dbAnalysis->setWorkspaceId($workspaceUid); $dbAnalysis->start($fieldValue, $allowedTables, $conf['MM'] ?? '', $uid, $tableName, $conf); $itemArray = $dbAnalysis->itemArray; if (ExtensionManagementUtility::isLoaded('workspaces') && $workspaceUid > 0 && !empty($conf['MM'] ?? '') // Catch type=group 'allowed' and type=select 'foreign_table' MM scenarios && (!empty($conf['allowed'] ?? '') || !empty($conf['foreign_table'] ?? '')) && empty($conf['MM_opposite_field'] ?? '') && (int)($row['t3ver_wsid'] ?? 0) === 0 ) { // When dealing with local side mm relations in workspace 0, there may be workspace records on the foreign // side, for instance when those got an additional category. See ManyToMany Modify addCategoryRelations test. // In those cases, the full set of relations must be written to sys_refindex as workspace rows. // But, if the relations in this workspace and live are identical, no sys_refindex workspace rows // have to be added. $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class); $dbAnalysis->setWorkspaceId(0); $dbAnalysis->start($fieldValue, $allowedTables, $conf['MM'], $uid, $tableName, $conf); $itemArrayLive = $dbAnalysis->itemArray; if ($itemArrayLive === $itemArray) { $itemArray = []; } } return $itemArray; } return []; } /** * Returns true if the TCA/columns field type is a DB reference field * * @param array $configuration Config array for TCA/columns field * @return bool TRUE if DB reference field (group/db or select with foreign-table) */ private function isDbReferenceField(array $configuration): bool { return $configuration['type'] === 'group' || ( in_array($configuration['type'], ['select', 'category', 'inline', 'file'], true) && !empty($configuration['foreign_table']) ); } /** * Returns true if the TCA/columns field may carry references. True for * group, inline and friends, for flex, and if there is a 'softref' definition. */ private function isReferenceField(FieldTypeInterface $field): bool { return $this->isDbReferenceField($field->getConfiguration()) || $field->isType(TableColumnType::FLEX) || $field->getSoftReferenceKeys() !== false ; } /** * List of TCA columns that can have relations. Typically inline, group * and friends, as well as flex fields and fields with 'softref' config. * If empty, the table can not have relations. * Uses a class cache to be quick for multiple calls on same table. */ private function getTableRelationFields(string $tableName): array { if (isset($this->tableRelationFieldCache[$tableName])) { return $this->tableRelationFieldCache[$tableName]; } if (!$this->tcaSchemaFactory->has($tableName)) { $this->tableRelationFieldCache[$tableName] = []; return []; } $tableTcaFields = $this->tcaSchemaFactory->get($tableName)->getFields(); $relationFields = []; foreach ($tableTcaFields as $field) { if ($this->isReferenceField($field)) { $relationFields[] = $field->getName(); } } $this->tableRelationFieldCache[$tableName] = $relationFields; return $relationFields; } /** * Create list of non-deleted "active" workspace uids. This contains at least 0 "live workspace". * * @return int[] */ private function getListOfActiveWorkspaces(): array { if (!ExtensionManagementUtility::isLoaded('workspaces')) { // If ext:workspaces is not loaded, "0" is the only valid one. return [0]; } $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_workspace'); // Workspaces can't be 'hidden', so we only use deleted restriction here. $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $result = $queryBuilder->select('uid')->from('sys_workspace')->orderBy('uid')->executeQuery(); // "0", plus non-deleted workspaces are active return array_merge([0 => 0], $result->fetchFirstColumn()); } /** * Helper method of updateIndex() to find number of rows in sys_refindex that * relate to a non-existing or deleted workspace record, even if workspaces is * not loaded at all, but has been loaded somewhere in the past and sys_refindex * rows have been created. */ private function getNumberOfUnusedWorkspaceRowsInReferenceIndex(array $activeWorkspaces): int { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex'); $queryBuilder->getRestrictions()->removeAll(); $numberOfInvalidWorkspaceRecords = $queryBuilder ->count('hash') ->from('sys_refindex') ->where( $queryBuilder->expr()->notIn('workspace', $queryBuilder->createNamedParameter($activeWorkspaces, Connection::PARAM_INT_ARRAY)) ) ->executeQuery() ->fetchOne(); return (int)$numberOfInvalidWorkspaceRecords; } /** * Delete sys_refindex rows of deleted / not existing workspace records, or all if ext:workspace is not loaded. */ private function removeUnusedWorkspaceRowsFromReferenceIndex(array $activeWorkspaces): int { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex'); $queryBuilder->getRestrictions()->removeAll(); return $queryBuilder ->delete('sys_refindex') ->where( $queryBuilder->expr()->notIn('workspace', $queryBuilder->createNamedParameter($activeWorkspaces, Connection::PARAM_INT_ARRAY)) ) ->executeStatement(); } /** * When a TCA table with references has been removed, there may be old sys_refindex * rows for it. The query finds the number of affected rows. */ private function getNumberOfUnusedTablesInReferenceIndex(array $tableNames): int { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex'); $queryBuilder->getRestrictions()->removeAll(); $numberOfRowsOfUnusedTables = $queryBuilder ->count('hash') ->from('sys_refindex') ->where( $queryBuilder->expr()->notIn('tablename', $queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY)) ) ->executeQuery() ->fetchOne(); return (int)$numberOfRowsOfUnusedTables; } /** * When a TCA table with references has been removed, there may be old sys_refindex * rows for it. The query deletes those. */ private function removeReferenceIndexDataFromUnusedDatabaseTables(array $tableNames): int { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex'); $queryBuilder->getRestrictions()->removeAll(); return $queryBuilder ->delete('sys_refindex') ->where( $queryBuilder->expr()->notIn('tablename', $queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY)) ) ->executeStatement(); } /** * Checks if a given table should be excluded from ReferenceIndex */ private function shouldExcludeTableFromReferenceIndex(string $tableName): bool { if (isset($this->excludedTables[$tableName])) { return $this->excludedTables[$tableName]; } $event = new IsTableExcludedFromReferenceIndexEvent($tableName); $event = $this->eventDispatcher->dispatch($event); $this->excludedTables[$tableName] = $event->isTableExcluded(); return $this->excludedTables[$tableName]; } }