- vitec:align-solutions: declarative target = the 39 Generic Solutions in 8 group categories under the Solution root (sheet is binding, group typo Engagament->Engagement fixed). Two legacy records renamed onto their sheet names instead of deleted (keeps the only editorial teaser/description), slugs generated for all, market categories stripped from solutions, the ~82 market-specific records soft-deleted (markets link editorially) - vitec:map-solution-pages: writes matching page uids into detail_page (unique matches, fill-only); disambiguates the duplicated page branches by slug suffix and prunes the 10 empty duplicate pages - read-only diagnostics: check_solutions.php, check_dup_solution_pages.php
301 lines
13 KiB
PHP
301 lines
13 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\Utility\GeneralUtility;
|
|
|
|
/**
|
|
* DECLARATIVE alignment of the solution records with the "Generic Solutions"
|
|
* tab of the merged sitemap - the binding target per decision 2026-09-04
|
|
* (Olli, after the interim state briefly held 121 records): the solution
|
|
* table contains EXACTLY these 39 solutions in 8 thematic groups. Market-
|
|
* specific solutions are NOT records; the market connection is editorial
|
|
* (solution lists on market pages).
|
|
*
|
|
* What a run does (idempotent, --dry-run):
|
|
* 1. ensures the 8 group categories under the root category "Solution"
|
|
* (group typo "Engagament" corrected to "Engagement")
|
|
* 2. renames two legacy records onto their sheet names instead of
|
|
* deleting them:
|
|
* "Real-time Data Monitoring (CCTV, sensors)" -> "(CCTV, IoT, sensors)"
|
|
* "Passenger Information Displays (Digital Signage)" -> "Passenger / Public Information Displays"
|
|
* (the latter keeps the only editorial teaser/description in the table)
|
|
* 3. creates missing target solutions (title + slug)
|
|
* 4. sets each target's categories to exactly its group (market categories
|
|
* are stripped from solutions - they stay in place for other models)
|
|
* 5. regenerates empty/stale slugs from the (possibly new) title
|
|
* 6. soft-DELETES every solution record whose title is not a target
|
|
* (DataHandler delete - restorable via recycler)
|
|
*
|
|
* vendor/bin/typo3 vitec:align-solutions --dry-run
|
|
* vendor/bin/typo3 vitec:align-solutions
|
|
*/
|
|
#[AsCommand(
|
|
name: 'vitec:align-solutions',
|
|
description: 'Align solution records to EXACTLY the 39 sitemap Generic Solutions in 8 groups (prunes the rest)'
|
|
)]
|
|
final class AlignSolutionsCommand extends Command
|
|
{
|
|
private const TABLE = 'tx_vitec_domain_model_solution';
|
|
|
|
/** Binding target: group => solutions, verbatim from the sheet (typo fixed). */
|
|
private const TARGET = [
|
|
'Control Room & Command Centre Solutions' => [
|
|
'Control Room Platforms (SOC / NOC / Command Centres)',
|
|
'Command & Control Visualisation',
|
|
'Operations Management Environments',
|
|
'Crisis & Incident Management Centres',
|
|
],
|
|
'Real-time Monitoring & Situational Awareness' => [
|
|
'Multi-source Video Monitoring',
|
|
'Real-time Data Monitoring (CCTV, IoT, sensors)',
|
|
'Situational Awareness Platforms',
|
|
'Surveillance & Operational Visibility',
|
|
'Incident Detection & Response',
|
|
],
|
|
'Video Distribution & Streaming' => [
|
|
'IPTV Distribution',
|
|
'AV-over-IP Distribution',
|
|
'Secure Video Streaming (low latency)',
|
|
'Multi-site Video Delivery',
|
|
'Internal & External Broadcast Distribution',
|
|
],
|
|
'Video Capture, Encoding & Processing' => [
|
|
'Video Encoding / Decoding',
|
|
'Live Video Capture',
|
|
'Signal Processing & Conversion',
|
|
'Transcoding & Optimisation',
|
|
'Contribution & Remote Production',
|
|
],
|
|
'Video Wall & Data Visualisation' => [
|
|
'Video Wall Platforms',
|
|
'Multi-display Visualisation',
|
|
'Real-time Dashboards',
|
|
'Data Aggregation & Display',
|
|
'Operational Intelligence Displays',
|
|
],
|
|
'Digital Signage & Content Delivery' => [
|
|
'Digital Signage Networks',
|
|
'Passenger / Public Information Displays',
|
|
'Wayfinding & Messaging',
|
|
'Advertising & Sponsorship Displays',
|
|
'Targeted Content Delivery',
|
|
],
|
|
'Enterprise Communications & Engagement' => [
|
|
'Internal Communications (IPTV)',
|
|
'Staff Messaging Systems',
|
|
'Corporate Broadcasting',
|
|
'Guest / Visitor Engagement',
|
|
'Multi-location Communication Networks',
|
|
],
|
|
'Security & Surveillance Solutions' => [
|
|
'Surveillance & CCTV Integration',
|
|
'Security Operations Centres (SOC)',
|
|
'Perimeter Monitoring',
|
|
'Incident Response Systems',
|
|
'Secure Monitoring Environments',
|
|
],
|
|
];
|
|
|
|
/** Legacy record title => target title (rename instead of delete+create). */
|
|
private const RENAMES = [
|
|
'Real-time Data Monitoring (CCTV, sensors)' => 'Real-time Data Monitoring (CCTV, IoT, sensors)',
|
|
'Passenger Information Displays (Digital Signage)' => 'Passenger / Public Information Displays',
|
|
];
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Report the plan - write nothing');
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
Bootstrap::initializeBackendAuthentication();
|
|
$dryRun = (bool)$input->getOption('dry-run');
|
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::TABLE);
|
|
|
|
$solutionRoot = (int)$connection->fetchOne(
|
|
"SELECT uid FROM sys_category WHERE deleted = 0 AND parent = 0 AND title = 'Solution'"
|
|
);
|
|
if ($solutionRoot <= 0) {
|
|
$output->writeln('<error>Root category "Solution" not found.</error>');
|
|
return Command::FAILURE;
|
|
}
|
|
$categoryPid = (int)$connection->fetchOne('SELECT pid FROM sys_category WHERE uid = ' . $solutionRoot);
|
|
|
|
// ---- 1. group categories ------------------------------------------
|
|
$groupCats = [];
|
|
$groupDatamap = [];
|
|
$index = 0;
|
|
$newGroupIds = [];
|
|
foreach (array_keys(self::TARGET) as $group) {
|
|
$uid = (int)$connection->fetchOne(
|
|
'SELECT uid FROM sys_category WHERE deleted = 0 AND parent = ? AND title = ?',
|
|
[$solutionRoot, $group]
|
|
);
|
|
if ($uid > 0) {
|
|
$groupCats[$group] = $uid;
|
|
} else {
|
|
$newId = 'NEW' . ++$index;
|
|
$newGroupIds[$newId] = $group;
|
|
$groupDatamap['sys_category'][$newId] = ['pid' => $categoryPid, 'parent' => $solutionRoot, 'title' => $group];
|
|
$output->writeln(($dryRun ? 'plan ' : 'create') . ' Gruppe: ' . $group);
|
|
}
|
|
}
|
|
if (!$dryRun && $groupDatamap !== []) {
|
|
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
|
$dataHandler->start($groupDatamap, []);
|
|
$dataHandler->process_datamap();
|
|
foreach ($newGroupIds as $newId => $group) {
|
|
$groupCats[$group] = (int)($dataHandler->substNEWwithIDs[$newId] ?? 0);
|
|
}
|
|
}
|
|
|
|
// target title => group
|
|
$groupByTitle = [];
|
|
foreach (self::TARGET as $group => $titles) {
|
|
foreach ($titles as $title) {
|
|
$groupByTitle[$title] = $group;
|
|
}
|
|
}
|
|
|
|
$solutions = [];
|
|
foreach ($connection->fetchAllAssociative(
|
|
'SELECT uid, title, slug FROM ' . self::TABLE . ' WHERE deleted = 0'
|
|
) as $row) {
|
|
$solutions[(string)$row['title']] = ['uid' => (int)$row['uid'], 'slug' => (string)$row['slug']];
|
|
}
|
|
$pids = $connection->fetchFirstColumn('SELECT pid FROM ' . self::TABLE . ' WHERE deleted = 0');
|
|
$pidCounts = array_count_values(array_map('intval', $pids));
|
|
arsort($pidCounts);
|
|
$storagePid = (int)(array_key_first($pidCounts) ?? 0);
|
|
|
|
$datamap = [];
|
|
$usedSlugs = [];
|
|
|
|
// ---- 2. renames ----------------------------------------------------
|
|
foreach (self::RENAMES as $oldTitle => $targetTitle) {
|
|
if (!isset($solutions[$oldTitle])) {
|
|
continue;
|
|
}
|
|
if (isset($solutions[$targetTitle])) {
|
|
$output->writeln('<comment>Rename uebersprungen, Ziel existiert schon: ' . $targetTitle . '</comment>');
|
|
continue;
|
|
}
|
|
$record = $solutions[$oldTitle];
|
|
$datamap[self::TABLE][$record['uid']]['title'] = $targetTitle;
|
|
$datamap[self::TABLE][$record['uid']]['slug'] = $this->slugFor($targetTitle, $usedSlugs);
|
|
$solutions[$targetTitle] = $record;
|
|
unset($solutions[$oldTitle]);
|
|
$output->writeln(sprintf('%s uid %d: "%s" -> "%s"', $dryRun ? 'plan ' : 'rename', $record['uid'], $oldTitle, $targetTitle));
|
|
}
|
|
|
|
// ---- 3.-5. targets: create / categories / slugs -------------------
|
|
$mm = [];
|
|
foreach ($connection->fetchAllAssociative(
|
|
"SELECT uid_local, uid_foreign FROM sys_category_record_mm
|
|
WHERE tablenames = ? AND fieldname = 'categories'",
|
|
[self::TABLE]
|
|
) as $row) {
|
|
$mm[(int)$row['uid_foreign']][] = (int)$row['uid_local'];
|
|
}
|
|
|
|
$created = 0;
|
|
$newIndex = 0;
|
|
foreach ($groupByTitle as $title => $group) {
|
|
$groupUid = (int)($groupCats[$group] ?? 0);
|
|
if (isset($solutions[$title])) {
|
|
$record = $solutions[$title];
|
|
$have = $mm[$record['uid']] ?? [];
|
|
// categories := exactly the group (strips market categories)
|
|
if ($groupUid > 0 && ($have !== [$groupUid])) {
|
|
$datamap[self::TABLE][$record['uid']]['categories'] = (string)$groupUid;
|
|
}
|
|
if ($groupUid === 0 && $dryRun) {
|
|
$output->writeln('plan Kategorie (neu anzulegende Gruppe) fuer: ' . $title);
|
|
}
|
|
$slug = (string)($datamap[self::TABLE][$record['uid']]['slug'] ?? $record['slug']);
|
|
if ($slug === '') {
|
|
$datamap[self::TABLE][$record['uid']]['slug'] = $this->slugFor($title, $usedSlugs);
|
|
}
|
|
} else {
|
|
$newId = 'NEW_S' . ++$newIndex;
|
|
$datamap[self::TABLE][$newId] = [
|
|
'pid' => $storagePid,
|
|
'title' => $title,
|
|
'slug' => $this->slugFor($title, $usedSlugs),
|
|
'categories' => $groupUid > 0 ? (string)$groupUid : '',
|
|
];
|
|
$created++;
|
|
$output->writeln(($dryRun ? 'plan ' : 'create') . ' Solution: ' . $title . ' [' . $group . ']');
|
|
}
|
|
}
|
|
|
|
// ---- 6. prune ------------------------------------------------------
|
|
$cmdmap = [];
|
|
$deleted = 0;
|
|
foreach ($solutions as $title => $record) {
|
|
if (isset($groupByTitle[$title])) {
|
|
continue;
|
|
}
|
|
$cmdmap[self::TABLE][$record['uid']]['delete'] = 1;
|
|
$deleted++;
|
|
$output->writeln(sprintf('%s uid %d: %s', $dryRun ? 'plan-DELETE' : 'DELETE ', $record['uid'], $title));
|
|
}
|
|
|
|
$output->writeln(sprintf("\nZiel: %d Solutions in %d Gruppen | anlegen: %d, loeschen: %d, aktualisieren: %d",
|
|
count($groupByTitle), count(self::TARGET), $created, $deleted,
|
|
count(array_filter(array_keys($datamap[self::TABLE] ?? []), 'is_int'))));
|
|
|
|
if ($dryRun) {
|
|
$output->writeln('DRY RUN - nichts geschrieben.');
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
foreach ([[$datamap, []], [[], $cmdmap]] as [$dm, $cm]) {
|
|
if ($dm === [] && $cm === []) {
|
|
continue;
|
|
}
|
|
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
|
$dataHandler->start($dm, $cm);
|
|
if ($dm !== []) {
|
|
$dataHandler->process_datamap();
|
|
}
|
|
if ($cm !== []) {
|
|
$dataHandler->process_cmdmap();
|
|
}
|
|
if ($dataHandler->errorLog !== []) {
|
|
foreach ($dataHandler->errorLog as $error) {
|
|
$output->writeln('<error>' . $error . '</error>');
|
|
}
|
|
return Command::FAILURE;
|
|
}
|
|
}
|
|
$output->writeln('Fertig. Cache leeren: vendor/bin/typo3 cache:flush');
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
/** @param string[] $used by-reference collection of slugs handed out in this run */
|
|
private function slugFor(string $title, array &$used): string
|
|
{
|
|
$slug = trim((string)preg_replace('/[^a-z0-9]+/', '-', str_replace('+', ' plus ', mb_strtolower($title))), '-');
|
|
$candidate = $slug;
|
|
$suffix = 1;
|
|
while (in_array($candidate, $used, true)) {
|
|
$candidate = $slug . '-' . ++$suffix;
|
|
}
|
|
$used[] = $candidate;
|
|
return $candidate;
|
|
}
|
|
}
|