Files
VITEC-website/packages/vitec/Classes/Command/AttachPageMediaCommand.php
T
orascheandClaude Fable 5 77661bf0b6 Datapath stories, grid repairs, menu v2 pure, hero placeholders, fec test
Batch of 2026-10-28/29 plus 2026-11-03:

- NEW vitec:import-datapath-stories: 22 story packages from the agency
  dump (docx + Links/ assets) as usecase records with content, quotes,
  images, customer logos, download cards; --repair-quotes normalizes
  attribution lines; asset backfill for existing stories
- /products grid incident repaired (fix_products_grid, fix_graphics_grid,
  check_grid_page): containers unhidden, 46 duplicate cards soft-deleted,
  section order restored; BuildProductPortfolio and
  ImportProductCategoryPages now GUARD against re-runs on pages that
  already carry vitec_cols_% containers
- ' double-encoding fixed: render-pipeline importers write literal
  apostrophes (ENT_NOQUOTES), fix_double_entities literalizes existing rows
- ticket quick wins (ticket_quickwins_20261028), broadcast market page
  cleanup, AttachCardImages market-card handling, SubmarketWorkbook
  META REPAIR, story content/video fixes (fix_story_*)
- ProvideProductCatalog: transitional v1 fields removed (categories
  uid/link, legacyGroupFields, item products teaser) - pure v2 shape
  (ticket 0024-FE); search renderer keeps its teaser
- BuildProductOverviews: heroes get the numbered placeholder background
  (placeholders/pages/<uid>-01.png, wide grey fallback) - backfill on
  existing pages with empty hero_bgimage, attached at creation for new
  pages; existing references always win (ticket 0026-FE); 40 placeholder
  PNGs shipped to fileadmin (not tracked)
- AttachPageMedia: --depth=3 with sysfolders as pass-through parents for
  the overview pages' -08 menu tiles
- NEW migrations/fec_isolation_test.sh: two-user live test proving
  fe_comment ticket isolation (backend verified tight, cross-user
  sighting traced to the FE page-JSON cache, ticket 0027-BE)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-11-03 18:29:56 +01:00

149 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Attaches a page-media image to every child page of a root that has none
* yet. The menu uses page media as tile/card visuals (megamenu `pagetree`
* column images and `spotlight` nav images, 2026-10-27), so a branch
* becomes a visual panel the moment its pages carry an image.
*
* Image per page: the numbered placeholder
* `fileadmin/placeholders/pages/<pid>-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/<market>) 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('<error>Root page ' . $rootSlug . ' not found.</error>');
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(' <comment>! #%d "%s": no placeholder file - skipped</comment>', $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('<error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
return Command::FAILURE;
}
$output->writeln('<info>Done. Flush the frontend cache; the menu picks the images up.</info>');
return Command::SUCCESS;
}
}