Solution detail page and a single LinkResolver
Closes two annex items that turned out to be the same defect seen from two
sides: markets could link to a detail page, solutions could not, and the code
that turns a page uid into a URL existed four times over.
detail_page for Solution (B-11)
- ext_tables.sql, TCA (group on pages, maxitems 1, in showitem behind slug)
and Domain\Model\Solution mirror the Market field exactly
- SolutionShowJsonRenderer emits detailUrl
- ModelcardJsonRenderer serialized no detailUrl at all: its shared
market|solution branch never carried the field. Market modelcards therefore
gain a link here too, not just solutions.
Service\LinkResolver (B-12)
- one implementation replaces four private copies (Market list/show, Usecase
list/show). The copies had drifted: Market returned null on failure, Usecase
an empty string. The contract is now explicit -- null means "no link".
- Usecase keeps a thin resolvePageUrl() wrapper appending ?? '', because it
concatenates paths and a null would tear the strings apart.
- LocationsJsonRenderer and NewsJsonRenderer stay out on purpose: they resolve
a full parameter construct resp. slug paths plus canonical, which is not
page-uid-to-URL and does not fit the same contract.
Requires on the server, the column exists in code only:
vendor/bin/typo3 database:updateschema "*.add,*.change"
vendor/bin/typo3 cache:flush
This commit is contained in:
@@ -18,6 +18,13 @@ class Solution extends AbstractEntity
|
||||
*/
|
||||
protected $slug = '';
|
||||
|
||||
/**
|
||||
* Page uid presenting this solution (TCA type "group", allowed: pages).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $detailPage = 0;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
@@ -86,6 +93,22 @@ class Solution extends AbstractEntity
|
||||
$this->slug = $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDetailPage(): int
|
||||
{
|
||||
return $this->detailPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $detailPage
|
||||
*/
|
||||
public function setDetailPage(int $detailPage): void
|
||||
{
|
||||
$this->detailPage = $detailPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
48
packages/vitec/Classes/Service/LinkResolver.php
Normal file
48
packages/vitec/Classes/Service/LinkResolver.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,13 @@ declare(strict_types=1);
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Evomedien\Vitec\Service\LinkResolver;
|
||||
use Evomedien\Vitec\Service\RteResolver;
|
||||
use Evomedien\Vitec\Service\UsecaseSerializer;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* UserFunc: render all VITEC markets as JSON (headless).
|
||||
@@ -168,29 +168,8 @@ class MarketListJsonRenderer
|
||||
'subtitle' => (string)($r['subtitle'] ?? ''),
|
||||
'teaser' => (string)($r['teaser'] ?? ''),
|
||||
'description' => RteResolver::html($r['description'] ?? ''),
|
||||
'detailUrl' => $this->detailUrl((int)($r['detail_page'] ?? 0)),
|
||||
'detailUrl' => LinkResolver::pageUrl((int)($r['detail_page'] ?? 0)),
|
||||
'image' => $serializer->image($uid, 'image', self::TABLE, true),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the `detail_page` uid to a URL. The frontend cannot do anything
|
||||
* with a raw page uid, so the link is built server side — same convention
|
||||
* as `headerLink` and LocationsJsonRenderer.
|
||||
*
|
||||
* Returns null when no page is set or the link cannot be resolved.
|
||||
*/
|
||||
private function detailUrl(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ use Doctrine\DBAL\ParameterType;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use Evomedien\Vitec\Service\LinkResolver;
|
||||
use Evomedien\Vitec\Service\RteResolver;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* UserFunc to render a single market as JSON for headless output.
|
||||
@@ -161,7 +161,7 @@ class MarketShowJsonRenderer
|
||||
'subtitle' => (string)($market['subtitle'] ?? ''),
|
||||
'teaser' => (string)($market['teaser'] ?? ''),
|
||||
'description' => RteResolver::html($market['description'] ?? ''),
|
||||
'detailUrl' => $this->detailUrl((int)($market['detail_page'] ?? 0)),
|
||||
'detailUrl' => LinkResolver::pageUrl((int)($market['detail_page'] ?? 0)),
|
||||
'categories' => $this->getMarketCategories($uid),
|
||||
'image' => $this->getMarketImage($uid),
|
||||
];
|
||||
@@ -271,25 +271,4 @@ class MarketShowJsonRenderer
|
||||
];
|
||||
}, $categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the `detail_page` uid to a URL. The frontend cannot do anything
|
||||
* with a raw page uid, so the link is built server side — same convention
|
||||
* as `headerLink` and LocationsJsonRenderer.
|
||||
*
|
||||
* Returns null when no page is set or the link cannot be resolved.
|
||||
*/
|
||||
private function detailUrl(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Evomedien\Vitec\Service\LinkResolver;
|
||||
use Evomedien\Vitec\Service\RteResolver;
|
||||
use Evomedien\Vitec\Service\UsecaseSerializer;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
@@ -163,7 +164,7 @@ class ModelcardJsonRenderer
|
||||
];
|
||||
}
|
||||
|
||||
// market | solution — identical field set.
|
||||
// market | solution — identical field set, both carry `detail_page`.
|
||||
return [
|
||||
'uid' => $uid,
|
||||
'title' => (string)($row['title'] ?? ''),
|
||||
@@ -171,6 +172,7 @@ class ModelcardJsonRenderer
|
||||
'subtitle' => (string)($row['subtitle'] ?? ''),
|
||||
'teaser' => (string)($row['teaser'] ?? ''),
|
||||
'description' => RteResolver::html($row['description'] ?? ''),
|
||||
'detailUrl' => LinkResolver::pageUrl((int)($row['detail_page'] ?? 0)),
|
||||
'image' => $serializer->image($uid, 'image', self::MODEL_TABLES[$modelType], true),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Doctrine\DBAL\ParameterType;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use Evomedien\Vitec\Service\LinkResolver;
|
||||
use Evomedien\Vitec\Service\RteResolver;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
@@ -160,6 +161,7 @@ class SolutionShowJsonRenderer
|
||||
'subtitle' => (string)($solution['subtitle'] ?? ''),
|
||||
'teaser' => (string)($solution['teaser'] ?? ''),
|
||||
'description' => RteResolver::html($solution['description'] ?? ''),
|
||||
'detailUrl' => LinkResolver::pageUrl((int)($solution['detail_page'] ?? 0)),
|
||||
'categories' => $this->getSolutionCategories($uid),
|
||||
'image' => $this->getSolutionImage($uid),
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Evomedien\Vitec\Service\LinkResolver;
|
||||
use Evomedien\Vitec\Service\UsecaseSerializer;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
@@ -146,14 +147,14 @@ class UsecaseListJsonRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve a page uid to its frontend path (route enhancer aware). */
|
||||
/**
|
||||
* Resolve a page uid to its frontend path (route enhancer aware).
|
||||
*
|
||||
* Callers here build paths by concatenation, so the empty string stays the
|
||||
* "no link" value; LinkResolver's null is coalesced away.
|
||||
*/
|
||||
private function resolvePageUrl(int $pageUid): string
|
||||
{
|
||||
try {
|
||||
$cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
|
||||
return (string)$cObj->typoLink_URL(['parameter' => (string)$pageUid]);
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
return LinkResolver::pageUrl($pageUid) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Evomedien\Vitec\Service\LinkResolver;
|
||||
use Evomedien\Vitec\Service\UsecaseSerializer;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
@@ -153,14 +154,14 @@ class UsecaseShowJsonRenderer
|
||||
return (int)($row['uid'] ?? 0);
|
||||
}
|
||||
|
||||
/** Resolve a page uid to its frontend path (route enhancer aware). */
|
||||
/**
|
||||
* Resolve a page uid to its frontend path (route enhancer aware).
|
||||
*
|
||||
* Callers here build paths by concatenation, so the empty string stays the
|
||||
* "no link" value; LinkResolver's null is coalesced away.
|
||||
*/
|
||||
private function resolvePageUrl(int $pageUid): string
|
||||
{
|
||||
try {
|
||||
$cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
|
||||
return (string)$cObj->typoLink_URL(['parameter' => (string)$pageUid]);
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
return LinkResolver::pageUrl($pageUid) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ return [
|
||||
],
|
||||
'types' => [
|
||||
'1' => [
|
||||
'showitem' => 'title, slug, subtitle, teaser, description, image,
|
||||
'showitem' => 'title, slug, detail_page, subtitle, teaser, description, image,
|
||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories,
|
||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, sys_language_uid, l10n_parent, l10n_diffsource,
|
||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, hidden, starttime, endtime',
|
||||
@@ -129,6 +129,17 @@ return [
|
||||
'eval' => 'uniqueInPid',
|
||||
],
|
||||
],
|
||||
'detail_page' => [
|
||||
'exclude' => true,
|
||||
'label' => 'Detail Page',
|
||||
'description' => 'Page that presents this solution. Resolved to a URL in the JSON output; leave empty for no link.',
|
||||
'config' => [
|
||||
'type' => 'group',
|
||||
'allowed' => 'pages',
|
||||
'size' => 1,
|
||||
'maxitems' => 1,
|
||||
],
|
||||
],
|
||||
'subtitle' => [
|
||||
'exclude' => true,
|
||||
'label' => 'Subtitle',
|
||||
|
||||
@@ -155,6 +155,7 @@ CREATE TABLE tx_vitec_domain_model_solution (
|
||||
l10n_diffsource mediumblob,
|
||||
title varchar(255) DEFAULT '' NOT NULL,
|
||||
slug varchar(255) DEFAULT '' NOT NULL,
|
||||
detail_page int(11) DEFAULT '0' NOT NULL,
|
||||
subtitle varchar(255) DEFAULT '' NOT NULL,
|
||||
teaser varchar(255) DEFAULT '' NOT NULL,
|
||||
description text,
|
||||
|
||||
Reference in New Issue
Block a user