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;
}
}