Add evo_megamenu_json extension and seed the megamenu
- new project-neutral extension evo_megamenu_json (Evomedien): a Megamenu plugin holding entries, columns, items, teaser cards and a featured slot as nested records; presentation settings travel with the payload so the front end reads behaviour instead of hard-coding it; published as page.10.fields.megaMenu, driven by the site settings megamenu.contentUid and megamenu.storagePid - vitec:seed-megamenu fills one element from the live structure: 5 entries, 26 columns, 95 items, 4 story cards; items without a page yet are flagged pending so the front end falls back to the column link - vitec:debug-sets prints the resolved site set order - read-only diagnostics: check_megamenu_schema.php, check_typoscript_templates.php - docs: schema updates run via extension:setup (database:updateschema no longer exists in v14), and a JSON Accept header activates headless-mixed, which strips every set-added page field
This commit is contained in:
84
packages/vitec/Classes/Command/DebugSetsCommand.php
Normal file
84
packages/vitec/Classes/Command/DebugSetsCommand.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?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\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Diagnostic: prints the site sets in the order TYPO3 actually resolves them.
|
||||
*
|
||||
* Set order decides who wins: friendsoftypo3/headless resets the whole `page`
|
||||
* object (`page < lib.headlessPage`), so every set that adds page-level
|
||||
* fields has to load AFTER it. When headless ends up last, those fields
|
||||
* vanish without any error - which is exactly the failure this command was
|
||||
* written for (2026-09-11).
|
||||
*
|
||||
* vendor/bin/typo3 vitec:debug-sets
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'vitec:debug-sets',
|
||||
description: 'Print the resolved site set order (diagnoses page-level TypoScript being reset)'
|
||||
)]
|
||||
final class DebugSetsCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('site', null, InputOption::VALUE_REQUIRED, 'Site identifier', 'vitec');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$identifier = (string)$input->getOption('site');
|
||||
$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByIdentifier($identifier);
|
||||
|
||||
if (!method_exists($site, 'getSets')) {
|
||||
$output->writeln('<error>This TYPO3 version does not expose Site::getSets().</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$sets = $site->getSets();
|
||||
$output->writeln(sprintf('Site "%s": %d sets, in load order', $identifier, count($sets)));
|
||||
|
||||
$position = 0;
|
||||
$headlessAt = null;
|
||||
$vitecAt = null;
|
||||
foreach ($sets as $set) {
|
||||
$position++;
|
||||
$name = is_object($set) && property_exists($set, 'name') ? (string)$set->name : (string)$set;
|
||||
if ($name === 'friendsoftypo3/headless') {
|
||||
$headlessAt = $position;
|
||||
}
|
||||
if ($name === 'evomedien/vitecset') {
|
||||
$vitecAt = $position;
|
||||
}
|
||||
$mark = in_array($name, ['friendsoftypo3/headless', 'evomedien/vitecset'], true) ? ' <-- ' : ' ';
|
||||
$output->writeln(sprintf('%2d.%s%s', $position, $mark, $name));
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
if ($headlessAt === null || $vitecAt === null) {
|
||||
$output->writeln('<comment>headless or vitecset is not in the resolved list at all.</comment>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
if ($headlessAt > $vitecAt) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>PROBLEM: headless loads at %d, AFTER vitecset at %d - it resets `page` and drops the page-level fields.</error>',
|
||||
$headlessAt, $vitecAt
|
||||
));
|
||||
} else {
|
||||
$output->writeln(sprintf(
|
||||
'<info>Order is fine: headless at %d, vitecset at %d.</info>',
|
||||
$headlessAt, $vitecAt
|
||||
));
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
523
packages/vitec/Classes/Command/SeedMegamenuCommand.php
Normal file
523
packages/vitec/Classes/Command/SeedMegamenuCommand.php
Normal file
@@ -0,0 +1,523 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Command;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Fills one "Megamenu" element (EXT:evo_megamenu_json) with the site's real
|
||||
* structure, so the editors start from a complete menu instead of a blank
|
||||
* element - roughly 5 entries, 25 columns and 90 items that would otherwise
|
||||
* be clicked together by hand.
|
||||
*
|
||||
* This is a PROJECT-side seed, deliberately not part of the neutral
|
||||
* extension: it knows where VITEC keeps its structure.
|
||||
*
|
||||
* Products columns = level-1 product categories, items = level-2
|
||||
* (the family pages do not exist yet, so every item
|
||||
* is marked `pending` and the front end falls back to
|
||||
* the column link)
|
||||
* Solutions columns = the 8 group categories, items = the 39 solution
|
||||
* records, linked through their `detail_page`
|
||||
* Markets columns = level-1 market categories, items = level-2; linked
|
||||
* through the market record's `detail_page` where one
|
||||
* exists, `pending` otherwise
|
||||
* Insights one column of section links (pages still to be built)
|
||||
* Stories teaser layout, four usecase records with their card image
|
||||
*
|
||||
* Column links are resolved against the page tree by title, so a renamed
|
||||
* page is reported rather than silently linked to nothing. Run it once;
|
||||
* everything after that is editorial work in the backend.
|
||||
*
|
||||
* vendor/bin/typo3 vitec:seed-megamenu --pid=42 --dry-run
|
||||
* vendor/bin/typo3 vitec:seed-megamenu --pid=42
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'vitec:seed-megamenu',
|
||||
description: 'Create a Megamenu element filled with the live product, solution and market structure'
|
||||
)]
|
||||
final class SeedMegamenuCommand extends Command
|
||||
{
|
||||
private const CTYPE = 'evomegamenujson_megamenu';
|
||||
private const T_ENTRY = 'tx_evomegamenujson_entry';
|
||||
private const T_COLUMN = 'tx_evomegamenujson_column';
|
||||
private const T_ITEM = 'tx_evomegamenujson_item';
|
||||
private const T_TEASER = 'tx_evomegamenujson_teaser';
|
||||
|
||||
private int $newId = 0;
|
||||
/** @var array<string,array<string,mixed>> */
|
||||
private array $datamap = [];
|
||||
/** @var string[] */
|
||||
private array $warnings = [];
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('pid', null, InputOption::VALUE_REQUIRED, 'Storage page/folder for the element and its records');
|
||||
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Report the plan - write nothing');
|
||||
$this->addOption('force', null, InputOption::VALUE_NONE, 'Seed even though a megamenu element already exists');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
$dryRun = (bool)$input->getOption('dry-run');
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
|
||||
|
||||
$existing = $connection->fetchAssociative(
|
||||
"SELECT uid, pid FROM tt_content WHERE deleted = 0 AND CType = ? ORDER BY uid LIMIT 1",
|
||||
[self::CTYPE]
|
||||
);
|
||||
if ($existing !== false && !$input->getOption('force')) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>A megamenu element already exists (uid %d on pid %d). Edit it in the backend, or re-run with --force to add a second one.</comment>',
|
||||
$existing['uid'], $existing['pid']
|
||||
));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$pid = (int)($input->getOption('pid') ?? 0);
|
||||
if ($pid <= 0) {
|
||||
$pid = (int)($existing['pid'] ?? 0);
|
||||
}
|
||||
if ($pid <= 0) {
|
||||
$output->writeln('<error>No storage page given - pass --pid=<uid of a sysfolder or page>.</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$contentId = $this->id();
|
||||
$entryIds = [];
|
||||
|
||||
foreach ([
|
||||
$this->buildProducts($pid, $output),
|
||||
$this->buildSolutions($pid, $output),
|
||||
$this->buildMarkets($pid, $output),
|
||||
$this->buildInsights($pid, $output),
|
||||
$this->buildStories($pid, $output),
|
||||
] as $entryId) {
|
||||
if ($entryId !== null) {
|
||||
$entryIds[] = $entryId;
|
||||
}
|
||||
}
|
||||
|
||||
if ($entryIds === []) {
|
||||
$output->writeln('<error>Nothing could be built - is the structure in place?</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->datamap['tt_content'][$contentId] = [
|
||||
'pid' => $pid,
|
||||
'CType' => self::CTYPE,
|
||||
'header' => 'Main navigation',
|
||||
'tx_evomegamenujson_entries' => implode(',', $entryIds),
|
||||
];
|
||||
|
||||
foreach ($this->warnings as $warning) {
|
||||
$output->writeln('<comment>' . $warning . '</comment>');
|
||||
}
|
||||
$output->writeln(sprintf("\n%d entries, %d columns, %d items, %d teaser cards on pid %d.",
|
||||
count($entryIds),
|
||||
count($this->datamap[self::T_COLUMN] ?? []),
|
||||
count($this->datamap[self::T_ITEM] ?? []),
|
||||
count($this->datamap[self::T_TEASER] ?? []),
|
||||
$pid
|
||||
));
|
||||
|
||||
if ($dryRun) {
|
||||
$output->writeln('DRY RUN - nothing written.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start($this->datamap, []);
|
||||
$dataHandler->process_datamap();
|
||||
if ($dataHandler->errorLog !== []) {
|
||||
foreach ($dataHandler->errorLog as $error) {
|
||||
$output->writeln('<error>' . $error . '</error>');
|
||||
}
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$uid = (int)($dataHandler->substNEWwithIDs[$contentId] ?? 0);
|
||||
$output->writeln(sprintf('Megamenu element created: uid %d.', $uid));
|
||||
$output->writeln(sprintf('Point the site setting megamenu.contentUid at %d, then: vendor/bin/typo3 cache:flush', $uid));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- entries
|
||||
|
||||
private function buildProducts(int $pid, OutputInterface $output): ?string
|
||||
{
|
||||
$root = $this->pageBySlug('/products');
|
||||
$categories = $this->categoryTree('Product');
|
||||
if ($categories === []) {
|
||||
$this->warnings[] = 'Products skipped: no category tree below "Product".';
|
||||
return null;
|
||||
}
|
||||
$pages = $root ? $this->childPagesByTitle((int)$root['uid']) : [];
|
||||
|
||||
$columnIds = [];
|
||||
foreach ($categories as $group) {
|
||||
$page = $pages[$this->norm($group['title'])] ?? null;
|
||||
if ($page === null) {
|
||||
$this->warnings[] = 'Products: no page found for category "' . $group['title'] . '".';
|
||||
}
|
||||
$itemIds = [];
|
||||
foreach ($group['children'] as $family) {
|
||||
// The family pages are still to be built (decision 2026-09-04:
|
||||
// product families are content pages) - mark them pending so
|
||||
// the front end falls back to the category link.
|
||||
$itemIds[] = $this->item($pid, $family['title'], '', true);
|
||||
}
|
||||
$columnIds[] = $this->column($pid, $group['title'], $page ? $this->pageLink((int)$page['uid']) : '', $itemIds);
|
||||
}
|
||||
$output->writeln(sprintf('Products: %d columns, %d items (all pending - family pages missing)',
|
||||
count($columnIds), count($this->datamap[self::T_ITEM] ?? [])));
|
||||
|
||||
return $this->entry($pid, 'Products', $root ? $this->pageLink((int)$root['uid']) : '', 'columns', $columnIds);
|
||||
}
|
||||
|
||||
private function buildSolutions(int $pid, OutputInterface $output): ?string
|
||||
{
|
||||
$root = $this->pageBySlug('/solutions');
|
||||
$categories = $this->categoryTree('Solution');
|
||||
if ($categories === []) {
|
||||
$this->warnings[] = 'Solutions skipped: no category tree below "Solution".';
|
||||
return null;
|
||||
}
|
||||
$pages = $root ? $this->childPagesByTitle((int)$root['uid']) : [];
|
||||
$byCategory = $this->recordsByCategory('tx_vitec_domain_model_solution');
|
||||
|
||||
$columnIds = [];
|
||||
$itemCount = 0;
|
||||
foreach ($categories as $group) {
|
||||
$records = $byCategory[(int)$group['uid']] ?? [];
|
||||
if ($records === []) {
|
||||
continue;
|
||||
}
|
||||
$page = $pages[$this->norm($group['title'])] ?? null;
|
||||
$itemIds = [];
|
||||
foreach ($records as $record) {
|
||||
$detail = (int)$record['detail_page'];
|
||||
$itemIds[] = $this->item($pid, (string)$record['title'],
|
||||
$detail > 0 ? $this->pageLink($detail) : '', $detail === 0);
|
||||
$itemCount++;
|
||||
}
|
||||
$columnIds[] = $this->column($pid, $group['title'], $page ? $this->pageLink((int)$page['uid']) : '', $itemIds);
|
||||
}
|
||||
$output->writeln(sprintf('Solutions: %d columns, %d items', count($columnIds), $itemCount));
|
||||
|
||||
return $this->entry($pid, 'Solutions', $root ? $this->pageLink((int)$root['uid']) : '', 'columns', $columnIds);
|
||||
}
|
||||
|
||||
private function buildMarkets(int $pid, OutputInterface $output): ?string
|
||||
{
|
||||
$root = $this->pageBySlug('/markets');
|
||||
$categories = $this->categoryTree('Market');
|
||||
if ($categories === []) {
|
||||
$this->warnings[] = 'Markets skipped: no category tree below "Market".';
|
||||
return null;
|
||||
}
|
||||
$pages = $root ? $this->childPagesByTitle((int)$root['uid']) : [];
|
||||
$detailByTitle = $this->detailPageByTitle('tx_vitec_domain_model_market');
|
||||
|
||||
$columnIds = [];
|
||||
$pending = 0;
|
||||
$itemCount = 0;
|
||||
foreach ($categories as $group) {
|
||||
$page = $pages[$this->norm($group['title'])] ?? null;
|
||||
$itemIds = [];
|
||||
foreach ($group['children'] as $market) {
|
||||
$detail = (int)($detailByTitle[$this->norm($market['title'])] ?? 0);
|
||||
$itemIds[] = $this->item($pid, $market['title'],
|
||||
$detail > 0 ? $this->pageLink($detail) : '', $detail === 0);
|
||||
$pending += $detail === 0 ? 1 : 0;
|
||||
$itemCount++;
|
||||
}
|
||||
$columnIds[] = $this->column($pid, $group['title'], $page ? $this->pageLink((int)$page['uid']) : '', $itemIds);
|
||||
}
|
||||
$output->writeln(sprintf('Markets: %d columns, %d items (%d pending - no detail page)',
|
||||
count($columnIds), $itemCount, $pending));
|
||||
|
||||
return $this->entry($pid, 'Markets', $root ? $this->pageLink((int)$root['uid']) : '', 'columns', $columnIds);
|
||||
}
|
||||
|
||||
private function buildInsights(int $pid, OutputInterface $output): ?string
|
||||
{
|
||||
$root = $this->pageBySlug('/insights');
|
||||
if ($root === null) {
|
||||
$this->warnings[] = 'Insights skipped: no page /insights.';
|
||||
return null;
|
||||
}
|
||||
$pages = $this->childPagesByTitle((int)$root['uid']);
|
||||
$sections = ['News & Articles', 'Events & Webinars', 'Blog'];
|
||||
|
||||
$itemIds = [];
|
||||
foreach ($sections as $section) {
|
||||
$page = $pages[$this->norm($section)] ?? null;
|
||||
$itemIds[] = $this->item($pid, $section, $page ? $this->pageLink((int)$page['uid']) : '', $page === null);
|
||||
}
|
||||
$columnId = $this->column($pid, 'Sections', $this->pageLink((int)$root['uid']), $itemIds);
|
||||
$output->writeln('Insights: 1 column, ' . count($itemIds) . ' section links (teaser cards stay editorial)');
|
||||
|
||||
return $this->entry($pid, 'Insights', $this->pageLink((int)$root['uid']), 'columns', [$columnId]);
|
||||
}
|
||||
|
||||
private function buildStories(int $pid, OutputInterface $output): ?string
|
||||
{
|
||||
$root = $this->pageBySlug('/success-stories');
|
||||
if ($root === null) {
|
||||
$this->warnings[] = 'Success Stories skipped: no page /success-stories.';
|
||||
return null;
|
||||
}
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_vitec_domain_model_usecase');
|
||||
$stories = $connection->fetchAllAssociative(
|
||||
"SELECT uid, title, teaser, slug, detail_page FROM tx_vitec_domain_model_usecase
|
||||
WHERE deleted = 0 AND hidden = 0 AND slug <> ''
|
||||
ORDER BY tstamp DESC LIMIT 4"
|
||||
);
|
||||
|
||||
// Stories have no `detail_page` of their own - one detail page serves
|
||||
// them all and SuccessStoryPathRewrite builds /success-stories/<slug>.
|
||||
// The seed writes that path, the same URL the list plugin emits.
|
||||
$basePath = '/' . trim((string)$root['slug'], '/');
|
||||
|
||||
$teaserIds = [];
|
||||
$withoutLink = 0;
|
||||
foreach ($stories as $story) {
|
||||
$detail = (int)$story['detail_page'];
|
||||
$slug = trim((string)$story['slug'], '/');
|
||||
$link = $detail > 0
|
||||
? $this->pageLink($detail)
|
||||
: ($slug !== '' ? $basePath . '/' . $slug : '');
|
||||
$withoutLink += $link === '' ? 1 : 0;
|
||||
$teaserIds[] = $this->teaser(
|
||||
$pid,
|
||||
'Success Story',
|
||||
(string)$story['title'],
|
||||
$this->shorten((string)($story['teaser'] ?? '')),
|
||||
$link,
|
||||
$this->fileOf('tx_vitec_domain_model_usecase', 'card_image', (int)$story['uid'])
|
||||
);
|
||||
}
|
||||
if ($withoutLink > 0) {
|
||||
$this->warnings[] = 'Success Stories: ' . $withoutLink . ' card(s) without link - story has neither detail page nor slug.';
|
||||
}
|
||||
$output->writeln('Success Stories: teaser layout, ' . count($teaserIds) . ' cards');
|
||||
|
||||
return $this->entry($pid, 'Success Stories', $this->pageLink((int)$root['uid']), 'teasers', [], $teaserIds);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------- record makers
|
||||
|
||||
/** @param string[] $columnIds @param string[] $teaserIds */
|
||||
private function entry(int $pid, string $title, string $link, string $layout, array $columnIds, array $teaserIds = []): string
|
||||
{
|
||||
$id = $this->id();
|
||||
$record = [
|
||||
'pid' => $pid,
|
||||
'title' => $title,
|
||||
'link' => $link,
|
||||
'layout' => $layout,
|
||||
];
|
||||
if ($columnIds !== []) {
|
||||
$record['menu_columns'] = implode(',', $columnIds);
|
||||
}
|
||||
if ($teaserIds !== []) {
|
||||
$record['menu_teasers'] = implode(',', $teaserIds);
|
||||
}
|
||||
$this->datamap[self::T_ENTRY][$id] = $record;
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** @param string[] $itemIds */
|
||||
private function column(int $pid, string $title, string $link, array $itemIds): string
|
||||
{
|
||||
$id = $this->id();
|
||||
$this->datamap[self::T_COLUMN][$id] = [
|
||||
'pid' => $pid,
|
||||
'title' => $title,
|
||||
'link' => $link,
|
||||
'menu_items' => implode(',', $itemIds),
|
||||
];
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function item(int $pid, string $title, string $link, bool $pending): string
|
||||
{
|
||||
$id = $this->id();
|
||||
$this->datamap[self::T_ITEM][$id] = [
|
||||
'pid' => $pid,
|
||||
'title' => $title,
|
||||
'link' => $link,
|
||||
'pending' => $pending ? 1 : 0,
|
||||
];
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function teaser(int $pid, string $kicker, string $title, string $text, string $link, int $fileUid): string
|
||||
{
|
||||
$id = $this->id();
|
||||
$record = [
|
||||
'pid' => $pid,
|
||||
'kicker' => $kicker,
|
||||
'title' => $title,
|
||||
'teasertext' => $text,
|
||||
'link' => $link,
|
||||
];
|
||||
if ($fileUid > 0) {
|
||||
$referenceId = $this->id();
|
||||
$this->datamap['sys_file_reference'][$referenceId] = [
|
||||
'pid' => $pid,
|
||||
'table_local' => 'sys_file',
|
||||
'uid_local' => $fileUid,
|
||||
'tablenames' => self::T_TEASER,
|
||||
'fieldname' => 'image',
|
||||
'uid_foreign' => $id,
|
||||
];
|
||||
$record['image'] = $referenceId;
|
||||
}
|
||||
$this->datamap[self::T_TEASER][$id] = $record;
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function id(): string
|
||||
{
|
||||
return 'NEW' . str_pad((string)++$this->newId, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ lookups
|
||||
|
||||
/** @return array<int,array{uid:int,title:string,children:array<int,array{uid:int,title:string}>}> */
|
||||
private function categoryTree(string $rootTitle): array
|
||||
{
|
||||
$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 = ?', [$rootTitle]
|
||||
);
|
||||
if ($rootUid <= 0) {
|
||||
return [];
|
||||
}
|
||||
$tree = [];
|
||||
foreach ($connection->fetchAllAssociative(
|
||||
'SELECT uid, title FROM sys_category WHERE deleted = 0 AND hidden = 0 AND parent = ? ORDER BY sorting', [$rootUid]
|
||||
) as $group) {
|
||||
$children = [];
|
||||
foreach ($connection->fetchAllAssociative(
|
||||
'SELECT uid, title FROM sys_category WHERE deleted = 0 AND hidden = 0 AND parent = ? ORDER BY sorting', [(int)$group['uid']]
|
||||
) as $child) {
|
||||
$children[] = ['uid' => (int)$child['uid'], 'title' => (string)$child['title']];
|
||||
}
|
||||
$tree[] = ['uid' => (int)$group['uid'], 'title' => (string)$group['title'], 'children' => $children];
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records of a table grouped by the category they hang on, sorted by
|
||||
* their position in the category (uid order = creation order).
|
||||
*
|
||||
* @return array<int,array<int,array<string,mixed>>>
|
||||
*/
|
||||
private function recordsByCategory(string $table): array
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
|
||||
$rows = $connection->fetchAllAssociative(
|
||||
"SELECT r.uid, r.title, r.detail_page, mm.uid_local AS category
|
||||
FROM $table r
|
||||
JOIN sys_category_record_mm mm ON mm.uid_foreign = r.uid AND mm.tablenames = ? AND mm.fieldname = 'categories'
|
||||
WHERE r.deleted = 0 AND r.hidden = 0
|
||||
ORDER BY r.uid",
|
||||
[$table]
|
||||
);
|
||||
$grouped = [];
|
||||
foreach ($rows as $row) {
|
||||
$grouped[(int)$row['category']][] = $row;
|
||||
}
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
/** @return array<string,int> normalized record title => detail_page uid */
|
||||
private function detailPageByTitle(string $table): array
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
|
||||
$map = [];
|
||||
foreach ($connection->fetchAllAssociative(
|
||||
"SELECT title, detail_page FROM $table WHERE deleted = 0 AND hidden = 0"
|
||||
) as $row) {
|
||||
$detail = (int)$row['detail_page'];
|
||||
if ($detail > 0) {
|
||||
$map[$this->norm((string)$row['title'])] = $detail;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
private function pageBySlug(string $slug): ?array
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
|
||||
$row = $connection->fetchAssociative(
|
||||
'SELECT uid, title, slug FROM pages WHERE deleted = 0 AND slug = ? AND sys_language_uid IN (0,-1) ORDER BY uid LIMIT 1',
|
||||
[$slug]
|
||||
);
|
||||
if ($row === false) {
|
||||
$this->warnings[] = 'No page with slug ' . $slug . '.';
|
||||
return null;
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/** @return array<string,array<string,mixed>> normalized title => page row */
|
||||
private function childPagesByTitle(int $parentUid): array
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
|
||||
$map = [];
|
||||
foreach ($connection->fetchAllAssociative(
|
||||
'SELECT uid, title, slug FROM pages WHERE deleted = 0 AND pid = ? AND doktype = 1 AND sys_language_uid IN (0,-1) ORDER BY sorting',
|
||||
[$parentUid]
|
||||
) as $row) {
|
||||
$map[$this->norm((string)$row['title'])] = $row;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/** uid of the first file behind a FAL field, 0 when none. */
|
||||
private function fileOf(string $table, string $field, int $uid): int
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_file_reference');
|
||||
$fileUid = $connection->fetchOne(
|
||||
"SELECT uid_local FROM sys_file_reference
|
||||
WHERE deleted = 0 AND tablenames = ? AND fieldname = ? AND uid_foreign = ?
|
||||
ORDER BY sorting_foreign LIMIT 1",
|
||||
[$table, $field, $uid]
|
||||
);
|
||||
return is_numeric($fileUid) ? (int)$fileUid : 0;
|
||||
}
|
||||
|
||||
private function pageLink(int $pageUid): string
|
||||
{
|
||||
return 't3://page?uid=' . $pageUid;
|
||||
}
|
||||
|
||||
private function shorten(string $text, int $max = 120): string
|
||||
{
|
||||
$flat = trim((string)preg_replace('/\s+/u', ' ', strip_tags($text)));
|
||||
return mb_strlen($flat) > $max ? mb_substr($flat, 0, $max - 1) . '…' : $flat;
|
||||
}
|
||||
|
||||
private function norm(string $value): string
|
||||
{
|
||||
return (string)preg_replace('/[^a-z0-9]+/', '', mb_strtolower($value));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user