addOption('page', null, InputOption::VALUE_REQUIRED, 'Slug of the page, e.g. /products'); $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'); $slug = '/' . trim((string)$input->getOption('page'), '/ '); $pool = GeneralUtility::makeInstance(ConnectionPool::class); $pages = $pool->getConnectionForTable('pages'); $content = $pool->getConnectionForTable('tt_content'); $page = $pages->fetchAssociative('SELECT uid, title FROM pages WHERE deleted = 0 AND slug = ? AND sys_language_uid IN (0,-1)', [$slug]); if (!$page) { $output->writeln('Page ' . $slug . ' not found.'); return Command::FAILURE; } $pageUid = (int)$page['uid']; $elements = $content->fetchAllAssociative( 'SELECT uid, CType, header, hidden FROM tt_content WHERE deleted = 0 AND pid = ? AND colPos = ? AND tx_container_parent = 0 AND sys_language_uid IN (0,-1) ORDER BY sorting', [$pageUid, self::COLPOS_MAIN] ); $output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . sprintf(' · %s #%d · %d top-level element(s) in main', $slug, $pageUid, count($elements))); // --- parse into an ordered plan ------------------------------------------------- // order entries: ['el', uid] | ['section', ['intro'=>?uid,'cards'=>[uids]]] // Containers go into ONE global pool: a half-applied earlier run may // have left empty grids anywhere on the page - every card gets an // explicit column assignment anyway, so the shells are interchangeable. $order = []; $section = null; $pool = []; $flush = function () use (&$order, &$section): void { if ($section !== null) { $order[] = ['section', $section]; $section = null; } }; foreach ($elements as $element) { $uid = (int)$element['uid']; $ctype = (string)$element['CType']; if ((int)$element['hidden'] === 1) { continue; // hidden legacy stays untouched and unsequenced } if ($ctype === 'vitec_introparagraph') { $flush(); $section = ['intro' => $uid, 'cards' => []]; continue; } if ($ctype === 'vitec_card') { if ($section === null) { $section = ['intro' => null, 'cards' => []]; } $section['cards'][] = $uid; continue; } if ($ctype === self::CONTAINER) { $pool[] = $uid; $children = $content->fetchAllAssociative( "SELECT uid FROM tt_content WHERE deleted = 0 AND hidden = 0 AND tx_container_parent = ? AND CType = 'vitec_card' ORDER BY colPos, sorting", [$uid] ); if ($children !== []) { if ($section === null) { $section = ['intro' => null, 'cards' => []]; } foreach ($children as $child) { $section['cards'][] = (int)$child['uid']; } } continue; } $flush(); $order[] = ['el', $uid]; } $flush(); // --- plan containers per section -------------------------------------------------- $datamap = []; $newIndex = 0; $newContainers = []; $sequenceTop = []; $cardAssignments = []; // containerId(string) => [cardUid,...] max 3 $hiddenContainers = 0; foreach ($order as [$type, $value]) { if ($type === 'el') { $sequenceTop[] = (string)$value; continue; } $sectionPlan = $value; if ($sectionPlan['intro'] !== null) { $sequenceTop[] = (string)$sectionPlan['intro']; } $cards = $sectionPlan['cards']; $needed = (int)ceil(count($cards) / 3); for ($i = 0; $i < $needed; $i++) { $containerId = array_shift($pool); if ($containerId !== null) { $containerId = (string)$containerId; } else { $containerId = 'NEWGRID' . (++$newIndex); $newContainers[$containerId] = ['pid' => $pageUid, 'CType' => self::CONTAINER, 'colPos' => self::COLPOS_MAIN]; } $sequenceTop[] = $containerId; $cardAssignments[$containerId] = array_slice($cards, $i * 3, 3); } $intro = $sectionPlan['intro'] !== null ? '#' . $sectionPlan['intro'] : '(no intro)'; $output->writeln(sprintf(' section %s: %d card(s) -> %d container(s)', $intro, count($cards), $needed)); } foreach ($pool as $surplus) { $datamap['tt_content'][(string)(int)$surplus]['hidden'] = 1; $hiddenContainers++; } if ($hiddenContainers > 0) { $output->writeln(' ' . $hiddenContainers . ' surplus grid container(s) hidden'); } $output->writeln(sprintf(' plan: %d new container(s), %d card(s) distributed', count($newContainers), array_sum(array_map('count', $cardAssignments)))); if (!$apply) { $output->writeln("\nDRY RUN - nothing written. Add --apply to write."); return Command::SUCCESS; } // pass A: create missing containers $idMap = []; if ($newContainers !== []) { $dhA = GeneralUtility::makeInstance(DataHandler::class); $dhA->start(['tt_content' => $newContainers], []); $dhA->process_datamap(); if ($dhA->errorLog !== []) { $output->writeln('DataHandler containers: ' . implode(' | ', $dhA->errorLog) . ''); return Command::FAILURE; } foreach ($newContainers as $newId => $fields) { $idMap[$newId] = (int)($dhA->substNEWwithIDs[$newId] ?? 0); if ($idMap[$newId] <= 0) { $output->writeln('Container ' . $newId . ' was not created - aborting before any move.'); return Command::FAILURE; } } } $resolve = static fn($id): int => str_starts_with((string)$id, 'NEWGRID') ? (int)($idMap[(string)$id] ?? 0) : (int)$id; // pass B: move the cards into the container columns foreach ($cardAssignments as $containerId => $cards) { $containerUid = $resolve($containerId); if ($containerUid <= 0) { continue; } foreach ($cards as $i => $cardUid) { $datamap['tt_content'][(string)$cardUid] = [ 'colPos' => self::COLS[$i % 3], 'tx_container_parent' => $containerUid, ]; } } if ($datamap !== []) { try { $dhB = GeneralUtility::makeInstance(DataHandler::class); $dhB->start($datamap, []); $dhB->process_datamap(); } catch (\Throwable $e) { $output->writeln('Move pass threw: ' . get_class($e) . ' - ' . $e->getMessage() . ''); return Command::FAILURE; } if ($dhB->errorLog !== []) { $output->writeln('DataHandler cards: ' . implode(' | ', $dhB->errorLog) . ''); } } // top-level order $sorting = 0; foreach ($sequenceTop as $id) { $uid = $resolve($id); if ($uid > 0) { $sorting += 256; $content->update('tt_content', ['sorting' => $sorting], ['uid' => $uid]); } } // card sorting inside the columns (one card per column and container) foreach ($cardAssignments as $containerId => $cards) { foreach ($cards as $cardUid) { $content->update('tt_content', ['sorting' => 256], ['uid' => (int)$cardUid]); } } $left = (int)$content->fetchOne( "SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = ? AND CType = 'vitec_card' AND tx_container_parent = 0 AND colPos = ?", [$pageUid, self::COLPOS_MAIN] ); if ($left > 0) { $output->writeln('' . $left . ' card(s) are STILL top-level - inspect with migrations/check_grid_447.php'); return Command::FAILURE; } $output->writeln('Done - every card sits in a grid container. Flush the frontend cache.'); return Command::SUCCESS; } }