Align product records with sitemap V2; product family model; CLI workbook import
- vitec:migrate-product-structure: exact V2 titles, Milestone typo fix (incl. slug), QTX100 stray VSN category removed, Aligo record moved to its own category with slug /product/aligo, new Aligo Workstation record takes over /product/aligo-workstation - product families (sitemap column K) are ordinary content pages now: vitec:remove-family-records deletes the interim landing records again, reset_subproduct_flags.php clears the misused subproduct flag (the list renderer has always excluded subproduct=1) - vitec:import-product-workbook: non-interactive counterpart of the module import (same reader, mapping and DataHandler path); Arqa imported, Aligo re-applied with the corrected capabilities markup - ProductXlsxReader: new :copy cell selector (Body Copy with CTA fallback) absorbs the agency's column drift; default teaser mapping uses it - ProductListJsonRenderer: selected categories now match their whole subtree, results ordered by category tree position, sheet order within a category - read-only diagnostics: check_product_structure.php, check_productlist_ces.php
This commit is contained in:
@@ -111,6 +111,12 @@ class ProductListJsonRenderer
|
||||
$categoryUids = array_filter(
|
||||
array_map('intval', explode(',', (string)($settings['categories'] ?? '')))
|
||||
);
|
||||
// A selected category matches its WHOLE subtree (decision
|
||||
// 2026-09-04): editors pick e.g. the "Platforms and End-Points"
|
||||
// parent, the products hang on its child categories.
|
||||
if ($categoryUids !== []) {
|
||||
$categoryUids = $this->expandWithDescendants($categoryUids);
|
||||
}
|
||||
$debugMode = (bool)($settings['debug'] ?? false);
|
||||
$allProducts = (bool)($settings['allproducts'] ?? false);
|
||||
|
||||
@@ -147,6 +153,41 @@ class ProductListJsonRenderer
|
||||
|
||||
$products = $productQuery->executeQuery()->fetchAllAssociative();
|
||||
|
||||
// Order by category in TREE order (decision 2026-09-04): products
|
||||
// of the first selected/child category first, then the next, so a
|
||||
// list over a parent category groups its families like the
|
||||
// sitemap. $categoryUids comes from expandWithDescendants in
|
||||
// depth-first tree order; within one category the uid order is
|
||||
// kept - the records were created in sitemap-V2 row order.
|
||||
if (!empty($categoryUids) && !$allProducts && $products !== []) {
|
||||
$rankByCategory = array_flip($categoryUids);
|
||||
$mmQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_category_record_mm');
|
||||
$assignments = $mmQueryBuilder->select('uid_local', 'uid_foreign')
|
||||
->from('sys_category_record_mm')
|
||||
->where(
|
||||
$mmQueryBuilder->expr()->eq('tablenames', $mmQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
|
||||
$mmQueryBuilder->expr()->eq('fieldname', $mmQueryBuilder->createNamedParameter('categories', ParameterType::STRING)),
|
||||
$mmQueryBuilder->expr()->in('uid_foreign', $mmQueryBuilder->createNamedParameter(
|
||||
array_map(static fn(array $p): int => (int)$p['uid'], $products),
|
||||
Connection::PARAM_INT_ARRAY
|
||||
))
|
||||
)->executeQuery()->fetchAllAssociative();
|
||||
$rankByProduct = [];
|
||||
foreach ($assignments as $assignment) {
|
||||
$productUid = (int)$assignment['uid_foreign'];
|
||||
$rank = $rankByCategory[(int)$assignment['uid_local']] ?? null;
|
||||
if ($rank !== null && $rank < ($rankByProduct[$productUid] ?? PHP_INT_MAX)) {
|
||||
$rankByProduct[$productUid] = $rank;
|
||||
}
|
||||
}
|
||||
usort($products, static function (array $a, array $b) use ($rankByProduct): int {
|
||||
$rankA = $rankByProduct[(int)$a['uid']] ?? PHP_INT_MAX;
|
||||
$rankB = $rankByProduct[(int)$b['uid']] ?? PHP_INT_MAX;
|
||||
return $rankA <=> $rankB ?: (int)$a['uid'] <=> (int)$b['uid'];
|
||||
});
|
||||
}
|
||||
|
||||
$productsData = [];
|
||||
foreach ($products as $product) {
|
||||
$productsData[] = $this->serializeProduct($product);
|
||||
@@ -181,6 +222,45 @@ class ProductListJsonRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The given category uids plus every descendant category uid, DEPTH first
|
||||
* over sys_category.parent with siblings in sys_category.sorting order -
|
||||
* the result is in backend-tree order and doubles as the sort rank for
|
||||
* the list. One query for the whole table - the tree is small (< 100
|
||||
* rows).
|
||||
*
|
||||
* @param int[] $categoryUids
|
||||
* @return int[]
|
||||
*/
|
||||
private function expandWithDescendants(array $categoryUids): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_category');
|
||||
$rows = $queryBuilder->select('uid', 'parent')->from('sys_category')
|
||||
->where($queryBuilder->expr()->eq('deleted', 0))
|
||||
->orderBy('parent')->addOrderBy('sorting')
|
||||
->executeQuery()->fetchAllAssociative();
|
||||
$childrenByParent = [];
|
||||
foreach ($rows as $row) {
|
||||
$childrenByParent[(int)$row['parent']][] = (int)$row['uid'];
|
||||
}
|
||||
$result = [];
|
||||
$visit = function (int $categoryUid) use (&$visit, &$result, $childrenByParent): void {
|
||||
$result[] = $categoryUid;
|
||||
foreach ($childrenByParent[$categoryUid] ?? [] as $childUid) {
|
||||
if (!in_array($childUid, $result, true)) {
|
||||
$visit($childUid);
|
||||
}
|
||||
}
|
||||
};
|
||||
foreach (array_map('intval', $categoryUids) as $categoryUid) {
|
||||
if (!in_array($categoryUid, $result, true)) {
|
||||
$visit($categoryUid);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a single product DB row to the full headless JSON structure.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user