fix: date tables and group booking links on TYPO3 13

- Uri\ProductViewHelper builds links with the Extbase UriBuilder (no
  controller context in v13), also from content elements
- Null-safe reads of missing row fields and plugin settings, with the
  values PHP 7.4 produced
- /gruppe/ without a product redirects to the search page again
  instead of failing on findByIdentifier(null)

Checked against the TYPO3 10 project: 355 of 359 /gruppe/{product}
pages identical (the rest differ by newer content), all product pages
render, 377 of 396 date tables load (the 19 others fail on TYPO3 10
too: products without a hotel).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-21 15:03:10 +02:00
1 parent 34f2d55f2c
commit e70c5f48d5
7 files changed
+92 -72

No files matched your search

@@ -126,7 +126,7 @@ class AjaxTableController extends ActionController
?string $dateFrom = null,
?string $dateTo = null
): ResponseInterface {
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'] ?? '', true);
$filterSettings = $this->filterService->getNormalizedFilterSettings([], $forcedConceptUids);
$filterSettings['dateFrom'] = $dateFrom;
$filterSettings['dateTo'] = $dateTo;
@@ -184,7 +184,7 @@ class AjaxTableController extends ActionController
$hasSurchargeOrDiscount = $this->dateService->checkSurcharcheOrDiscount($allDates);
// $hasIncludedServices = $this->dateService->hasIncludedServices($allDates);
$hasIncludedServices = true;
$noInfoConceptUids = GeneralUtility::trimExplode(',', $this->settings['noInfoConceptUids']);
$noInfoConceptUids = GeneralUtility::trimExplode(',', $this->settings['noInfoConceptUids'] ?? '');
$isProductWithNoInfoConcept = ($product->getConcept() !== null) && \in_array($product->getConcept()->getUid(),
$noInfoConceptUids, false);
@@ -223,7 +223,7 @@ class AjaxTableController extends ActionController
Date $date,
bool $singleDate = false
): ResponseInterface {
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'] ?? '', true);
$filterSettings = $this->filterService->getNormalizedFilterSettings([], $forcedConceptUids);
$myEpUrl = $this->myEpUrlResolverService->resolve();
@@ -242,7 +242,7 @@ class AjaxTableController extends ActionController
$dateEnd = $date ? $date->getDateEnd()->format('d.m.y') : '';
$dateRange = sprintf('%s - %s', $dateStart, $dateEnd);
$noInfoConceptUids = GeneralUtility::trimExplode(',', $this->settings['noInfoConceptUids']);
$noInfoConceptUids = GeneralUtility::trimExplode(',', $this->settings['noInfoConceptUids'] ?? '');
$isProductWithNoInfoConcept = ($product->getConcept() !== null) && \in_array($product->getConcept()->getUid(), $noInfoConceptUids);
$nonBookableInfo = $this->getNonBookableInfobox($product);
@@ -298,7 +298,7 @@ class AjaxTableController extends ActionController
*/
public function eventPricetableAction(Product $product): ResponseInterface
{
$roomTypes = GeneralUtility::trimExplode(',', $this->settings['priceTableRoomTypes']);
$roomTypes = GeneralUtility::trimExplode(',', $this->settings['priceTableRoomTypes'] ?? '');
$roomTypesPax = array_map(function ($type) {
return (int) $type;
}, $roomTypes);
@@ -326,7 +326,7 @@ class AjaxTableController extends ActionController
'pricetable' => $priceTable,
'hasIncludedServices' => $hasIncludedServices,
'categories' => $categories,
'themekey' => $this->settings['themekey'],
'themekey' => ($this->settings['themekey'] ?? null),
'nonBookableInfo' => $nonBookableInfo,
'myEpUrl' => $myEpUrl,
]);
@@ -130,8 +130,8 @@ class ProductController extends ActionController
): ResponseInterface {
// Get product to display from flexform settings if not provided via url
if ($product === null) {
$productUid = $this->settings['productUid'];
$product = $this->productRepository->findByIdentifier($productUid);
$productUid = ($this->settings['productUid'] ?? null);
$product = $productUid ? $this->productRepository->findByIdentifier($productUid) : null;
}
// Redirect to search page in case no product can be determined
if ($product === null) {
@@ -141,12 +141,12 @@ class ProductController extends ActionController
'Search',
null,
['filterSettings' => $filterSettings],
$this->settings['defaultSearchPageUid']
($this->settings['defaultSearchPageUid'] ?? null)
);
}
// Get hotel to display from flexform settings if not provided via url
if ((int)$this->settings['hotelUid'] > 0) {
$hotelUid = $this->settings['hotelUid'];
if ((int)($this->settings['hotelUid'] ?? null) > 0) {
$hotelUid = ($this->settings['hotelUid'] ?? null);
$hotel = $this->hotelRepository->findByIdentifier($hotelUid);
}
if ($hotel === null) {
@@ -165,11 +165,11 @@ class ProductController extends ActionController
}
$concept = $product->getConcept();
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'], true);
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'] ?? '', true);
$isProductWithExcludedConcept = ($concept !== null) && \in_array($concept->getUid(), $excludedConceptUids);
$noInfoConceptUids = GeneralUtility::trimExplode(',', $this->settings['noInfoConceptUids'], true);
$noInfoConceptUids = GeneralUtility::trimExplode(',', $this->settings['noInfoConceptUids'] ?? '', true);
$isProductWithNoInfoConcept = ($concept !== null) && \in_array($concept->getUid(), $noInfoConceptUids);
$groupInquiryFormConceptUids = GeneralUtility::trimExplode(',', $this->settings['groupInquiryFormConceptUids'], true);
$groupInquiryFormConceptUids = GeneralUtility::trimExplode(',', $this->settings['groupInquiryFormConceptUids'] ?? '', true);
$isProductWithGroupInquiryForm = ($concept !== null) && \in_array($concept->getUid(), $groupInquiryFormConceptUids);
$nonBookableInfo = $product->getNonBookableInfo();
@@ -198,10 +198,10 @@ class ProductController extends ActionController
public function pricetableAction(): ResponseInterface
{
$productUid = $this->settings['productUid'];
$productUid = ($this->settings['productUid'] ?? null);
/** @var Product $product */
$product = $this->productRepository->findByIdentifier($productUid);
$hotelUid = $this->settings['hotelUid'];
$hotelUid = ($this->settings['hotelUid'] ?? null);
/** @var Hotel $hotel */
$hotel = $this->hotelRepository->findByIdentifier($hotelUid);
$pricetable = $this->dateService->getPriceTable([
@@ -230,14 +230,14 @@ class ProductController extends ActionController
public function teasergroupAction(): ResponseInterface
{
$filterSettings = $this->filterService->getFiltersettingsFromSettings($this->settings);
$dateAuto = !$this->settings['dateFrom'] && !$this->settings['dateTo'];
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'], true);
$limit = $this->settings['hideMoreLink'] ? 0 : 6;
$dateAuto = !($this->settings['dateFrom'] ?? null) && !($this->settings['dateTo'] ?? null);
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'] ?? '', true);
$limit = ($this->settings['hideMoreLink'] ?? null) ? 0 : 6;
$teasers = $this->productDataService->getTeasergroup($filterSettings, $excludedConceptUids, $limit);
$dateFrom = $this->settings['dateFrom'] ? date('Y-m-d', $this->settings['dateFrom']) : null;
$dateTo = $this->settings['dateTo'] ? date('Y-m-d', $this->settings['dateTo']) : null;
$dateFrom = ($this->settings['dateFrom'] ?? null) ? date('Y-m-d', ($this->settings['dateFrom'] ?? null)) : null;
$dateTo = ($this->settings['dateTo'] ?? null) ? date('Y-m-d', ($this->settings['dateTo'] ?? null)) : null;
$data = $this->request->getAttribute('currentContentObject')->data;
$nonBookableInfo = $this->settings['infoboxUid'] ? $this->infoboxRepository->findByUid($this->settings['infoboxUid']) : $this->infoboxRepository->getDefault();
$nonBookableInfo = ($this->settings['infoboxUid'] ?? null) ? $this->infoboxRepository->findByUid($this->settings['infoboxUid']) : $this->infoboxRepository->getDefault();
$this->view->assign('data', $data);
$this->view->assign('teasers', $teasers);
@@ -267,7 +267,7 @@ class DateService implements SingletonInterface
}
if (!array_key_exists($key, $processed)) {
$processed[$key] = [
'label' => $this->serviceSections[$key],
'label' => $this->serviceSections[$key] ?? null,
'services' => [],
];
}
@@ -436,7 +436,7 @@ class DateService implements SingletonInterface
public function hasIncludedServices(array $tableData): bool
{
foreach ($tableData as $row) {
if ($row['busIncluded'] || $row['skiPassIncluded']) {
if (($row['busIncluded'] ?? null) || ($row['skiPassIncluded'] ?? null)) {
return true;
}
}
@@ -516,10 +516,10 @@ class DateService implements SingletonInterface
];
foreach ($tableData as $row) {
if ($row['busPrice'] && $row['busPrice'] > 0) {
if (($row['busPrice'] ?? null) && $row['busPrice'] > 0) {
$data['surcharge'] = true;
}
if ($row['discount'] && $row['discount'] < 0) {
if (($row['discount'] ?? null) && $row['discount'] < 0) {
$data['discount'] = true;
}
}
@@ -88,20 +88,20 @@ class HotelDataService implements SingletonInterface
return [
'minDateStart' => $dateStart,
'maxDateEnd' => $dateEnd,
'minPrice' => $hotelData['minPrice'],
'pseudoPrice' => $hotelData['pseudoPrice'],
'available' => $hotelData['available'],
'detailPage' => $hotelData['hotelDetailPage'],
'name' => $hotelData['hotelName'],
'teaser' => $hotelData['hotelTeaser'],
'minPrice' => $hotelData['minPrice'] ?? null,
'pseudoPrice' => $hotelData['pseudoPrice'] ?? null,
'available' => $hotelData['available'] ?? null,
'detailPage' => $hotelData['hotelDetailPage'] ?? null,
'name' => $hotelData['hotelName'] ?? null,
'teaser' => $hotelData['hotelTeaser'] ?? null,
'category' => $hotelCategory,
'images' => $this->hotelImageService->getImages($hotelUid),
'country' => [
'name' => $hotelData['country'],
'code' => $hotelData['countryCode'],
'name' => $hotelData['country'] ?? null,
'code' => $hotelData['countryCode'] ?? null,
],
'region' => [
'name' => $hotelData['regionName'],
'name' => $hotelData['regionName'] ?? null,
]
];
}
@@ -131,11 +131,11 @@ class ProductDataService implements SingletonInterface
$teaser['dates'][] = $row['dateStart'];
}
if (!$teaser['skiPassIncluded'] && $row['skiPassIncluded']) {
if (!($teaser['skiPassIncluded'] ?? false) && ($row['skiPassIncluded'] ?? null)) {
$teaser['skiPassIncluded'] = true;
}
if (!$teaser['busIncluded'] && $row['busIncluded']) {
if (!($teaser['busIncluded'] ?? false) && ($row['busIncluded'] ?? null)) {
$teaser['busIncluded'] = true;
}
@@ -182,47 +182,47 @@ class ProductDataService implements SingletonInterface
'maxDateEnd' => null,
'minPrice' => null,
'hotelCount' => 1,
'pseudoPrice' => $teaserData['pseudoPrice'],
'board' => $teaserData['board'],
'earlybird' => $teaserData['earlybird'],
'new' => $teaserData['new'],
'daytrip' => $teaserData['daytrip'],
'hideBookingButton' => $teaserData['hideBookingButton'],
'lowContingent' => $teaserData['lowContingent'],
'servicesIncluded' => unserialize($teaserData['servicesIncluded'], ['allowed_classes' => false]),
'season' => $teaserData['season'],
'pseudoPrice' => $teaserData['pseudoPrice'] ?? null,
'board' => $teaserData['board'] ?? null,
'earlybird' => $teaserData['earlybird'] ?? null,
'new' => $teaserData['new'] ?? null,
'daytrip' => $teaserData['daytrip'] ?? null,
'hideBookingButton' => $teaserData['hideBookingButton'] ?? null,
'lowContingent' => $teaserData['lowContingent'] ?? null,
'servicesIncluded' => unserialize((string)($teaserData['servicesIncluded'] ?? ''), ['allowed_classes' => false]),
'season' => $teaserData['season'] ?? null,
'product' => [
'uid' => $teaserData['productUid'],
'name' => $teaserData['productName'],
'subline' => $teaserData['productSubline'],
'detailPage' => $teaserData['productDetailPage'],
'slug' => $teaserData['productSlug'],
'fact' => $teaserData['productFact'],
'feature' => $teaserData['productFeature'],
'teaser' => $teaserData['productTeaser'],
'images' => $this->productImageService->getImages($teaserData['productUid']),
'uid' => $teaserData['productUid'] ?? null,
'name' => $teaserData['productName'] ?? null,
'subline' => $teaserData['productSubline'] ?? null,
'detailPage' => $teaserData['productDetailPage'] ?? null,
'slug' => $teaserData['productSlug'] ?? null,
'fact' => $teaserData['productFact'] ?? null,
'feature' => $teaserData['productFeature'] ?? null,
'teaser' => $teaserData['productTeaser'] ?? null,
'images' => $this->productImageService->getImages($teaserData['productUid'] ?? null),
],
'concept' => [
'uid' => $teaserData['conceptUid'],
'name' => $teaserData['conceptName'],
'code' => $teaserData['conceptCode'],
'popupText' => $teaserData['conceptPopupText'],
'uid' => $teaserData['conceptUid'] ?? null,
'name' => $teaserData['conceptName'] ?? null,
'code' => $teaserData['conceptCode'] ?? null,
'popupText' => $teaserData['conceptPopupText'] ?? null,
],
'hotel' => [
'name' => $teaserData['hotelName'],
'name' => $teaserData['hotelName'] ?? null,
'category' => $hotelCategory,
'fact' => $teaserData['hotelFact'],
'uid' => $teaserData['hotelUid'],
'images' => $this->hotelImageService->getImages($teaserData['hotelUid']),
'fact' => $teaserData['hotelFact'] ?? null,
'uid' => $teaserData['hotelUid'] ?? null,
'images' => $this->hotelImageService->getImages($teaserData['hotelUid'] ?? null),
],
'country' => [
'name' => $teaserData['countryName'],
'code' => $teaserData['countryCode'],
'name' => $teaserData['countryName'] ?? null,
'code' => $teaserData['countryCode'] ?? null,
],
'region' => [
'name' => $teaserData['regionName'],
'fact' => $teaserData['regionFact'],
'feature' => $teaserData['regionFeature'],
'name' => $teaserData['regionName'] ?? null,
'fact' => $teaserData['regionFact'] ?? null,
'feature' => $teaserData['regionFeature'] ?? null,
],
'dates' => [],
'nights' => [],
@@ -61,7 +61,7 @@ class ContextProcessor implements DataProcessorInterface
if ($regionUid > 0) {
$processedData['context']['region'] = $this->getContextRecord($regionUid, 'tx_epproducts_domain_model_region');
}
if ($data['tx_eptheme_context_pacode']) {
if ($data['tx_eptheme_context_pacode'] ?? null) {
$processedData['context']['paCode'] = $data['tx_eptheme_context_pacode'];
} elseif ($paCode = $this->getPaCodeFromRequest()) {
$processedData['context']['paCode'] = $paCode;
@@ -27,6 +27,12 @@ namespace EP\EpTheme\ViewHelpers\Uri;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
use TYPO3\CMS\Extbase\Mvc\Request;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
@@ -73,10 +79,9 @@ class ProductViewHelper extends AbstractViewHelper
];
}
$targetPageUid = is_array($product) ? (int)$product['detailPage'] : $product->getDetailPage();
$uri = $this->renderingContext
->getControllerContext()
->getUriBuilder()
$uri = GeneralUtility::makeInstance(UriBuilder::class)
->reset()
->setRequest($this->getExtbaseRequest())
->setTargetPageUid($targetPageUid)
->uriFor('detail', $linkArguments, 'Product', 'epproducts', 'product_detail')
;
@@ -89,4 +94,19 @@ class ProductViewHelper extends AbstractViewHelper
return $uri;
}
/**
* The view helper is also used in content elements, which render without an Extbase request
*/
private function getExtbaseRequest(): RequestInterface
{
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
if ($request instanceof RequestInterface) {
return $request;
}
if ($request->getAttribute('extbase') === null) {
$request = $request->withAttribute('extbase', new ExtbaseRequestParameters());
}
return new Request($request);
}
}