addOption('dry-run', null, InputOption::VALUE_NONE, 'Report the candidates - delete nothing'); } protected function execute(InputInterface $input, OutputInterface $output): int { Bootstrap::initializeBackendAuthentication(); $dryRun = (bool)$input->getOption('dry-run'); // Level-2 category titles under root "Product" (parent chain root->J->K). $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_category'); $rootUid = (int)$connection->fetchOne( "SELECT uid FROM sys_category WHERE deleted = 0 AND parent = 0 AND title = 'Product'" ); $level1 = $connection->fetchFirstColumn( 'SELECT uid FROM sys_category WHERE deleted = 0 AND parent = ?', [$rootUid] ); if ($level1 === []) { $output->writeln('No level-1 product categories found.'); return Command::FAILURE; } $placeholders = implode(',', array_fill(0, count($level1), '?')); $catTitleByUid = []; foreach ($connection->fetchAllAssociative( "SELECT uid, title FROM sys_category WHERE deleted = 0 AND parent IN ($placeholders)", array_map('intval', $level1) ) as $row) { $catTitleByUid[(int)$row['uid']] = (string)$row['title']; } // Products with their category assignments and the content probe. $productConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::TABLE); $products = $productConnection->fetchAllAssociative( 'SELECT uid, title, slug, subproduct, teaser, description, capabilities FROM ' . self::TABLE . ' WHERE deleted = 0' ); $mm = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_category_record_mm') ->fetchAllAssociative( "SELECT uid_local, uid_foreign FROM sys_category_record_mm WHERE tablenames = ? AND fieldname = ?", [self::TABLE, 'categories'] ); $catsByProduct = []; foreach ($mm as $row) { $catsByProduct[(int)$row['uid_foreign']][] = (int)$row['uid_local']; } $delete = []; foreach ($products as $product) { $uid = (int)$product['uid']; $isK = false; foreach ($catsByProduct[$uid] ?? [] as $catUid) { if (($catTitleByUid[$catUid] ?? null) === (string)$product['title']) { $isK = true; } } if (!$isK || (int)$product['subproduct'] === 1) { continue; } if (in_array($uid, self::KEEP, true)) { $output->writeln(sprintf('keep %d %s - carries imported workbook content (delete manually once its family page exists)', $uid, $product['title'])); continue; } $hasContent = trim((string)$product['teaser']) !== '' || trim((string)$product['description']) !== '' || trim((string)$product['capabilities']) !== ''; if ($hasContent) { $output->writeln(sprintf('skip %d %s - has content, review manually', $uid, $product['title'])); continue; } $delete[] = $uid; $output->writeln(sprintf('delete %d %s (/%s)', $uid, $product['title'], $product['slug'])); } if ($delete === []) { $output->writeln('No empty family landing records found - nothing to delete.'); return Command::SUCCESS; } if ($dryRun) { $output->writeln(sprintf('DRY RUN - %d record(s) would be deleted.', count($delete))); return Command::SUCCESS; } $cmdmap = []; foreach ($delete as $uid) { $cmdmap[self::TABLE][$uid]['delete'] = 1; } $dataHandler = GeneralUtility::makeInstance(DataHandler::class); $dataHandler->start([], $cmdmap); $dataHandler->process_cmdmap(); if ($dataHandler->errorLog !== []) { foreach ($dataHandler->errorLog as $error) { $output->writeln('' . $error . ''); } return Command::FAILURE; } $output->writeln(sprintf('%d record(s) deleted. Flush the cache: vendor/bin/typo3 cache:flush', count($delete))); return Command::SUCCESS; } }