Files
Oliver Rasche 7bd1161abf Product text import from agency XLSX workbooks
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
2026-08-21 11:00:45 +02:00

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;
}
}
}