VITEC Import module: Products tab is now a searchable/sortable product overview. "Edit Product" opens an XLSX upload with per-field source mapping (component + cell), old/new preview and checkbox apply via DataHandler; the mapping and manual matches are persisted and preselect the next workbook. Category workbooks are detected and rejected. - new: ProductXlsxReader (PhpSpreadsheet), ProductTextImportController, Products/ProductTexts templates, 4 module routes - related products: per-pair card text in new side table tx_vitec_product_related_text (survives MM rewrites), emitted as `cardtext` in the product JSON; missing MM relations added add-only - card copy read from Body Copy (D) with fallback to CTA/Card Copy (E) - the workbooks fill either depending on row type - product detail page <title> now uses seotitle with title fallback (provider made singleton and fed from the JSON renderer) - per-user recent-search badges; last loaded workbook stored per product (tx_vitec_product_workbook), Edit Product reopens on it - composer: add phpoffice/phpspreadsheet ^5.9 - diagnostics: migrations/check_workbook.php
71 lines
2.3 KiB
PHP
71 lines
2.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Evomedien\Vitec\Service;
|
|
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
|
|
|
/**
|
|
* Resolves a page uid to its frontend URL.
|
|
*
|
|
* A headless renderer cannot hand a raw page uid to the frontend — React has no
|
|
* way to turn `12` into a path. Every field that carries a link (`detailUrl`,
|
|
* `headerLink`, the usecase list's item links) is therefore resolved server side.
|
|
*
|
|
* The resolution goes through `typoLink_URL`, so route enhancers and the slug of
|
|
* the target page are honoured; building the path by hand would bypass both.
|
|
*
|
|
* This lived as a private copy in four renderers (Market list/show, Usecase
|
|
* list/show). The copies had drifted in their failure handling — two returned
|
|
* null, two an empty string — which is why the contract is spelled out here:
|
|
* **null means "no link"**, and callers that need a string coalesce it.
|
|
*/
|
|
final class LinkResolver
|
|
{
|
|
/**
|
|
* Returns null when no page is set or the link cannot be resolved.
|
|
*
|
|
* Never throws: a broken link must not take down the whole JSON response,
|
|
* so a failure downgrades to "no link" like an empty field would.
|
|
*/
|
|
public static function pageUrl(int $pageUid): ?string
|
|
{
|
|
if ($pageUid <= 0) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
|
$url = $cObj->typoLink_URL(['parameter' => (string)$pageUid]);
|
|
|
|
return $url !== '' ? $url : null;
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolves a full typolink parameter (t3://page..., t3://file..., an
|
|
* external URL, mailto:...) to a URL. Same contract as pageUrl():
|
|
* null means "no link", and a failure never throws.
|
|
*/
|
|
public static function typolinkUrl(string $parameter): ?string
|
|
{
|
|
$parameter = trim($parameter);
|
|
if ($parameter === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
|
$url = $cObj->typoLink_URL(['parameter' => $parameter]);
|
|
|
|
return $url !== '' ? $url : null;
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|