-08.png` when it exists (slot 08 = * page media / menu tile; the number in the picture doubles as the image * wish list towards the agency), else the page's own hero background * (`vitec_bgimage`, same file referenced - 2026-11-02, for the sub-market / * sub-solution cards of the megamenu `rail` display), otherwise the generic * wide grey placeholder. A page that already has a media reference is NEVER * touched - editors swap the image in the page properties. * * --depth=2 targets the grandchildren (e.g. the sub-markets below * /markets/) instead of the children; --depth=3 reaches through two * levels (sysfolders pass as intermediate parents, so the product overview * pages below /products > category folder > group folder are covered). * * vendor/bin/typo3 vitec:attach-page-media --root=/support * vendor/bin/typo3 vitec:attach-page-media --root=/markets --depth=2 --apply * vendor/bin/typo3 vitec:attach-page-media --root=/products --depth=3 --apply */ #[AsCommand( name: 'vitec:attach-page-media', description: 'Attach placeholder page media to the children of a root page (dry-run by default)' )] final class AttachPageMediaCommand extends Command { protected function configure(): void { $this->addOption('root', null, InputOption::VALUE_REQUIRED, 'Slug of the root page, e.g. /support'); $this->addOption('depth', null, InputOption::VALUE_REQUIRED, '1 = children of the root, 2 = their children, 3 = one level further', '1'); $this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)'); } protected function execute(InputInterface $input, OutputInterface $output): int { Bootstrap::initializeBackendAuthentication(); $apply = (bool)$input->getOption('apply'); $rootSlug = '/' . trim((string)$input->getOption('root'), '/ '); $pool = GeneralUtility::makeInstance(ConnectionPool::class); $pages = $pool->getConnectionForTable('pages'); $root = $pages->fetchAssociative('SELECT uid, title FROM pages WHERE deleted = 0 AND slug = ? AND sys_language_uid IN (0,-1)', [$rootSlug]); if ($root === null || $root === false) { $output->writeln('Root page ' . $rootSlug . ' not found.'); return Command::FAILURE; } $depth = max(1, min(3, (int)$input->getOption('depth'))); $parents = [(int)$root['uid']]; for ($level = 1; $level <= $depth; $level++) { // intermediate levels pass through sysfolders (254); only real // pages (doktype 1) are targets on the final level $doktypes = $level < $depth ? '1, 254' : '1'; $children = $pages->fetchAllAssociative( 'SELECT uid, pid, title, doktype FROM pages WHERE deleted = 0 AND hidden = 0 AND doktype IN (' . $doktypes . ') AND pid IN (' . implode(',', $parents ?: [0]) . ') AND sys_language_uid IN (0,-1) ORDER BY pid, sorting' ); $parents = array_map('intval', array_column($children, 'uid')); } $output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . sprintf(' · %s #%d · depth %d · %d page(s)', $rootSlug, (int)$root['uid'], $depth, count($children))); $storage = GeneralUtility::makeInstance(StorageRepository::class)->getDefaultStorage(); $refs = $pool->getConnectionForTable('sys_file_reference'); $datamap = []; $newIndex = 0; foreach ($children as $child) { $uid = (int)$child['uid']; $existing = (int)$refs->fetchOne( "SELECT COUNT(*) FROM sys_file_reference WHERE deleted = 0 AND tablenames = 'pages' AND fieldname = 'media' AND uid_foreign = ?", [$uid] ); if ($existing > 0) { $output->writeln(sprintf(' = #%d "%s": has media (%d) - untouched', $uid, (string)$child['title'], $existing)); continue; } $fileUid = 0; $identifier = 'placeholders/pages/' . $uid . '-08.png'; if (!$storage->hasFile($identifier)) { // the page's own hero background, first hero on the page $fileUid = (int)$refs->fetchOne( "SELECT r.uid_local FROM sys_file_reference r JOIN tt_content c ON c.uid = r.uid_foreign WHERE r.deleted = 0 AND r.tablenames = 'tt_content' AND r.fieldname = 'vitec_bgimage' AND c.deleted = 0 AND c.hidden = 0 AND c.pid = ? ORDER BY c.colPos, c.sorting, r.sorting_foreign LIMIT 1", [$uid] ); $identifier = $fileUid > 0 ? 'hero image (sys_file #' . $fileUid . ')' : 'placeholders/placeholder-grey-wide.png'; if ($fileUid === 0 && !$storage->hasFile($identifier)) { $output->writeln(sprintf(' ! #%d "%s": no placeholder file - skipped', $uid, (string)$child['title'])); continue; } } $output->writeln(sprintf(' + #%d "%s": attach %s', $uid, (string)$child['title'], $identifier)); if (!$apply) { continue; } if ($fileUid === 0) { $fileUid = $storage->getFile($identifier)->getUid(); } $datamap['sys_file_reference']['NEWPM' . (++$newIndex)] = [ 'pid' => $uid, 'table_local' => 'sys_file', 'uid_local' => $fileUid, 'tablenames' => 'pages', 'uid_foreign' => $uid, 'fieldname' => 'media', ]; $datamap['pages'][$uid] = ['media' => 1]; } if (!$apply) { $output->writeln("\nDRY RUN - nothing written. Add --apply to write."); return Command::SUCCESS; } if ($datamap === []) { $output->writeln('Nothing to attach.'); return Command::SUCCESS; } $dataHandler = GeneralUtility::makeInstance(DataHandler::class); $dataHandler->start($datamap, []); $dataHandler->process_datamap(); if ($dataHandler->errorLog !== []) { $output->writeln('DataHandler: ' . implode(' | ', $dataHandler->errorLog) . ''); return Command::FAILURE; } $output->writeln('Done. Flush the frontend cache; the menu picks the images up.'); return Command::SUCCESS; } }