Fill the /products portfolio page from the agency workbook
- new vitec:build-product-portfolio (dry-run by default): hero, nine category sections (intro paragraphs with the exact sitemap H2s and Learn-more buttons onto the new category pages), 38 product cards with record-first link resolution (30 resolved; the 8 open record gaps stay reported), a Case Studies & Events banner (midnight, success stories / events), ONE vitec_faq accordion whose four questions are created as vitec_faq_items collection children (field names probed against the schema at runtime), and the orange prefooter banner (contact / solutions); full page properties and page layout 0 -> 1 "Index Page" - diagnosis first (new read-only migrations/check_products_page.php and check_products_page2.php): the dry run reported zero legacy elements although the payload rendered ~40 - every own element of page 447 was already soft-deleted, the visible rendering came from the "Collection Product Infos" compose page. Building in place was therefore safe; unconsumed visible legacy would have been hidden, not deleted - side finding, left open on purpose: six old-taxonomy category stubs (#451-454, #456, #458) still sit visible under /products with the family pages beneath them - hide/move/redirect is an editorial decision queued for Olli - applied and verified live (hero, 9 category buttons, 30 card links, 4 FAQ items, both banner CTA pairs, meta) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2d4367d7a
commit
c6147d020e
3 files changed
+497
No files matched your search
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose: where does the /products page (#447) content actually live?
|
||||
* The portfolio dry-run found ZERO visible elements on pid 447 although the
|
||||
* JSON payload renders ~40. Suspicion: content_from_pid or elements on
|
||||
* another pid (10331/10368 = payload samples).
|
||||
*
|
||||
* READ-ONLY, plain PDO (pattern migrations/check_bg.php).
|
||||
* Usage: php migrations/check_products_page.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== page 447 ===\n";
|
||||
$p = $pdo->query('SELECT uid, title, slug, doktype, layout, content_from_pid, l10n_parent, sys_language_uid FROM pages WHERE uid = 447')->fetch();
|
||||
print_r($p);
|
||||
|
||||
echo "\n=== tt_content on pid 447 (any state) ===\n";
|
||||
foreach ($pdo->query('SELECT colPos, CType, COUNT(*) c, SUM(hidden) hid, SUM(deleted) del FROM tt_content WHERE pid = 447 GROUP BY colPos, CType ORDER BY colPos') as $r) {
|
||||
printf(" colPos %-4s %-24s n=%-3d hidden=%d deleted=%d\n", $r['colPos'], $r['CType'], $r['c'], $r['hid'], $r['del']);
|
||||
}
|
||||
|
||||
echo "\n=== sample payload elements ===\n";
|
||||
foreach ([10331, 10368, 10306] as $uid) {
|
||||
$r = $pdo->query("SELECT uid, pid, CType, colPos, tx_container_parent, hidden, deleted, sys_language_uid FROM tt_content WHERE uid = $uid")->fetch();
|
||||
print_r($r);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose part 2: page 448 (where the rendered /products elements live)
|
||||
* plus every page whose slug starts with /products, and the visible
|
||||
* element summary of 448.
|
||||
*
|
||||
* READ-ONLY. Usage: php migrations/check_products_page2.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== pages with slug like /products% (incl. deleted/hidden) ===\n";
|
||||
foreach ($pdo->query("SELECT uid, pid, title, slug, doktype, layout, hidden, deleted, sorting FROM pages WHERE slug LIKE '/products%' ORDER BY uid") as $r) {
|
||||
printf(" #%-4d pid=%-4d dok=%d lay=%-2d hid=%d del=%d sort=%-5d %-42s %s\n",
|
||||
$r['uid'], $r['pid'], $r['doktype'], $r['layout'], $r['hidden'], $r['deleted'], $r['sorting'], $r['slug'], $r['title']);
|
||||
}
|
||||
|
||||
echo "\n=== page 448 ===\n";
|
||||
print_r($pdo->query('SELECT uid, pid, title, slug, doktype, layout, hidden, deleted, content_from_pid FROM pages WHERE uid = 448')->fetch());
|
||||
|
||||
echo "\n=== visible tt_content on pid 448 (top level) ===\n";
|
||||
foreach ($pdo->query('SELECT colPos, CType, COUNT(*) c FROM tt_content WHERE pid = 448 AND deleted = 0 AND hidden = 0 AND tx_container_parent = 0 GROUP BY colPos, CType ORDER BY colPos') as $r) {
|
||||
printf(" colPos %-4s %-24s n=%d\n", $r['colPos'], $r['CType'], $r['c']);
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Command;
|
||||
|
||||
use Evomedien\Vitec\Import\ProductFamilyAliases;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
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 the /products PORTFOLIO page from the agency workbook
|
||||
* "Product_Portfolio_Page_Content_US_English.xlsx" (staged in
|
||||
* migrations/product_categories_xlsx_in/portfolio/). Companion of
|
||||
* vitec:import-product-category-pages - same reader shape, same card
|
||||
* resolution, one fixed page.
|
||||
*
|
||||
* Structure per workbook: hero, nine category sections (H2 + intro +
|
||||
* "Learn more" to the category page, then the product cards), a Case
|
||||
* Studies & Events banner (two CTAs), one vitec_faq accordion (four
|
||||
* items as collection children) and the pre-footer banner (Talk to an
|
||||
* Expert -> /contact, Explore Solutions -> /solutions).
|
||||
*
|
||||
* The page is hand-built today (textpic/productlist/containers): every
|
||||
* pre-existing visible top-level element that this run does not reuse is
|
||||
* HIDDEN, not deleted - re-runs reuse the elements this command created
|
||||
* (slot pattern) and leave the hidden legacy untouched.
|
||||
*
|
||||
* vendor/bin/typo3 vitec:build-product-portfolio
|
||||
* vendor/bin/typo3 vitec:build-product-portfolio --apply
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'vitec:build-product-portfolio',
|
||||
description: 'Fill the /products portfolio page from the agency workbook (dry-run by default)'
|
||||
)]
|
||||
final class BuildProductPortfolioCommand extends Command
|
||||
{
|
||||
private const COLPOS_HERO = 1;
|
||||
private const COLPOS_MAIN = 0;
|
||||
private const COLPOS_PREFOOTER = 3;
|
||||
private const DEFAULT_FILE = 'migrations/product_categories_xlsx_in/portfolio/Product_Portfolio_Page_Content_US_English.xlsx';
|
||||
|
||||
/** @var array<string,array<string|int,array<string,mixed>>> */
|
||||
private array $datamap = [];
|
||||
/** @var array<int,string[]> */
|
||||
private array $sequence = [];
|
||||
/** @var array<string,int[]> */
|
||||
private array $slotCursor = [];
|
||||
/** @var array<int,bool> pre-existing uids this run reuses */
|
||||
private array $consumed = [];
|
||||
private int $newCounter = 0;
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('file', null, InputOption::VALUE_REQUIRED, 'Workbook path', self::DEFAULT_FILE);
|
||||
$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');
|
||||
$file = (string)$input->getOption('file');
|
||||
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$pages = $pool->getConnectionForTable('pages');
|
||||
$content = $pool->getConnectionForTable('tt_content');
|
||||
|
||||
$root = $pages->fetchAssociative("SELECT uid FROM pages WHERE deleted = 0 AND slug = '/products'");
|
||||
if (!$root || !is_file($file)) {
|
||||
$output->writeln('<error>' . (!$root ? '/products not found.' : $file . ' missing.') . '</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$pageUid = (int)$root['uid'];
|
||||
// the page sits on layout 0 (Default, main only) - the portfolio
|
||||
// needs hero/main/preFooter: 1 "Index Page" (diagnosis 2026-10-27:
|
||||
// the page's own content was all deleted, the rendered payload came
|
||||
// from the Collection Product Infos compose mechanics)
|
||||
$this->datamap['pages'][$pageUid]['layout'] = 1;
|
||||
[$meta, $components] = $this->readSheet($file);
|
||||
$output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . ' · /products #' . $pageUid . ' · ' . count($components) . ' component(s)');
|
||||
|
||||
$categoryPages = [];
|
||||
foreach ($pages->fetchAllAssociative(
|
||||
'SELECT uid, slug FROM pages WHERE deleted = 0 AND pid = ? AND doktype = 1 AND sys_language_uid IN (0,-1)', [$pageUid]
|
||||
) as $row) {
|
||||
$categoryPages[$this->norm((string)basename((string)$row['slug']))] = (int)$row['uid'];
|
||||
}
|
||||
|
||||
// --- hero ---------------------------------------------------------------------
|
||||
$hero = $this->firstComponent($components, 'hero');
|
||||
if ($hero !== null) {
|
||||
$id = $this->slotNext($pageUid, self::COLPOS_HERO, 'vitec_herosection');
|
||||
$this->setFields($id, [
|
||||
'header' => (string)$hero['heading'], 'header_layout' => 1,
|
||||
'bodytext' => $this->p((string)$hero['body']),
|
||||
'vitec_cta_label' => (string)$hero['cta'],
|
||||
]);
|
||||
$output->writeln(' hero: "' . $hero['heading'] . '"');
|
||||
}
|
||||
|
||||
// --- main: categories, cards, case studies, faq --------------------------------
|
||||
$faqItems = [];
|
||||
foreach ($components as $component) {
|
||||
$kind = mb_strtolower($component['component']);
|
||||
if (str_starts_with($kind, 'hero')) {
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($kind, 'product category')) {
|
||||
$fields = [
|
||||
'header' => (string)$component['heading'],
|
||||
'bodytext' => $this->p((string)$component['body']),
|
||||
];
|
||||
$categoryUid = $categoryPages[$this->norm((string)$component['heading'])] ?? 0;
|
||||
if ($categoryUid > 0) {
|
||||
$fields['vitec_button_text'] = (string)$component['cta'] !== '' ? (string)$component['cta'] : 'Learn more';
|
||||
$fields['vitec_button_link'] = 't3://page?uid=' . $categoryUid;
|
||||
$output->writeln(' category "' . $component['heading'] . '" -> page #' . $categoryUid);
|
||||
} else {
|
||||
$output->writeln(' <comment>category "' . $component['heading'] . '": NO category page matched - button left empty</comment>');
|
||||
}
|
||||
$id = $this->slotNext($pageUid, self::COLPOS_MAIN, 'vitec_introparagraph');
|
||||
$this->setFields($id, $fields);
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($kind, 'product card')) {
|
||||
$cardTitle = (string)$component['heading'];
|
||||
$fields = [
|
||||
'vitec_layout_variant' => 'text_only',
|
||||
'header' => $cardTitle,
|
||||
'bodytext' => $this->p((string)$component['cta']),
|
||||
'vitec_primary_cta_label' => 'Learn more',
|
||||
];
|
||||
$product = $this->resolveProduct($cardTitle);
|
||||
if ($product !== null) {
|
||||
$fields['vitec_primary_cta'] = '/product/' . $product['slug'];
|
||||
} else {
|
||||
$output->writeln(' <comment>product card "' . $cardTitle . '": NO record resolved - link left empty</comment>');
|
||||
}
|
||||
$id = $this->slotNext($pageUid, self::COLPOS_MAIN, 'vitec_card');
|
||||
$this->setFields($id, $fields);
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($kind, 'case studies')) {
|
||||
$id = $this->slotNext($pageUid, self::COLPOS_MAIN, 'vitec_ctabanner');
|
||||
$this->setFields($id, [
|
||||
'header' => (string)$component['heading'],
|
||||
'bodytext' => $this->p((string)$component['body']),
|
||||
'vitec_primary_cta_label' => 'View Success Stories',
|
||||
'vitec_primary_cta' => $this->pageLink('/success-stories'),
|
||||
'vitec_secondary_cta_label' => 'View Events',
|
||||
'vitec_secondary_cta' => $this->pageLink('/insights/events'),
|
||||
'vitec_layout_variant' => 'centered',
|
||||
'vitec_background_variant' => 'midnight',
|
||||
]);
|
||||
$output->writeln(' case studies & events banner -> /success-stories + /insights/events');
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($kind, 'faq')) {
|
||||
$faqItems[] = ['question' => (string)$component['heading'], 'answer' => (string)$component['body']];
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($kind, 'pre-footer')) {
|
||||
$id = $this->slotNext($pageUid, self::COLPOS_PREFOOTER, 'vitec_ctabanner');
|
||||
$this->setFields($id, [
|
||||
'header' => (string)$component['heading'],
|
||||
'bodytext' => $this->p((string)$component['body']),
|
||||
'vitec_primary_cta_label' => 'Talk to an Expert',
|
||||
'vitec_primary_cta' => $this->pageLink('/contact'),
|
||||
'vitec_secondary_cta_label' => 'Explore Solutions',
|
||||
'vitec_secondary_cta' => $this->pageLink('/solutions'),
|
||||
'vitec_layout_variant' => 'centered',
|
||||
'vitec_background_variant' => 'orange',
|
||||
]);
|
||||
$output->writeln(' prefooter -> /contact + /solutions');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// faq accordion with its collection children
|
||||
if ($faqItems !== []) {
|
||||
$faqId = $this->slotNext($pageUid, self::COLPOS_MAIN, 'vitec_faq');
|
||||
$questionField = $this->faqColumn($pool, 'question');
|
||||
$answerField = $this->faqColumn($pool, 'answer');
|
||||
$childIds = [];
|
||||
$existingChildren = [];
|
||||
if (!str_starts_with($faqId, 'NEW')) {
|
||||
$existingChildren = array_map('intval', $pool->getConnectionForTable('vitec_faq_items')->fetchFirstColumn(
|
||||
'SELECT uid FROM vitec_faq_items WHERE deleted = 0 AND foreign_table_parent_uid = ? ORDER BY sorting', [(int)$faqId]
|
||||
));
|
||||
}
|
||||
foreach ($faqItems as $item) {
|
||||
$childId = array_shift($existingChildren);
|
||||
$childKey = $childId !== null ? (string)$childId : $this->newId();
|
||||
$this->datamap['vitec_faq_items'][$childKey] = array_filter([
|
||||
'pid' => $childId !== null ? null : $pageUid,
|
||||
$questionField => $item['question'],
|
||||
$answerField => $this->p($item['answer']),
|
||||
], static fn($v): bool => $v !== null);
|
||||
$childIds[] = $childKey;
|
||||
}
|
||||
$this->setFields($faqId, [
|
||||
'header' => 'Frequently asked questions',
|
||||
'vitec_faq_items' => implode(',', $childIds),
|
||||
'vitec_first_item_open' => 1,
|
||||
]);
|
||||
$output->writeln(' faq accordion: ' . count($faqItems) . ' item(s) (fields ' . $questionField . '/' . $answerField . ')');
|
||||
}
|
||||
|
||||
// --- hide the unconsumed legacy elements ----------------------------------------
|
||||
$legacy = $content->fetchAllAssociative(
|
||||
'SELECT uid, CType, header FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = ? AND tx_container_parent = 0 AND colPos >= 0 ORDER BY sorting',
|
||||
[$pageUid]
|
||||
);
|
||||
$hidden = 0;
|
||||
foreach ($legacy as $row) {
|
||||
if (isset($this->consumed[(int)$row['uid']])) {
|
||||
continue;
|
||||
}
|
||||
$this->datamap['tt_content'][(string)(int)$row['uid']]['hidden'] = 1;
|
||||
$hidden++;
|
||||
}
|
||||
$output->writeln(' legacy: ' . $hidden . ' visible element(s) HIDDEN (kept for reference), ' . count($this->consumed) . ' reused');
|
||||
|
||||
// --- page properties --------------------------------------------------------------
|
||||
$heroBody = trim((string)($hero['body'] ?? ''));
|
||||
$seoTitle = 'Products | VITEC';
|
||||
foreach ([
|
||||
'seo_title' => $seoTitle, 'og_title' => $seoTitle, 'twitter_title' => $seoTitle,
|
||||
'description' => $heroBody, 'og_description' => $heroBody, 'twitter_description' => $heroBody,
|
||||
'abstract' => $heroBody, 'keywords' => (string)($meta['keyword'] ?? ''),
|
||||
'canonical_link' => 't3://page?uid=' . $pageUid,
|
||||
] as $field => $value) {
|
||||
$this->datamap['pages'][$pageUid][$field] = $value;
|
||||
}
|
||||
$output->writeln(' page meta: seo/og/twitter + abstract + keywords ("' . ($meta['keyword'] ?? '') . '")');
|
||||
|
||||
if (!$apply) {
|
||||
$output->writeln("\nDRY RUN - nothing written. Add --apply to write.");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start($this->datamap, []);
|
||||
$dataHandler->process_datamap();
|
||||
if ($dataHandler->errorLog !== []) {
|
||||
$output->writeln('<error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$this->applySequence($content, $dataHandler->substNEWwithIDs);
|
||||
$output->writeln('<info>Done. Flush the frontend cache.</info>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// ---- helpers (pattern of vitec:import-product-category-pages) ---------------------
|
||||
|
||||
private function faqColumn(ConnectionPool $pool, string $base): string
|
||||
{
|
||||
$conn = $pool->getConnectionForTable('vitec_faq_items');
|
||||
$columns = array_column($conn->fetchAllAssociative('SHOW COLUMNS FROM vitec_faq_items'), 'Field');
|
||||
return in_array($base, $columns, true) ? $base : 'vitec_' . $base;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
private function resolveProduct(string $cardTitle): ?array
|
||||
{
|
||||
$conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_vitec_domain_model_product');
|
||||
$rows = $conn->fetchAllAssociative('SELECT uid, title, slug FROM tx_vitec_domain_model_product WHERE deleted = 0');
|
||||
$full = $this->norm($cardTitle);
|
||||
$stripped = $this->norm((string)preg_replace('/\s*\(.*$/', '', $cardTitle));
|
||||
foreach ([$full, $stripped] as $needle) {
|
||||
foreach ($rows as $row) {
|
||||
if ($this->norm((string)$row['title']) === $needle) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
$alias = ProductFamilyAliases::resolve($stripped);
|
||||
if ($alias !== null) {
|
||||
return $alias;
|
||||
}
|
||||
$matches = array_values(array_filter($rows, fn(array $row): bool => str_starts_with($this->norm((string)$row['slug']), $stripped)));
|
||||
return count($matches) === 1 ? $matches[0] : null;
|
||||
}
|
||||
|
||||
private function pageLink(string $slug): string
|
||||
{
|
||||
$row = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages')
|
||||
->fetchAssociative('SELECT uid FROM pages WHERE deleted = 0 AND slug = ?', [$slug]);
|
||||
return $row ? 't3://page?uid=' . (int)$row['uid'] : '';
|
||||
}
|
||||
|
||||
/** @return array{0:array<string,string>,1:array<int,array{component:string,heading:string,body:string,cta:string}>} */
|
||||
private function readSheet(string $file): array
|
||||
{
|
||||
$spreadsheet = IOFactory::load($file);
|
||||
$sheet = $spreadsheet->getSheet(0);
|
||||
$rows = $sheet->toArray(null, false, false, false);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
$meta = [];
|
||||
$components = [];
|
||||
$inTable = false;
|
||||
foreach ($rows as $row) {
|
||||
$a = trim((string)($row[0] ?? ''));
|
||||
$b = trim((string)($row[1] ?? ''));
|
||||
if (!$inTable) {
|
||||
if (mb_strtolower($a) === 'primary keyword') {
|
||||
$meta['keyword'] = $b;
|
||||
}
|
||||
if (mb_strtolower($a) === 'order') {
|
||||
$inTable = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($b === '') {
|
||||
continue;
|
||||
}
|
||||
$components[] = [
|
||||
'component' => $b,
|
||||
'heading' => trim((string)($row[2] ?? '')),
|
||||
'body' => trim((string)($row[3] ?? '')),
|
||||
'cta' => trim((string)($row[4] ?? '')),
|
||||
];
|
||||
}
|
||||
return [$meta, $components];
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,string>> $components */
|
||||
private function firstComponent(array $components, string $needle): ?array
|
||||
{
|
||||
foreach ($components as $component) {
|
||||
if (str_contains(mb_strtolower($component['component']), $needle)) {
|
||||
return $component;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function slotNext(int $pageUid, int $colPos, string $ctype): string
|
||||
{
|
||||
$key = $colPos . '/' . $ctype;
|
||||
if (!isset($this->slotCursor[$key])) {
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
|
||||
$this->slotCursor[$key] = array_map('intval', $connection->fetchFirstColumn(
|
||||
'SELECT uid FROM tt_content WHERE deleted = 0 AND pid = ? AND colPos = ? AND CType = ? AND tx_container_parent = 0 ORDER BY sorting',
|
||||
[$pageUid, $colPos, $ctype]
|
||||
));
|
||||
}
|
||||
$uid = array_shift($this->slotCursor[$key]);
|
||||
if ($uid !== null) {
|
||||
$this->consumed[$uid] = true;
|
||||
$this->sequence[$colPos][] = (string)$uid;
|
||||
$this->datamap['tt_content'][(string)$uid]['hidden'] = 0;
|
||||
return (string)$uid;
|
||||
}
|
||||
$id = $this->newId();
|
||||
$this->datamap['tt_content'][$id] = ['pid' => $pageUid, 'colPos' => $colPos, 'CType' => $ctype];
|
||||
$this->sequence[$colPos][] = $id;
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $fields */
|
||||
private function setFields(string $id, array $fields): void
|
||||
{
|
||||
foreach ($fields as $field => $value) {
|
||||
$this->datamap['tt_content'][$id][$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string,int> $substNEWwithIDs */
|
||||
private function applySequence($connection, array $substNEWwithIDs): void
|
||||
{
|
||||
foreach ($this->sequence as $ids) {
|
||||
$sorting = 0;
|
||||
foreach ($ids as $id) {
|
||||
$uid = str_starts_with($id, 'NEW') ? (int)($substNEWwithIDs[$id] ?? 0) : (int)$id;
|
||||
if ($uid > 0) {
|
||||
$sorting += 256;
|
||||
$connection->update('tt_content', ['sorting' => $sorting], ['uid' => $uid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function p(string $text): string
|
||||
{
|
||||
$text = trim($text);
|
||||
return $text === '' ? '' : '<p>' . htmlspecialchars($text, ENT_QUOTES | ENT_HTML5) . '</p>';
|
||||
}
|
||||
|
||||
private function norm(string $value): string
|
||||
{
|
||||
$value = mb_strtolower($value);
|
||||
$value = str_replace(['visualization', 'center'], ['visualisation', 'centre'], $value);
|
||||
return (string)preg_replace('/[^a-z0-9]+/', '', str_replace(['+', '&'], ['plus', 'and'], $value));
|
||||
}
|
||||
|
||||
private function newId(): string
|
||||
{
|
||||
return 'NEWPF' . (++$this->newCounter);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user