2 declare(strict_types = 1);
18 use Symfony\Component\Console\Command\Command;
19 use Symfony\Component\Console\Input\InputInterface;
20 use Symfony\Component\Console\Input\InputOption;
21 use Symfony\Component\Console\Output\OutputInterface;
22 use Symfony\Component\Console\Style\SymfonyStyle;
41 ->setDescription(
'Find and delete records that have lost their connection with the page tree.')
42 ->setHelp(
'Assumption: All actively used records on the website from TCA configured tables are located in the page tree exclusively.
44 All records managed by TYPO3 via the TCA array configuration has to belong to a page in the page tree, either directly or indirectly as a version of another record.
45 VERY TIME, CPU and MEMORY intensive operation since the full page tree is looked up!
47 Automatic Repair of Errors:
48 - Silently deleting the orphaned records. In theory they should not be used anywhere in the system, but there could be references. See below for more details on this matter.
50 Manual repair suggestions:
51 - Possibly re-connect orphaned records to page tree by setting their "pid" field to a valid page id. A lookup in the sys_refindex table can reveal if there are references to a orphaned record. If there are such references (from records that are not themselves orphans) you might consider to re-connect the record to the page tree, otherwise it should be safe to delete it.
53 If you want to get more detailed information, use the --verbose option.')
57 InputOption::VALUE_NONE,
58 'If this option is set, the records will not actually be deleted, but just the output which records would be deleted are shown'
74 $io =
new SymfonyStyle($input,
$output);
75 $io->title($this->getDescription());
77 if ($io->isVerbose()) {
78 $io->section(
'Searching the database now for orphaned records.');
82 $dryRun = $input->hasOption(
'dry-run') && $input->getOption(
'dry-run') !=
false ? true :
false;
89 foreach (array_keys(
$GLOBALS[
'TCA']) as $tableName) {
91 if (is_array($allRecords[$tableName]) && !empty($allRecords[$tableName])) {
92 $idList = $allRecords[$tableName];
95 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
96 ->getQueryBuilderForTable($tableName);
98 $result = $queryBuilder
102 $queryBuilder->expr()->notIn(
105 array_map(
'intval', $idList)
111 $rowCount = $queryBuilder->count(
'uid')->execute()->fetchColumn(0);
113 $orphans[$tableName] = [];
114 while ($orphanRecord = $result->fetch()) {
115 $orphans[$tableName][$orphanRecord[
'uid']] = $orphanRecord[
'uid'];
118 if (count($orphans[$tableName])) {
119 $io->note(
'Found ' . count($orphans[$tableName]) .
' orphan records in table "' . $tableName .
'" with following ids: ' . implode(
', ', $orphans[$tableName]));
124 if (count($orphans)) {
125 $io->section(
'Deletion process starting now.' . ($dryRun ?
' (Not deleting now, just a dry run)' :
''));
130 $io->success(
'All done!');
132 $io->success(
'No orphan records found.');
151 $allRecords[
'pages'][$pageId] = $pageId;
154 foreach (array_keys(
$GLOBALS[
'TCA']) as $tableName) {
155 if ($tableName !==
'pages') {
157 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
158 ->getQueryBuilderForTable($tableName);
160 $queryBuilder->getRestrictions()->removeAll();
162 $result = $queryBuilder
166 $queryBuilder->expr()->eq(
168 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
173 while ($rowSub = $result->fetch()) {
174 $allRecords[$tableName][$rowSub[
'uid']] = $rowSub[
'uid'];
177 if (is_array($versions)) {
178 foreach ($versions as $verRec) {
179 if (!$verRec[
'_CURRENT_VERSION']) {
180 $allRecords[$tableName][$verRec[
'uid']] = $verRec[
'uid'];
190 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
191 ->getQueryBuilderForTable(
'pages');
193 $queryBuilder->getRestrictions()->removeAll();
195 $result = $queryBuilder
199 $queryBuilder->expr()->eq(
201 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
207 while ($row = $result->fetch()) {
215 if (is_array($versions)) {
216 foreach ($versions as $verRec) {
217 if (!$verRec[
'_CURRENT_VERSION']) {
233 protected function deleteRecords(array $orphanedRecords,
bool $dryRun, SymfonyStyle $io)
236 if (isset($orphanedRecords[
'pages'])) {
237 $_pages = $orphanedRecords[
'pages'];
238 unset($orphanedRecords[
'pages']);
240 $orphanedRecords[
'pages'] = array_reverse($_pages);
244 $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
245 $dataHandler->start([], []);
248 foreach ($orphanedRecords as $table => $list) {
249 if ($io->isVerbose()) {
250 $io->writeln(
'Flushing ' . count($list) .
' orphaned records from table "' . $table .
'"');
252 foreach ($list as $uid) {
253 if ($io->isVeryVerbose()) {
254 $io->writeln(
'Flushing record "' . $table .
':' . $uid .
'"');
260 $dataHandler->deleteRecord($table, $uid,
true,
true);
262 if (!empty($dataHandler->errorLog)) {
263 $errorMessage = array_merge([
'DataHandler reported an error'], $dataHandler->errorLog);
264 $io->error($errorMessage);
265 } elseif (!$io->isQuiet()) {
266 $io->writeln(
'Permanently deleted orphaned record "' . $table .
':' . $uid .
'".');