refactor: PHP 8.5 language features via Rector

rector.php gets the PHP sets up to 8.5. Mostly constructor promotion with
readonly properties, typed class constants, ??=, arrow functions,
str_contains/str_starts_with, catch without unused variables, array_any
and #[\Override] on overridden properties; array_key_exists() keys are
cast to string (null keys are deprecated in PHP 8.5). Long promoted
constructor signatures are wrapped one parameter per line.

Checked locally: a crawl of ~1,900 frontend URLs over all sites and the
backchannel APIs (/api/hotel, /api/product, content feed manifest and
records of every type) render the same as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-26 10:59:53 +01:00
1 parent 96632edf44
commit b51fa4ddad
61 files changed
+320 -609

No files matched your search

@@ -19,53 +19,29 @@ class ContentFeedApi
{ {
public const PATH_PREFIX = '/api/content/'; public const PATH_PREFIX = '/api/content/';
private const MANIFEST_CACHE_IDENTIFIER = 'content_feed_manifest'; private const string MANIFEST_CACHE_IDENTIFIER = 'content_feed_manifest';
// The consumer polls every 15 minutes, so this caps the build at once per poll however // The consumer polls every 15 minutes, so this caps the build at once per poll however
// often it retries. Edits reach the chatbot within about 25 minutes. // often it retries. Edits reach the chatbot within about 25 minutes.
private const MANIFEST_CACHE_LIFETIME = 600; private const int MANIFEST_CACHE_LIFETIME = 600;
private const MAX_RECORDS_PER_REQUEST = 100; private const int MAX_RECORDS_PER_REQUEST = 100;
private const DISABLED_RETRY_AFTER = 3600; private const int DISABLED_RETRY_AFTER = 3600;
private const JSON_FLAGS = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; private const JSON_FLAGS = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
/**
* @var ApiKeyAuthenticator
*/
private $authenticator;
/**
* @var ContentFeedConfiguration
*/
private $configuration;
/**
* @var ManifestBuilder
*/
private $manifestBuilder;
/**
* @var RecordSerializer
*/
private $recordSerializer;
/** /**
* @var FrontendInterface * @var FrontendInterface
*/ */
private $cache; private $cache;
public function __construct( public function __construct(
ApiKeyAuthenticator $authenticator, private readonly ApiKeyAuthenticator $authenticator,
ContentFeedConfiguration $configuration, private readonly ContentFeedConfiguration $configuration,
ManifestBuilder $manifestBuilder, private readonly ManifestBuilder $manifestBuilder,
RecordSerializer $recordSerializer, private readonly RecordSerializer $recordSerializer,
CacheManager $cacheManager CacheManager $cacheManager
) { ) {
$this->authenticator = $authenticator;
$this->configuration = $configuration;
$this->manifestBuilder = $manifestBuilder;
$this->recordSerializer = $recordSerializer;
$this->cache = $cacheManager->getCache('ep_products_cache'); $this->cache = $cacheManager->getCache('ep_products_cache');
} }
@@ -112,9 +88,7 @@ class ContentFeedApi
$uids = array_values(array_unique(array_filter( $uids = array_values(array_unique(array_filter(
GeneralUtility::intExplode(',', (string) ($request->getQueryParams()['uids'] ?? ''), true), GeneralUtility::intExplode(',', (string) ($request->getQueryParams()['uids'] ?? ''), true),
function (int $uid): bool { fn(int $uid): bool => $uid > 0
return $uid > 0;
}
))); )));
if ([] === $uids) { if ([] === $uids) {
@@ -16,13 +16,13 @@ class ManifestBuilder
{ {
public const CONTENT_PAGE_TYPE = 1730; public const CONTENT_PAGE_TYPE = 1730;
private const DOKTYPE_DEFAULT = 1; private const int DOKTYPE_DEFAULT = 1;
// Recycler and backend user section: nothing below them is public. // Recycler and backend user section: nothing below them is public.
private const DOKTYPES_NOT_TRAVERSED = [6, 255]; private const array DOKTYPES_NOT_TRAVERSED = [6, 255];
// l18n_cfg bit 1: "hide default translation of page". // l18n_cfg bit 1: "hide default translation of page".
private const L18N_HIDE_DEFAULT_LANGUAGE = 1; private const int L18N_HIDE_DEFAULT_LANGUAGE = 1;
private const RECORD_FIELDS = [ private const array RECORD_FIELDS = [
'product' => ['uid', 'tstamp', 'detail_page'], 'product' => ['uid', 'tstamp', 'detail_page'],
'hotel' => ['uid', 'tstamp', 'detail_page'], 'hotel' => ['uid', 'tstamp', 'detail_page'],
'region' => ['uid', 'tstamp', 'detail_page'], 'region' => ['uid', 'tstamp', 'detail_page'],
@@ -32,43 +32,13 @@ class ManifestBuilder
'news' => ['uid', 'tstamp', 'pid'], 'news' => ['uid', 'tstamp', 'pid'],
]; ];
/**
* @var PageTree
*/
private $pageTree;
/**
* @var UrlResolver
*/
private $urlResolver;
/**
* @var RecordFinder
*/
private $recordFinder;
/**
* @var ContentFeedConfiguration
*/
private $configuration;
/**
* @var ConnectionPool
*/
private $connectionPool;
public function __construct( public function __construct(
PageTree $pageTree, private readonly PageTree $pageTree,
UrlResolver $urlResolver, private readonly UrlResolver $urlResolver,
RecordFinder $recordFinder, private readonly RecordFinder $recordFinder,
ContentFeedConfiguration $configuration, private readonly ContentFeedConfiguration $configuration,
ConnectionPool $connectionPool private readonly ConnectionPool $connectionPool,
) { ) {
$this->pageTree = $pageTree;
$this->urlResolver = $urlResolver;
$this->recordFinder = $recordFinder;
$this->configuration = $configuration;
$this->connectionPool = $connectionPool;
} }
public function build(): array public function build(): array
@@ -78,7 +48,7 @@ class ManifestBuilder
return [ return [
'generatedAt' => date('c'), 'generatedAt' => date('c'),
'sites' => array_map([$this, 'describeSite'], $sites), 'sites' => array_map($this->describeSite(...), $sites),
'items' => array_merge($this->buildPageItems($sites, $this->collectDetailPages()), $records), 'items' => array_merge($this->buildPageItems($sites, $this->collectDetailPages()), $records),
]; ];
} }
@@ -211,8 +181,14 @@ class ManifestBuilder
return 0 !== $languageId || 0 === ((int) $page['l18n_cfg'] & self::L18N_HIDE_DEFAULT_LANGUAGE); return 0 !== $languageId || 0 === ((int) $page['l18n_cfg'] & self::L18N_HIDE_DEFAULT_LANGUAGE);
} }
private function buildPageItem(array $page, array $row, int $languageId, string $isoCode, array $breadcrumb, array $context): ?array private function buildPageItem(
{ array $page,
array $row,
int $languageId,
string $isoCode,
array $breadcrumb,
array $context,
): ?array {
$uid = (int) $page['uid']; $uid = (int) $page['uid'];
// "Show content from page": the elements live on another page, and so do their changes. // "Show content from page": the elements live on another page, and so do their changes.
// The site answers 404 when that page is hidden. // The site answers 404 when that page is hidden.
@@ -15,17 +15,12 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
*/ */
class PageTree class PageTree
{ {
private const FIELDS = [ private const array FIELDS = [
'uid', 'pid', 'sorting', 'doktype', 'hidden', 'starttime', 'endtime', 'fe_group', 'uid', 'pid', 'sorting', 'doktype', 'hidden', 'starttime', 'endtime', 'fe_group',
'extendToSubpages', 'no_index', 'title', 'nav_title', 'description', 'shortcut', 'extendToSubpages', 'no_index', 'title', 'nav_title', 'description', 'shortcut',
'shortcut_mode', 'content_from_pid', 'tstamp', 'sys_language_uid', 'l10n_parent', 'l18n_cfg', 'shortcut_mode', 'content_from_pid', 'tstamp', 'sys_language_uid', 'l10n_parent', 'l18n_cfg',
]; ];
/**
* @var ConnectionPool
*/
private $connectionPool;
/** /**
* @var int * @var int
*/ */
@@ -46,9 +41,8 @@ class PageTree
*/ */
private $overlays = []; private $overlays = [];
public function __construct(ConnectionPool $connectionPool, Context $context) public function __construct(private readonly ConnectionPool $connectionPool, Context $context)
{ {
$this->connectionPool = $connectionPool;
$this->now = (int) $context->getPropertyFromAspect('date', 'timestamp'); $this->now = (int) $context->getPropertyFromAspect('date', 'timestamp');
} }
@@ -26,14 +26,8 @@ class RecordFinder
'news' => 'tx_news_domain_model_news', 'news' => 'tx_news_domain_model_news',
]; ];
/** public function __construct(private readonly ConnectionPool $connectionPool)
* @var ConnectionPool
*/
private $connectionPool;
public function __construct(ConnectionPool $connectionPool)
{ {
$this->connectionPool = $connectionPool;
} }
/** /**
@@ -18,36 +18,12 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
*/ */
class RecordSerializer class RecordSerializer
{ {
/**
* @var RecordFinder
*/
private $recordFinder;
/**
* @var UrlResolver
*/
private $urlResolver;
/**
* @var RteLinkResolver
*/
private $rteLinkResolver;
/**
* @var ConnectionPool
*/
private $connectionPool;
public function __construct( public function __construct(
RecordFinder $recordFinder, private readonly RecordFinder $recordFinder,
UrlResolver $urlResolver, private readonly UrlResolver $urlResolver,
RteLinkResolver $rteLinkResolver, private readonly RteLinkResolver $rteLinkResolver,
ConnectionPool $connectionPool private readonly ConnectionPool $connectionPool,
) { ) {
$this->recordFinder = $recordFinder;
$this->urlResolver = $urlResolver;
$this->rteLinkResolver = $rteLinkResolver;
$this->connectionPool = $connectionPool;
} }
/** /**
@@ -62,18 +38,12 @@ class RecordSerializer
return []; return [];
} }
switch ($type) { return match ($type) {
case 'product': 'product' => $this->serializeProducts($rows),
return $this->serializeProducts($rows); 'hotel' => $this->serializeHotels($rows),
case 'hotel': 'news' => $this->serializeNews($rows),
return $this->serializeHotels($rows); default => array_values(array_map(fn(array $row): array => $this->serializeDestination($type, $row), $rows)),
case 'news': };
return $this->serializeNews($rows);
default:
return array_values(array_map(function (array $row) use ($type): array {
return $this->serializeDestination($type, $row);
}, $rows));
}
} }
private function serializeProducts(array $rows): array private function serializeProducts(array $rows): array
@@ -137,7 +107,7 @@ class RecordSerializer
foreach ($rows as $uid => $row) { foreach ($rows as $uid => $row) {
$code = trim((string) $row['code']); $code = trim((string) $row['code']);
$codes = array_values(array_unique(array_filter(array_merge([$code], $aliases[$uid] ?? []), 'strlen'))); $codes = array_values(array_unique(array_filter(array_merge([$code], $aliases[$uid] ?? []), strlen(...))));
$items[] = $this->withLink($row, max((int) $row['tstamp'], $matchcodeTimestamps[$uid] ?? 0)) + [ $items[] = $this->withLink($row, max((int) $row['tstamp'], $matchcodeTimestamps[$uid] ?? 0)) + [
'code' => '' === $code ? null : $code, 'code' => '' === $code ? null : $code,
@@ -152,9 +122,7 @@ class RecordSerializer
'countryUid' => (int) $row['country'] ?: null, 'countryUid' => (int) $row['country'] ?: null,
'regionUid' => (int) $row['region'] ?: null, 'regionUid' => (int) $row['region'] ?: null,
'cityUid' => (int) $row['city'] ?: null, 'cityUid' => (int) $row['city'] ?: null,
'productUids' => array_values(array_filter($products[$uid] ?? [], function (int $productUid) use ($eligibleProducts): bool { 'productUids' => array_values(array_filter($products[$uid] ?? [], fn(int $productUid): bool => isset($eligibleProducts[$productUid]))),
return isset($eligibleProducts[$productUid]);
})),
'address' => $this->text($row['address']), 'address' => $this->text($row['address']),
'latitude' => $this->text($row['latitude']), 'latitude' => $this->text($row['latitude']),
'longitude' => $this->text($row['longitude']), 'longitude' => $this->text($row['longitude']),
@@ -192,64 +160,61 @@ class RecordSerializer
'headline' => $this->text($row['headline']), 'headline' => $this->text($row['headline']),
]; ];
switch ($type) { return match ($type) {
case 'region': 'region' => $item + [
return $item + [ 'season' => $this->text($row['season']),
'season' => $this->text($row['season']), 'keywords' => $this->text($row['keywords']),
'keywords' => $this->text($row['keywords']), 'countryUid' => (int) $row['country'] ?: null,
'countryUid' => (int) $row['country'] ?: null, 'facts' => [
'facts' => [ 'length' => $this->text($row['length']),
'length' => $this->text($row['length']), 'height' => $this->text($row['height']),
'height' => $this->text($row['height']), 'lifts' => $this->text($row['lifts']),
'lifts' => $this->text($row['lifts']), ],
], 'webcam' => $this->findWebcamUrl((int) $row['webcam']),
'webcam' => $this->findWebcamUrl((int) $row['webcam']), 'text' => [
'text' => [ 'teaser' => $this->html($row['teaser']),
'teaser' => $this->html($row['teaser']), 'description' => $this->html($row['description']),
'description' => $this->html($row['description']), 'feature' => $this->text($row['feature']),
'feature' => $this->text($row['feature']), 'skiArea' => $this->html($row['ski_area']),
'skiArea' => $this->html($row['ski_area']), 'skiAreaExtended' => $this->html($row['ski_area_extended']),
'skiAreaExtended' => $this->html($row['ski_area_extended']), 'skiPass' => $this->html($row['ski_pass']),
'skiPass' => $this->html($row['ski_pass']), 'news' => $this->html($row['news']),
'news' => $this->html($row['news']), ],
], ],
]; 'country' => $item + [
case 'country': 'code' => $this->text($row['code']),
return $item + [ 'season' => $this->text($row['season']),
'code' => $this->text($row['code']), 'keywords' => $this->text($row['keywords']),
'season' => $this->text($row['season']), 'text' => [
'keywords' => $this->text($row['keywords']), 'teaser' => $this->html($row['teaser']),
'text' => [ 'description' => $this->html($row['description']),
'teaser' => $this->html($row['teaser']), 'descriptionExtended' => $this->html($row['description_extended']),
'description' => $this->html($row['description']), ],
'descriptionExtended' => $this->html($row['description_extended']), ],
], 'city' => $item + [
]; 'season' => $this->text($row['season']),
case 'city': 'keywords' => $this->text($row['keywords']),
return $item + [ 'countryUid' => (int) $row['country'] ?: null,
'season' => $this->text($row['season']), 'regionUid' => (int) $row['region'] ?: null,
'keywords' => $this->text($row['keywords']), 'text' => [
'countryUid' => (int) $row['country'] ?: null, 'teaser' => $this->html($row['teaser']),
'regionUid' => (int) $row['region'] ?: null, 'description' => $this->html($row['description']),
'text' => [ 'descriptionExtended' => $this->html($row['description_extended']),
'teaser' => $this->html($row['teaser']), 'party' => $this->html($row['party']),
'description' => $this->html($row['description']), 'leisure' => $this->html($row['leisure']),
'descriptionExtended' => $this->html($row['description_extended']), ],
'party' => $this->html($row['party']), ],
'leisure' => $this->html($row['leisure']), // concept
], default => $item + [
]; 'code' => $this->text($row['code']),
default: // concept 'text' => [
return $item + [ 'teaser' => $this->html($row['teaser']),
'code' => $this->text($row['code']), 'description' => $this->html($row['description']),
'text' => [ 'descriptionExtended' => $this->html($row['description_extended']),
'teaser' => $this->html($row['teaser']), 'popupText' => $this->html($row['popup_text']),
'description' => $this->html($row['description']), ],
'descriptionExtended' => $this->html($row['description_extended']), ],
'popupText' => $this->html($row['popup_text']), };
],
];
}
} }
private function serializeNews(array $rows): array private function serializeNews(array $rows): array
@@ -302,8 +267,13 @@ class RecordSerializer
* @param int[] $uids * @param int[] $uids
* @return array<int, int[]> related uids by $keyField, in $sortField order * @return array<int, int[]> related uids by $keyField, in $sortField order
*/ */
private function findMmUids(string $table, string $keyField, string $valueField, array $uids, string $sortField = 'sorting'): array private function findMmUids(
{ string $table,
string $keyField,
string $valueField,
array $uids,
string $sortField = 'sorting',
): array {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($table); $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
$rows = $queryBuilder $rows = $queryBuilder
->select($keyField, $valueField) ->select($keyField, $valueField)
@@ -386,7 +356,7 @@ class RecordSerializer
*/ */
private function findInfoboxTexts(array $infoboxUids): array private function findInfoboxTexts(array $infoboxUids): array
{ {
$infoboxUids = array_values(array_filter(array_map('intval', $infoboxUids))); $infoboxUids = array_values(array_filter(array_map(intval(...), $infoboxUids)));
if ([] === $infoboxUids) { if ([] === $infoboxUids) {
return []; return [];
@@ -15,25 +15,13 @@ use TYPO3\CMS\Core\Resource\File;
*/ */
class RteLinkResolver class RteLinkResolver
{ {
/** public function __construct(private readonly LinkService $linkService, private readonly UrlResolver $urlResolver)
* @var LinkService
*/
private $linkService;
/**
* @var UrlResolver
*/
private $urlResolver;
public function __construct(LinkService $linkService, UrlResolver $urlResolver)
{ {
$this->linkService = $linkService;
$this->urlResolver = $urlResolver;
} }
public function resolve(string $html): string public function resolve(string $html): string
{ {
if (false === strpos($html, 't3://')) { if (!str_contains($html, 't3://')) {
return $html; return $html;
} }
@@ -52,7 +40,7 @@ class RteLinkResolver
{ {
try { try {
$data = $this->linkService->resolve($link); $data = $this->linkService->resolve($link);
} catch (\Throwable $e) { } catch (\Throwable) {
return null; return null;
} }
@@ -13,22 +13,12 @@ use TYPO3\CMS\Core\Utility\MathUtility;
*/ */
class UrlResolver class UrlResolver
{ {
private const DOKTYPE_DEFAULT = 1; private const int DOKTYPE_DEFAULT = 1;
private const DOKTYPE_SHORTCUT = 4; private const int DOKTYPE_SHORTCUT = 4;
private const SHORTCUT_MODE_NONE = 0; private const int SHORTCUT_MODE_NONE = 0;
private const SHORTCUT_MODE_FIRST_SUBPAGE = 1; private const int SHORTCUT_MODE_FIRST_SUBPAGE = 1;
private const SHORTCUT_MODE_PARENT_PAGE = 3; private const int SHORTCUT_MODE_PARENT_PAGE = 3;
private const MAX_SHORTCUT_DEPTH = 5; private const int MAX_SHORTCUT_DEPTH = 5;
/**
* @var SiteFinder
*/
private $siteFinder;
/**
* @var PageTree
*/
private $pageTree;
/** /**
* @var string[] * @var string[]
@@ -45,10 +35,11 @@ class UrlResolver
*/ */
private $sitesByPage = []; private $sitesByPage = [];
public function __construct(SiteFinder $siteFinder, PageTree $pageTree, ContentFeedConfiguration $configuration) public function __construct(
{ private readonly SiteFinder $siteFinder,
$this->siteFinder = $siteFinder; private readonly PageTree $pageTree,
$this->pageTree = $pageTree; ContentFeedConfiguration $configuration,
) {
$this->includedSites = $configuration->getSites(); $this->includedSites = $configuration->getSites();
$this->newsDetailPages = $configuration->getNewsDetailPages(); $this->newsDetailPages = $configuration->getNewsDetailPages();
} }
@@ -63,7 +54,7 @@ class UrlResolver
foreach ($this->includedSites as $identifier) { foreach ($this->includedSites as $identifier) {
try { try {
$sites[] = $this->siteFinder->getSiteByIdentifier($identifier); $sites[] = $this->siteFinder->getSiteByIdentifier($identifier);
} catch (SiteNotFoundException $e) { } catch (SiteNotFoundException) {
// A misspelt identifier must not take the whole feed down; it just contributes nothing. // A misspelt identifier must not take the whole feed down; it just contributes nothing.
} }
} }
@@ -80,7 +71,7 @@ class UrlResolver
try { try {
$site = $this->siteFinder->getSiteByPageId($pageUid); $site = $this->siteFinder->getSiteByPageId($pageUid);
$this->sitesByPage[$pageUid] = in_array($site->getIdentifier(), $this->includedSites, true) ? $site : null; $this->sitesByPage[$pageUid] = in_array($site->getIdentifier(), $this->includedSites, true) ? $site : null;
} catch (SiteNotFoundException $e) { } catch (SiteNotFoundException) {
$this->sitesByPage[$pageUid] = null; $this->sitesByPage[$pageUid] = null;
} }
} }
@@ -100,7 +91,7 @@ class UrlResolver
$language = $site->getLanguageById($languageId); $language = $site->getLanguageById($languageId);
return (string) $site->getRouter()->generateUri($pageUid, ['_language' => $language] + $parameters); return (string) $site->getRouter()->generateUri($pageUid, ['_language' => $language] + $parameters);
} catch (\Throwable $e) { } catch (\Throwable) {
// Unroutable pages (missing slug, disabled language) are left out rather than failing the feed. // Unroutable pages (missing slug, disabled language) are left out rather than failing the feed.
return null; return null;
} }
@@ -2,6 +2,7 @@
namespace EP\EpProducts\Controller; namespace EP\EpProducts\Controller;
use League\Period\Period;
use EP\EpProducts\Domain\Model\Hotel; use EP\EpProducts\Domain\Model\Hotel;
use EP\EpProducts\MyEP\ApiClient as MyEpClient; use EP\EpProducts\MyEP\ApiClient as MyEpClient;
use EP\EpProducts\MyEP\ApiException; use EP\EpProducts\MyEP\ApiException;
@@ -12,11 +13,8 @@ use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class AjaxCalendarController extends ActionController class AjaxCalendarController extends ActionController
{ {
protected MyEpClient $myEpClient; public function __construct(protected MyEpClient $myEpClient)
public function __construct(MyEpClient $myEpClient)
{ {
$this->myEpClient = $myEpClient;
} }
public function priceCalendarAction(Hotel $hotel, int $months = 12): ResponseInterface public function priceCalendarAction(Hotel $hotel, int $months = 12): ResponseInterface
@@ -91,7 +89,7 @@ class AjaxCalendarController extends ActionController
if (null !== $lastDataDate) { if (null !== $lastDataDate) {
$dataEnd = $lastDataDate->modify('first day of next month')->setTime(0, 0, 0); $dataEnd = $lastDataDate->modify('first day of next month')->setTime(0, 0, 0);
if ($dataEnd > $period->getEndDate()) { if ($dataEnd > $period->getEndDate()) {
$period = new \League\Period\Period($period->getStartDate(), $dataEnd); $period = new Period($period->getStartDate(), $dataEnd);
} }
} }
} catch (ApiException $e) { } catch (ApiException $e) {
@@ -116,7 +114,7 @@ class AjaxCalendarController extends ActionController
public function priceTableAction(Hotel $hotel, ?string $year = null): ResponseInterface public function priceTableAction(Hotel $hotel, ?string $year = null): ResponseInterface
{ {
$year = $year ?? (new \DateTimeImmutable())->format('Y'); $year ??= new \DateTimeImmutable()->format('Y');
$hotelCode = trim($hotel->getCode() ?? ''); $hotelCode = trim($hotel->getCode() ?? '');
$this->tagPageCacheForHotel($hotelCode); $this->tagPageCacheForHotel($hotelCode);
@@ -212,7 +210,7 @@ class AjaxCalendarController extends ActionController
/** /**
* @return array<int, array<string, mixed>> * @return array<int, array<string, mixed>>
*/ */
private function buildCalendarMonths(\League\Period\Period $period): array private function buildCalendarMonths(Period $period): array
{ {
$months = []; $months = [];
$current = new \DateTimeImmutable($period->getStartDate()->format('Y-m-01 00:00:00')); $current = new \DateTimeImmutable($period->getStartDate()->format('Y-m-01 00:00:00'));
@@ -19,18 +19,12 @@ class AjaxFilterPanelController extends ActionController
*/ */
protected $filterService; protected $filterService;
/**
* @var SearchResultService
*/
private $searchResultService;
/** /**
* @param FilterService $filterService * @param FilterService $filterService
*/ */
public function __construct(FilterService $filterService, SearchResultService $searchResultService) public function __construct(FilterService $filterService, private SearchResultService $searchResultService)
{ {
$this->filterService = $filterService; $this->filterService = $filterService;
$this->searchResultService = $searchResultService;
} }
public function initializeAction(): void public function initializeAction(): void
@@ -117,7 +117,7 @@ class AjaxTableController extends ActionController
if (1 === count($dates)) { if (1 === count($dates)) {
$date = reset($dates); $date = reset($dates);
return (new ForwardResponse('pricetable'))->withExtensionName('ep_products')->withArguments([ return new ForwardResponse('pricetable')->withExtensionName('ep_products')->withArguments([
'product' => $product, 'product' => $product,
'hotel' => $hotel, 'hotel' => $hotel,
'date' => $date['dateUid'], 'date' => $date['dateUid'],
@@ -136,9 +136,7 @@ class AjaxTableController extends ActionController
'nonBookableDateUids' => $nonBookableDateUids, 'nonBookableDateUids' => $nonBookableDateUids,
]); ]);
$bookableDates = array_filter($allDates, function (array $date) { $bookableDates = array_filter($allDates, fn(array $date) => $date['available'] > 0);
return $date['available'] > 0;
});
$includedServices = $this->dateService->groupIncludedServices($bookableDates); $includedServices = $this->dateService->groupIncludedServices($bookableDates);
$altProductLink = null; $altProductLink = null;
@@ -275,12 +273,10 @@ class AjaxTableController extends ActionController
public function eventPricetableAction(Product $product): ResponseInterface public function eventPricetableAction(Product $product): ResponseInterface
{ {
$roomTypes = GeneralUtility::trimExplode(',', $this->settings['priceTableRoomTypes'] ?? ''); $roomTypes = GeneralUtility::trimExplode(',', $this->settings['priceTableRoomTypes'] ?? '');
$roomTypesPax = array_map(function ($type) { $roomTypesPax = array_map(fn($type) => (int) $type, $roomTypes);
return (int) $type;
}, $roomTypes);
$roomTypesLabels = array_map(function ($type) { $roomTypesLabels = array_map(function ($type) {
$label = (int) $type . 'er'; $label = (int) $type . 'er';
if (false !== strpos($type, '+')) { if (str_contains($type, '+')) {
$label .= '+'; $label .= '+';
} }
return $label; return $label;
@@ -16,19 +16,13 @@ class CityController extends ActionController
*/ */
protected $cityRepository; protected $cityRepository;
/**
* @var ProductDataService
*/
private $productDataService;
/** /**
* @param CityRepository $cityRepository * @param CityRepository $cityRepository
* @param ProductDataService $productDataService * @param ProductDataService $productDataService
*/ */
public function __construct(CityRepository $cityRepository, ProductDataService $productDataService) public function __construct(CityRepository $cityRepository, private readonly ProductDataService $productDataService)
{ {
$this->cityRepository = $cityRepository; $this->cityRepository = $cityRepository;
$this->productDataService = $productDataService;
} }
/** /**
@@ -23,21 +23,18 @@ class ConceptController extends ActionController
*/ */
protected $productDataService; protected $productDataService;
/**
* @var PageRepository
*/
private $pageRepository;
/** /**
* @param ConceptRepository $conceptRepository * @param ConceptRepository $conceptRepository
* @param ProductDataService $productDataService * @param ProductDataService $productDataService
* @param PageRepository $pageRepository * @param PageRepository $pageRepository
*/ */
public function __construct(ConceptRepository $conceptRepository, ProductDataService $productDataService, PageRepository $pageRepository) public function __construct(
{ ConceptRepository $conceptRepository,
ProductDataService $productDataService,
private readonly PageRepository $pageRepository,
) {
$this->conceptRepository = $conceptRepository; $this->conceptRepository = $conceptRepository;
$this->productDataService = $productDataService; $this->productDataService = $productDataService;
$this->pageRepository = $pageRepository;
} }
/** /**
@@ -10,22 +10,18 @@ use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class NewsletterController extends ActionController class NewsletterController extends ActionController
{ {
private ApiClient $apiClient; public function __construct(private readonly ApiClient $apiClient)
public function __construct(ApiClient $apiClient)
{ {
$this->apiClient = $apiClient;
} }
public function subscriptionFormAction(?NewsletterSubscriptionRequest $subscriptionRequest = null): ResponseInterface public function subscriptionFormAction(
{ ?NewsletterSubscriptionRequest $subscriptionRequest = null,
if (null === $subscriptionRequest) { ): ResponseInterface {
$subscriptionRequest = new NewsletterSubscriptionRequest(); $subscriptionRequest ??= new NewsletterSubscriptionRequest();
}
try { try {
$lists = $this->apiClient->getNewsletterLists(); $lists = $this->apiClient->getNewsletterLists();
} catch (ApiException $e) { } catch (ApiException) {
$lists = []; $lists = [];
} }
@@ -50,16 +50,6 @@ class ProductController extends ActionController
*/ */
protected $hotelDataService; protected $hotelDataService;
/**
* @var InfoboxRepository
*/
private $infoboxRepository;
/**
* @var PickupOverviewService
*/
private $pickupOverviewService;
/** /**
* @param ProductRepository $productRepository * @param ProductRepository $productRepository
* @param ProductDataService $productDataService * @param ProductDataService $productDataService
@@ -77,8 +67,8 @@ class ProductController extends ActionController
HotelDataService $hotelDataService, HotelDataService $hotelDataService,
DateService $dateService, DateService $dateService,
FilterService $filterService, FilterService $filterService,
InfoboxRepository $infoboxRepository, private readonly InfoboxRepository $infoboxRepository,
PickupOverviewService $pickupOverviewService private readonly PickupOverviewService $pickupOverviewService
) { ) {
$this->productRepository = $productRepository; $this->productRepository = $productRepository;
$this->productDataService = $productDataService; $this->productDataService = $productDataService;
@@ -86,8 +76,6 @@ class ProductController extends ActionController
$this->hotelDataService = $hotelDataService; $this->hotelDataService = $hotelDataService;
$this->dateService = $dateService; $this->dateService = $dateService;
$this->filterService = $filterService; $this->filterService = $filterService;
$this->infoboxRepository = $infoboxRepository;
$this->pickupOverviewService = $pickupOverviewService;
} }
/** /**
@@ -131,7 +119,7 @@ class ProductController extends ActionController
$arguments = [ $arguments = [
'product' => $product, 'product' => $product,
]; ];
return (new ForwardResponse('hotelList'))->withArguments($arguments); return new ForwardResponse('hotelList')->withArguments($arguments);
} }
// Use first hotel which should be the only one at this point // Use first hotel which should be the only one at this point
if ($product->getHotels()->count() > 0) { if ($product->getHotels()->count() > 0) {
@@ -42,9 +42,7 @@ class RegionController extends ActionController
$region = $this->regionRepository->findByIdentifier($regionUid); $region = $this->regionRepository->findByIdentifier($regionUid);
} }
$additionalRegionUids = GeneralUtility::trimExplode(',', ($this->settings['additionalRegionUids'] ?? ''), true); $additionalRegionUids = GeneralUtility::trimExplode(',', ($this->settings['additionalRegionUids'] ?? ''), true);
$additionalRegionUids = array_map(function ($uid) { $additionalRegionUids = array_map(fn($uid) => (int)$uid, $additionalRegionUids);
return (int)$uid;
}, $additionalRegionUids);
$teaserRegionUids = array_merge([$region->getUid()], $additionalRegionUids); $teaserRegionUids = array_merge([$region->getUid()], $additionalRegionUids);
$filterSettings = [ $filterSettings = [
'regionUids' => $teaserRegionUids, 'regionUids' => $teaserRegionUids,
@@ -27,18 +27,12 @@ class SearchController extends ActionController
*/ */
protected $filterService; protected $filterService;
/**
* @var SearchResultService
*/
private $searchResultService;
/** /**
* @param FilterService $filterService * @param FilterService $filterService
*/ */
public function __construct(FilterService $filterService, SearchResultService $searchResultService) public function __construct(FilterService $filterService, private SearchResultService $searchResultService)
{ {
$this->filterService = $filterService; $this->filterService = $filterService;
$this->searchResultService = $searchResultService;
} }
public function initializeSearchresultAction() public function initializeSearchresultAction()
@@ -57,7 +51,7 @@ class SearchController extends ActionController
// The fields of the search bar, which sends them here by GET // The fields of the search bar, which sends them here by GET
$searchbarParameters = array_filter( $searchbarParameters = array_filter(
array_intersect_key($this->request->getQueryParams(), array_flip(self::SEARCHBAR_PARAMETERS)), array_intersect_key($this->request->getQueryParams(), array_flip(self::SEARCHBAR_PARAMETERS)),
'is_string' is_string(...)
); );
// Anything but a real Y-m-d date is dropped: the URL is easy to edit, and // Anything but a real Y-m-d date is dropped: the URL is easy to edit, and
// DateUtility::formatDateRange() throws on what DateTime cannot read // DateUtility::formatDateRange() throws on what DateTime cannot read
@@ -14,22 +14,12 @@ use TYPO3\CMS\Frontend\Controller\ErrorController;
class TravelinfoController extends ActionController class TravelinfoController extends ActionController
{ {
private const SELECTION_ID_BOARD = 71; private const int SELECTION_ID_BOARD = 71;
/** public function __construct(
* @var ApiClient private readonly ApiClient $apiClient,
*/ private readonly TravelinfoRepository $travelinfoRepository,
private $apiClient; ) {
/**
* @var TravelinfoRepository
*/
private $travelinfoRepository;
public function __construct(ApiClient $apiClient, TravelinfoRepository $travelinfoRepository)
{
$this->apiClient = $apiClient;
$this->travelinfoRepository = $travelinfoRepository;
} }
public function travelCodeAction(string $travelCode): ResponseInterface public function travelCodeAction(string $travelCode): ResponseInterface
@@ -86,14 +76,12 @@ class TravelinfoController extends ActionController
try { try {
$pickupsPlanning = $this->apiClient->getPickupsPlanning($travelCode); $pickupsPlanning = $this->apiClient->getPickupsPlanning($travelCode);
} catch (ApiException $e) { } catch (ApiException) {
$pickupsPlanning = []; $pickupsPlanning = [];
} }
// extract courses from additional services // extract courses from additional services
$courses = array_filter($travelData['additionalServices'] ?? [], function ($service) { $courses = array_filter($travelData['additionalServices'] ?? [], fn($service) => 'KUR' === $service['subType']);
return 'KUR' === $service['subType'];
});
// extract board services from included services // extract board services from included services
$includedServicesBoard = $travelData['selectionGroups'][self::SELECTION_ID_BOARD] ?? []; $includedServicesBoard = $travelData['selectionGroups'][self::SELECTION_ID_BOARD] ?? [];
@@ -101,9 +89,7 @@ class TravelinfoController extends ActionController
// extract bus departure time if applicable // extract bus departure time if applicable
$departureDayTime = null; $departureDayTime = null;
$service = array_filter($travelData['transportationServices'] ?? [], function ($service) { $service = array_filter($travelData['transportationServices'] ?? [], fn($service) => 'BUS' === $service['subType'] && 'RUECK' === $service['direction']);
return 'BUS' === $service['subType'] && 'RUECK' === $service['direction'];
});
if ($serviceDetail = reset($service)) { if ($serviceDetail = reset($service)) {
$departureDayTime = $serviceDetail['dayTime']; $departureDayTime = $serviceDetail['dayTime'];
} }
@@ -15,6 +15,7 @@ class Country extends AbstractDestination implements TeaserInterface
/** /**
* @var string * @var string
*/ */
#[\Override]
protected $descriptionExtended = ''; protected $descriptionExtended = '';
/** /**
@@ -98,9 +98,7 @@ class NewsletterSubscriptionRequest implements JsonSerializable
'firstName' => $this->firstName, 'firstName' => $this->firstName,
'lastName' => $this->lastName, 'lastName' => $this->lastName,
'email' => $this->email, 'email' => $this->email,
'listIds' => array_map(function($value) { 'listIds' => array_map(fn($value) => (int) $value, $this->listIds),
return (int) $value;
}, $this->listIds),
]; ];
} }
} }
@@ -5,11 +5,6 @@ namespace EP\EpProducts\Domain\Model;
class SearchResult class SearchResult
{ {
/**
* @var int
*/
protected $total = 0;
/** /**
* @var array * @var array
*/ */
@@ -19,10 +14,9 @@ class SearchResult
* @param array $data * @param array $data
* @param int $total * @param int $total
*/ */
public function __construct(array $data = [], $total = 0) public function __construct(array $data = [], protected $total = 0)
{ {
$this->data = $data; $this->data = $data;
$this->total = $total;
} }
/** /**
@@ -6,11 +6,13 @@ use EP\EpProducts\Domain\Model\AbstractDestination;
class CityRepository extends AbstractRepository class CityRepository extends AbstractRepository
{ {
public function getTeasersByCountryOrRegionAndSeason(int $regionUid = 0, int $countryUid = 0, string $season = 'w', int $storagePid = 0) public function getTeasersByCountryOrRegionAndSeason(
{ int $regionUid = 0,
if ($season === null) { int $countryUid = 0,
$season = AbstractDestination::SEASON_WINTER; string $season = 'w',
} int $storagePid = 0,
) {
$season ??= AbstractDestination::SEASON_WINTER;
$query = $this->createQuery(); $query = $this->createQuery();
$constraints = [ $constraints = [
$query->equals('season', $season), $query->equals('season', $season),
@@ -8,9 +8,7 @@ class RegionRepository extends AbstractRepository
{ {
public function getTeasersByCountryAndSeason(int $countryUid, string $season = 'w', int $storagePid = 0) public function getTeasersByCountryAndSeason(int $countryUid, string $season = 'w', int $storagePid = 0)
{ {
if ($season === null) { $season ??= AbstractDestination::SEASON_WINTER;
$season = AbstractDestination::SEASON_WINTER;
}
$query = $this->createQuery(); $query = $this->createQuery();
$constraints = [ $constraints = [
$query->equals('country', $countryUid), $query->equals('country', $countryUid),
@@ -10,6 +10,7 @@ class TeammemberRepository extends AbstractRepository
/** /**
* @var array * @var array
*/ */
#[\Override]
protected $defaultOrderings = [ protected $defaultOrderings = [
'sorting' => 'ASC', 'sorting' => 'ASC',
]; ];
@@ -25,15 +25,15 @@ class ApiClient implements LoggerAwareInterface
/** /**
* Applied when extension configuration does not override them. Seconds. * Applied when extension configuration does not override them. Seconds.
*/ */
private const DEFAULT_TIMEOUT = 5; private const int DEFAULT_TIMEOUT = 5;
private const DEFAULT_MAX_DURATION = 15; private const int DEFAULT_MAX_DURATION = 15;
/** /**
* Status codes the endpoint answers a stored submission with. A conflict is included for the * Status codes the endpoint answers a stored submission with. A conflict is included for the
* dedupe the team application may add later - it is not returned today - because a * dedupe the team application may add later - it is not returned today - because a
* double-clicked submit button is not something to report. * double-clicked submit button is not something to report.
*/ */
private const ACCEPTED_STATUS_CODES = [ private const array ACCEPTED_STATUS_CODES = [
Response::HTTP_OK, Response::HTTP_OK,
Response::HTTP_CREATED, Response::HTTP_CREATED,
Response::HTTP_ACCEPTED, Response::HTTP_ACCEPTED,
@@ -46,7 +46,7 @@ class ApiClient implements LoggerAwareInterface
* offending field in its body, which is the only way to tell from a log why a submission * offending field in its body, which is the only way to tell from a log why a submission
* was refused. * was refused.
*/ */
private const ERROR_BODY_EXCERPT_LENGTH = 500; private const int ERROR_BODY_EXCERPT_LENGTH = 500;
/** /**
* Shared, like the MyE&P client: Symfony's CurlHttpClient keeps its connection pool and TLS * Shared, like the MyE&P client: Symfony's CurlHttpClient keeps its connection pool and TLS
@@ -126,7 +126,7 @@ class ApiClient implements LoggerAwareInterface
{ {
try { try {
$body = trim($response->getContent(false)); $body = trim($response->getContent(false));
} catch (TransportExceptionInterface $e) { } catch (TransportExceptionInterface) {
return '<no response body>'; return '<no response body>';
} }
@@ -151,7 +151,7 @@ class ApiClient implements LoggerAwareInterface
{ {
try { try {
$data = $response->toArray(false); $data = $response->toArray(false);
} catch (\Throwable $e) { } catch (\Throwable) {
// A body that is absent (204) or not JSON costs nothing here - the submission is // A body that is absent (204) or not JSON costs nothing here - the submission is
// stored either way, which the asserted status has already established. // stored either way, which the asserted status has already established.
return; return;
@@ -185,7 +185,7 @@ class ApiClient implements LoggerAwareInterface
if ('' === $apiKey) { if ('' === $apiKey) {
throw new ApiException('EP Team API key is not configured.'); throw new ApiException('EP Team API key is not configured.');
} }
if (0 !== strpos($baseUrl, 'https://')) { if (!str_starts_with($baseUrl, 'https://')) {
// The application answers an http request with a 301 to the https URL, and this // The application answers an http request with a 301 to the https URL, and this
// client does not follow redirects, so a plain http base URL fails every submission. // client does not follow redirects, so a plain http base URL fails every submission.
$this->logWarning(sprintf('EP Team API base URL is not https (%s); submissions will fail on the redirect.', $baseUrl)); $this->logWarning(sprintf('EP Team API base URL is not https (%s); submissions will fail on the redirect.', $baseUrl));
@@ -220,10 +220,8 @@ class ApiClient implements LoggerAwareInterface
private function getConfiguration(): array private function getConfiguration(): array
{ {
if (null === $this->configuration) { $this->configuration ??= GeneralUtility::makeInstance(ExtensionConfiguration::class)
$this->configuration = GeneralUtility::makeInstance(ExtensionConfiguration::class) ->get('ep_products');
->get('ep_products');
}
return $this->configuration; return $this->configuration;
} }
@@ -14,14 +14,8 @@ use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
*/ */
class ApiKeyAuthenticator class ApiKeyAuthenticator
{ {
/** public function __construct(private readonly ExtensionConfiguration $extensionConfiguration)
* @var ExtensionConfiguration
*/
private $extensionConfiguration;
public function __construct(ExtensionConfiguration $extensionConfiguration)
{ {
$this->extensionConfiguration = $extensionConfiguration;
} }
public function isAuthorized(ServerRequestInterface $request): bool public function isAuthorized(ServerRequestInterface $request): bool
@@ -19,57 +19,15 @@ use TYPO3\CMS\Extbase\Service\ImageService;
class ApiMiddleware implements MiddlewareInterface class ApiMiddleware implements MiddlewareInterface
{ {
/**
* @var MyEpImageService
*/
private $hotelImageService;
/**
* @var MyEpImageService
*/
private $productImageService;
/**
* @var MyEpImageService
*/
private $regionImageService;
/**
* @var FileRepository
*/
private $fileRepository;
/**
* @var ImageService
*/
private $imageService;
/**
* @var ContentFeedApi
*/
private $contentFeedApi;
/**
* @var ApiKeyAuthenticator
*/
private $authenticator;
public function __construct( public function __construct(
MyEpImageService $hotelImageService, private readonly MyEpImageService $hotelImageService,
MyEpImageService $productImageService, private readonly MyEpImageService $productImageService,
MyEpImageService $regionImageService, private readonly MyEpImageService $regionImageService,
FileRepository $fileRepository, private readonly FileRepository $fileRepository,
ImageService $imageService, private readonly ImageService $imageService,
ContentFeedApi $contentFeedApi, private readonly ContentFeedApi $contentFeedApi,
ApiKeyAuthenticator $authenticator private readonly ApiKeyAuthenticator $authenticator,
) { ) {
$this->hotelImageService = $hotelImageService;
$this->productImageService = $productImageService;
$this->regionImageService = $regionImageService;
$this->fileRepository = $fileRepository;
$this->imageService = $imageService;
$this->contentFeedApi = $contentFeedApi;
$this->authenticator = $authenticator;
} }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
@@ -84,7 +42,7 @@ class ApiMiddleware implements MiddlewareInterface
return $this->handleProductRequest($request); return $this->handleProductRequest($request);
} }
if (0 === strpos($path, ContentFeedApi::PATH_PREFIX)) { if (str_starts_with($path, ContentFeedApi::PATH_PREFIX)) {
return $this->contentFeedApi->handle($request); return $this->contentFeedApi->handle($request);
} }
@@ -309,12 +267,10 @@ class ApiMiddleware implements MiddlewareInterface
private function serializeFileReferences(array $files): array private function serializeFileReferences(array $files): array
{ {
return array_map(function ($file) { return array_map(fn($file) => [
return [ 'url' => $this->imageService->getImageUri($file, true),
'url' => $this->imageService->getImageUri($file, true), 'alt' => $file->getProperty('alternative'),
'alt' => $file->getProperty('alternative'), 'title' => $file->getProperty('title'),
'title' => $file->getProperty('title'), ], $files);
];
}, $files);
} }
} }
@@ -24,20 +24,10 @@ use TYPO3\CMS\Frontend\Controller\ErrorController;
*/ */
class ContentPageTypeGuardMiddleware implements MiddlewareInterface class ContentPageTypeGuardMiddleware implements MiddlewareInterface
{ {
/** public function __construct(
* @var ApiKeyAuthenticator private readonly ApiKeyAuthenticator $authenticator,
*/ private readonly ContentFeedConfiguration $configuration,
private $authenticator; ) {
/**
* @var ContentFeedConfiguration
*/
private $configuration;
public function __construct(ApiKeyAuthenticator $authenticator, ContentFeedConfiguration $configuration)
{
$this->authenticator = $authenticator;
$this->configuration = $configuration;
} }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
@@ -27,7 +27,7 @@ class StaticSearchParamsMiddleware implements MiddlewareInterface
$params = $request->getAttribute('normalizedParams'); $params = $request->getAttribute('normalizedParams');
$uri = $params->getRequestUri(); $uri = $params->getRequestUri();
if (strpos($uri, self::PREFIX_PATH_SEGMENT) === false) { if (!str_contains($uri, self::PREFIX_PATH_SEGMENT)) {
return $handler->handle($request); return $handler->handle($request);
} }
+27 -20
View File
@@ -28,31 +28,31 @@ class ApiClient implements LoggerAwareInterface
/** /**
* Applied when extension configuration does not override them. Seconds. * Applied when extension configuration does not override them. Seconds.
*/ */
private const DEFAULT_TIMEOUT = 5; private const int DEFAULT_TIMEOUT = 5;
private const DEFAULT_MAX_DURATION = 15; private const int DEFAULT_MAX_DURATION = 15;
/** /**
* Cache identifier for the shared OAuth access token. The token is valid for the whole * Cache identifier for the shared OAuth access token. The token is valid for the whole
* installation, so caching it here saves one round trip per request that would otherwise * installation, so caching it here saves one round trip per request that would otherwise
* have to re-authenticate before it can fetch anything. * have to re-authenticate before it can fetch anything.
*/ */
private const ACCESS_TOKEN_CACHE_IDENTIFIER = 'myep_api_access_token'; private const string ACCESS_TOKEN_CACHE_IDENTIFIER = 'myep_api_access_token';
/** /**
* Safety margin in seconds. A token that is about to expire is treated as expired so it * Safety margin in seconds. A token that is about to expire is treated as expired so it
* cannot lapse between the cache read and the API call that uses it. * cannot lapse between the cache read and the API call that uses it.
*/ */
private const ACCESS_TOKEN_EXPIRY_MARGIN = 60; private const int ACCESS_TOKEN_EXPIRY_MARGIN = 60;
/** /**
* Fallback lifetime for tokens that do not report an expiry. * Fallback lifetime for tokens that do not report an expiry.
*/ */
private const ACCESS_TOKEN_DEFAULT_TTL = 300; private const int ACCESS_TOKEN_DEFAULT_TTL = 300;
/** /**
* Status codes that mean the token was rejected rather than the request being wrong. * Status codes that mean the token was rejected rather than the request being wrong.
*/ */
private const UNAUTHORIZED_STATUS_CODES = [Response::HTTP_UNAUTHORIZED, Response::HTTP_FORBIDDEN]; private const array UNAUTHORIZED_STATUS_CODES = [Response::HTTP_UNAUTHORIZED, Response::HTTP_FORBIDDEN];
private static ?AccessToken $accessToken = null; private static ?AccessToken $accessToken = null;
@@ -192,8 +192,11 @@ class ApiClient implements LoggerAwareInterface
* *
* @throws ApiException * @throws ApiException
*/ */
public function getContingentCalendar(string $hotelCode, \DateTimeInterface $dateFrom, \DateTimeInterface $dateTo): array public function getContingentCalendar(
{ string $hotelCode,
\DateTimeInterface $dateFrom,
\DateTimeInterface $dateTo,
): array {
return $this->requestJson('GET', 'contingents/calendar', [ return $this->requestJson('GET', 'contingents/calendar', [
'query' => [ 'query' => [
'hotelCode' => $hotelCode, 'hotelCode' => $hotelCode,
@@ -219,8 +222,12 @@ class ApiClient implements LoggerAwareInterface
/** /**
* @throws ApiException * @throws ApiException
*/ */
private function requestJson(string $method, string $uri, array $options = [], array $expectedStatusCodes = [Response::HTTP_OK]): array private function requestJson(
{ string $method,
string $uri,
array $options = [],
array $expectedStatusCodes = [Response::HTTP_OK],
): array {
$response = $this->request($method, $uri, $options); $response = $this->request($method, $uri, $options);
$this->assertStatusCode($response, $expectedStatusCodes, $method, $uri); $this->assertStatusCode($response, $expectedStatusCodes, $method, $uri);
@@ -271,8 +278,12 @@ class ApiClient implements LoggerAwareInterface
* *
* @throws ApiException * @throws ApiException
*/ */
private function assertStatusCode(ResponseInterface $response, array $expectedStatusCodes, string $method, string $uri): void private function assertStatusCode(
{ ResponseInterface $response,
array $expectedStatusCodes,
string $method,
string $uri,
): void {
$statusCode = $this->readStatusCode($response, $method, $uri); $statusCode = $this->readStatusCode($response, $method, $uri);
if (true === in_array($statusCode, $expectedStatusCodes, true)) { if (true === in_array($statusCode, $expectedStatusCodes, true)) {
@@ -312,7 +323,7 @@ class ApiClient implements LoggerAwareInterface
{ {
try { try {
$response->getContent(false); $response->getContent(false);
} catch (TransportExceptionInterface $e) { } catch (TransportExceptionInterface) {
// The status has already been asserted, so a failure here costs nothing but the // The status has already been asserted, so a failure here costs nothing but the
// connection reuse this method exists for. // connection reuse this method exists for.
} }
@@ -375,19 +386,15 @@ class ApiClient implements LoggerAwareInterface
*/ */
private function getCache(): FrontendInterface private function getCache(): FrontendInterface
{ {
if (null === $this->cache) { $this->cache ??= GeneralUtility::makeInstance(CacheManager::class)->getCache('ep_products_cache');
$this->cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('ep_products_cache');
}
return $this->cache; return $this->cache;
} }
private function getConfiguration(): array private function getConfiguration(): array
{ {
if (null === $this->configuration) { $this->configuration ??= GeneralUtility::makeInstance(ExtensionConfiguration::class)
$this->configuration = GeneralUtility::makeInstance(ExtensionConfiguration::class) ->get('ep_products');
->get('ep_products');
}
return $this->configuration; return $this->configuration;
} }
@@ -27,25 +27,19 @@ final class ApiImportSource implements ProductImportSource, SingletonInterface,
/** /**
* The whole export is one page at current volumes; the walk does not rely on it. * The whole export is one page at current volumes; the walk does not rely on it.
*/ */
private const PER_PAGE = 500; private const int PER_PAGE = 500;
/** /**
* How often the walk restarts when MyE&P replaces its copy of the export * How often the walk restarts when MyE&P replaces its copy of the export
* mid-walk before giving up until the next run. * mid-walk before giving up until the next run.
*/ */
private const MAX_WALKS = 3; private const int MAX_WALKS = 3;
private const STAGING_SUBDIR = 'ep_products_import'; private const string STAGING_SUBDIR = 'ep_products_import';
private const BASELINE_FILE = 'api-imported.info'; private const string BASELINE_FILE = 'api-imported.info';
/** public function __construct(private ?ApiClient $apiClient = null)
* @var ApiClient|null
*/
private $apiClient;
public function __construct(?ApiClient $apiClient = null)
{ {
$this->apiClient = $apiClient;
} }
/** /**
@@ -202,16 +196,14 @@ final class ApiImportSource implements ProductImportSource, SingletonInterface,
try { try {
return new \DateTimeImmutable($value); return new \DateTimeImmutable($value);
} catch (\Exception $e) { } catch (\Exception) {
return null; return null;
} }
} }
private function getApiClient(): ApiClient private function getApiClient(): ApiClient
{ {
if ($this->apiClient === null) { $this->apiClient ??= GeneralUtility::makeInstance(ApiClient::class);
$this->apiClient = GeneralUtility::makeInstance(ApiClient::class);
}
return $this->apiClient; return $this->apiClient;
} }
@@ -157,9 +157,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$products = []; $products = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$code = $row['product']['code']; $code = $row['product']['code'];
if (!isset($products[$code])) { $products[$code] ??= ['busProId' => (int) $row['product']['busProId'], 'dates' => []];
$products[$code] = ['busProId' => (int) $row['product']['busProId'], 'dates' => []];
}
$products[$code]['dates'][(int) $row['date']['busProId']][] = $row; $products[$code]['dates'][(int) $row['date']['busProId']][] = $row;
} }
@@ -401,9 +399,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$servicesTransportationOptional = []; $servicesTransportationOptional = [];
if (array_key_exists('BUS', $servicesTransportation)) { if (array_key_exists('BUS', $servicesTransportation)) {
$servicesTransportationOptional = array_filter($servicesTransportation['BUS'], function ($service) { $servicesTransportationOptional = array_filter($servicesTransportation['BUS'], fn($service) => $service['price'] !== 0);
return $service['price'] !== 0;
});
} }
if (array_key_exists('BAH', $servicesTransportation)) { if (array_key_exists('BAH', $servicesTransportation)) {
$servicesTransportationOptional = array_merge( $servicesTransportationOptional = array_merge(
@@ -433,9 +429,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
foreach ($combinedServices as $key => $list) foreach ($combinedServices as $key => $list)
{ {
uasort($list, function ($a, $b) { uasort($list, fn($a, $b) => strcmp($a['label'], $b['label']));
return strcmp($a['label'], $b['label']);
});
$combinedServices[$key] = $list; $combinedServices[$key] = $list;
} }
$date['services_general'] = serialize($combinedServices); $date['services_general'] = serialize($combinedServices);
@@ -488,7 +482,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$code = $row['hotel']['code']; $code = $row['hotel']['code'];
$busProId = (int) $row['hotel']['busProId']; $busProId = (int) $row['hotel']['busProId'];
if (!array_key_exists($code, $this->hotelMappings)) { if (!array_key_exists((string) $code, $this->hotelMappings)) {
$this->logger->warning('hotel not found', ['code' => $code]); $this->logger->warning('hotel not found', ['code' => $code]);
return false; return false;
@@ -503,9 +497,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$this->addServices($generalServices, $services); $this->addServices($generalServices, $services);
$this->addServices($hotelServices, $services); $this->addServices($hotelServices, $services);
foreach ($services as $key => $list) { foreach ($services as $key => $list) {
uasort($list, function ($a, $b) { uasort($list, fn($a, $b) => strcmp($a['label'], $b['label']));
return strcmp($a['label'], $b['label']);
});
$services[$key] = $list; $services[$key] = $list;
} }
@@ -696,7 +688,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$label = static::LABEL_TRAIN; $label = static::LABEL_TRAIN;
} }
if (!array_key_exists($type, $data)) { if (!array_key_exists((string) $type, $data)) {
$data[$type] = []; $data[$type] = [];
} }
@@ -841,7 +833,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
return [$parts[3] . '.' . $parts[2] . '.' . $parts[1], $parts[4] . ':' . $parts[5]]; return [$parts[3] . '.' . $parts[2] . '.' . $parts[1], $parts[4] . ':' . $parts[5]];
} }
$pickupDateTime = (new \DateTimeImmutable($datetime))->setTimezone(new \DateTimeZone('Europe/Berlin')); $pickupDateTime = new \DateTimeImmutable($datetime)->setTimezone(new \DateTimeZone('Europe/Berlin'));
return [$pickupDateTime->format('d.m.Y'), $pickupDateTime->format('H:i')]; return [$pickupDateTime->format('d.m.Y'), $pickupDateTime->format('H:i')];
} }
@@ -97,9 +97,7 @@ class DateService implements SingletonInterface
$myEpUrl = $resolvedOptions['myEpUrl']; $myEpUrl = $resolvedOptions['myEpUrl'];
$paCode = $resolvedOptions['paCode'] ?? null; $paCode = $resolvedOptions['paCode'] ?? null;
if ($filterSettings === null) { $filterSettings ??= $this->filterService->getNormalizedFilterSettings();
$filterSettings = $this->filterService->getNormalizedFilterSettings();
}
$nonBookableDateUids = GeneralUtility::trimExplode(',', $product->getNonBookableDates(), true); $nonBookableDateUids = GeneralUtility::trimExplode(',', $product->getNonBookableDates(), true);
@@ -137,9 +135,7 @@ class DateService implements SingletonInterface
$hotel = $resolvedOptions['hotel']; $hotel = $resolvedOptions['hotel'];
$filterSettings = $resolvedOptions['filterSettings'] ?? null; $filterSettings = $resolvedOptions['filterSettings'] ?? null;
if ($filterSettings === null) { $filterSettings ??= $this->filterService->getNormalizedFilterSettings();
$filterSettings = $this->filterService->getNormalizedFilterSettings();
}
return $this->productRepository->getAvailableDates($product, $hotel, $filterSettings); return $this->productRepository->getAvailableDates($product, $hotel, $filterSettings);
} }
@@ -231,16 +227,14 @@ class DateService implements SingletonInterface
$processed = []; $processed = [];
foreach ($servicesFlat as $key => $services) { foreach ($servicesFlat as $key => $services) {
$servicesWithCosts = array_filter($services, function ($service) { $servicesWithCosts = array_filter($services, fn($service) => '0,00' !== $service['price']);
return '0,00' !== $service['price'];
});
if (0 === count($servicesWithCosts)) { if (0 === count($servicesWithCosts)) {
continue; continue;
} }
if (array_key_exists($key, $this->serviceSectionsMapping)) { if (array_key_exists($key, $this->serviceSectionsMapping)) {
$key = $this->serviceSectionsMapping[$key]; $key = $this->serviceSectionsMapping[$key];
} }
if (!array_key_exists($key, $processed)) { if (!array_key_exists((string) $key, $processed)) {
$processed[$key] = [ $processed[$key] = [
'label' => $this->serviceSections[$key] ?? null, 'label' => $this->serviceSections[$key] ?? null,
'services' => [], 'services' => [],
@@ -345,7 +339,7 @@ class DateService implements SingletonInterface
} }
$hotelUid = $row['hotelUid']; $hotelUid = $row['hotelUid'];
if (!array_key_exists($hotelUid, $priceTable[$categoryUid])) { if (!array_key_exists((string) $hotelUid, $priceTable[$categoryUid])) {
$priceTable[$categoryUid][$hotelUid] = [ $priceTable[$categoryUid][$hotelUid] = [
'category' => $categoryUid, 'category' => $categoryUid,
@@ -375,7 +369,7 @@ class DateService implements SingletonInterface
// Add new entry or replace possible existing entry that is not // Add new entry or replace possible existing entry that is not
// bookable with current record in case of matching category // bookable with current record in case of matching category
// and pax // and pax
if (!array_key_exists($roomPax, $entry)) { if (!array_key_exists((string) $roomPax, $entry)) {
$entry[$roomPax] = [ $entry[$roomPax] = [
'price' => $row['roomPrice'], 'price' => $row['roomPrice'],
'available' => $row['available'] && $row['roomPrice'], 'available' => $row['available'] && $row['roomPrice'],
@@ -398,12 +392,10 @@ class DateService implements SingletonInterface
{ {
// The rooms are keyed by pax, so this compares the 4-person prices; hotels without // The rooms are keyed by pax, so this compares the 4-person prices; hotels without
// one compare as '' (what PHP 7.4 read from the missing offset) // one compare as '' (what PHP 7.4 read from the missing offset)
uasort($rows, function ($a, $b) { uasort($rows, fn($a, $b) => strcmp(
return strcmp( $a['rooms'][Hotel::CATEGORY_SUPER_DELUXE]['price'] ?? '',
$a['rooms'][Hotel::CATEGORY_SUPER_DELUXE]['price'] ?? '', $b['rooms'][Hotel::CATEGORY_SUPER_DELUXE]['price'] ?? ''
$b['rooms'][Hotel::CATEGORY_SUPER_DELUXE]['price'] ?? '' ));
);
});
$priceTable[$category] = $rows; $priceTable[$category] = $rows;
} }
@@ -412,13 +404,7 @@ class DateService implements SingletonInterface
public function hasIncludedServices(array $tableData): bool public function hasIncludedServices(array $tableData): bool
{ {
foreach ($tableData as $row) { return array_any($tableData, fn($row) => ($row['busIncluded'] ?? null) || ($row['skiPassIncluded'] ?? null));
if (($row['busIncluded'] ?? null) || ($row['skiPassIncluded'] ?? null)) {
return true;
}
}
return false;
} }
/** /**
@@ -39,21 +39,17 @@ class FilterService implements SingletonInterface
'bus' => null, 'bus' => null,
]); ]);
foreach (['hotelUid', 'productUid', 'nights', 'pax'] as $key) { foreach (['hotelUid', 'productUid', 'nights', 'pax'] as $key) {
$optionsResolver->setNormalizer($key, function (Options $options, $value) { $optionsResolver->setNormalizer($key, fn(Options $options, $value) => null !== $value ? (int)$value : null);
return null !== $value ? (int)$value : null;
});
} }
foreach (['productsPid', 'priceRanges', 'countryUids', 'regionUids', 'cityUids', 'conceptUids', 'boardTypes', 'hotelTypes', 'roomTypes'] as $key) { foreach (['productsPid', 'priceRanges', 'countryUids', 'regionUids', 'cityUids', 'conceptUids', 'boardTypes', 'hotelTypes', 'roomTypes'] as $key) {
$optionsResolver->setNormalizer($key, function (Options $options, $value) { $optionsResolver->setNormalizer($key, function (Options $options, $value) {
if (empty($value)) { if (empty($value)) {
return []; return [];
} }
return array_map('intval', (array) $value); return array_map(intval(...), (array) $value);
}); });
} }
$optionsResolver->setNormalizer('bus', function (Options $options, $value) { $optionsResolver->setNormalizer('bus', fn(Options $options, $value) => null !== $value ? (bool)$value : null);
return null !== $value ? (bool)$value : null;
});
$filterSettings = $optionsResolver->resolve($filterSettings); $filterSettings = $optionsResolver->resolve($filterSettings);
@@ -250,9 +246,7 @@ class FilterService implements SingletonInterface
} }
foreach ($destinations as $key => $value) { foreach ($destinations as $key => $value) {
usort($value['regions'], function ($a, $b) { usort($value['regions'], fn($a, $b) => strcmp($a['label'], $b['label']));
return strcmp($a['label'], $b['label']);
});
$destinations[$key] = $value; $destinations[$key] = $value;
} }
sort($destinations); sort($destinations);
@@ -260,9 +254,7 @@ class FilterService implements SingletonInterface
sort($roomTypes); sort($roomTypes);
ksort($priceRanges); ksort($priceRanges);
usort($boardTypes, function ($a, $b) { usort($boardTypes, fn($a, $b) => strcmp($a['label'], $b['label']));
return strcmp($a['label'], $b['label']);
});
$filterOptions = new FilterOptions(); $filterOptions = new FilterOptions();
$filterOptions->setPriceRanges($priceRanges); $filterOptions->setPriceRanges($priceRanges);
@@ -22,11 +22,11 @@ class GroupsSwissService implements LoggerAwareInterface
{ {
use LoggerAwareTrait; use LoggerAwareTrait;
private const API_URL = 'https://www.groups.swiss/api/1.0/json.php'; private const string API_URL = 'https://www.groups.swiss/api/1.0/json.php';
private const STATE_FREE = 0; private const int STATE_FREE = 0;
private const STATE_ON_REQUEST = 1; private const int STATE_ON_REQUEST = 1;
private const STATE_BLOCKED = 2; private const int STATE_BLOCKED = 2;
private HttpClientInterface $httpClient; private HttpClientInterface $httpClient;
@@ -116,7 +116,7 @@ class HotelDataService implements SingletonInterface
$hotelCategory = Hotel::$categoryLabels[$row['hotelCategory']] ?? null; $hotelCategory = Hotel::$categoryLabels[$row['hotelCategory']] ?? null;
if (!array_key_exists($hotelUid, $teasers)) { if (!array_key_exists((string) $hotelUid, $teasers)) {
$teasers[$hotelUid] = [ $teasers[$hotelUid] = [
'minDateStart' => null, 'minDateStart' => null,
'maxDateEnd' => null, 'maxDateEnd' => null,
@@ -6,6 +6,7 @@ class HotelImageService extends AbstractImageService
/** /**
* @var string * @var string
*/ */
#[\Override]
protected $table = 'tx_epproducts_domain_model_hotel'; protected $table = 'tx_epproducts_domain_model_hotel';
/** /**
@@ -19,14 +19,8 @@ use EP\EpProducts\Domain\Repository\DateRepository;
*/ */
class PickupOverviewService class PickupOverviewService
{ {
/** public function __construct(private readonly DateRepository $dateRepository)
* @var DateRepository
*/
private $dateRepository;
public function __construct(DateRepository $dateRepository)
{ {
$this->dateRepository = $dateRepository;
} }
/** /**
@@ -6,6 +6,7 @@ class ProductImageService extends AbstractImageService
/** /**
* @var string * @var string
*/ */
#[\Override]
protected $table = 'tx_epproducts_domain_model_product'; protected $table = 'tx_epproducts_domain_model_product';
/** /**
@@ -7,33 +7,18 @@ namespace EP\EpProducts\Service;
* {@see DateImportService::import()}, the source's own timestamp, and whatever * {@see DateImportService::import()}, the source's own timestamp, and whatever
* the source needs to find it again in {@see ProductImportSource::markImported()}. * the source needs to find it again in {@see ProductImportSource::markImported()}.
*/ */
final class ProductImport final readonly class ProductImport
{ {
/**
* @var iterable<array>
*/
private $rows;
/**
* @var \DateTimeImmutable|null
*/
private $sourceGeneratedAt;
/**
* @var string|null
*/
private $location;
/** /**
* @param iterable<array> $rows * @param iterable<array> $rows
* @param \DateTimeImmutable|null $sourceGeneratedAt * @param \DateTimeImmutable|null $sourceGeneratedAt
* @param string|null $location local directory of an XML export, null otherwise * @param string|null $location local directory of an XML export, null otherwise
*/ */
public function __construct(iterable $rows, ?\DateTimeImmutable $sourceGeneratedAt, ?string $location = null) public function __construct(
{ private iterable $rows,
$this->rows = $rows; private ?\DateTimeImmutable $sourceGeneratedAt,
$this->sourceGeneratedAt = $sourceGeneratedAt; private ?string $location = null,
$this->location = $location; ) {
} }
/** /**
@@ -93,12 +93,6 @@ class RoomTypeResolver
*/ */
protected function matchesAny(string $label, array $patterns): bool protected function matchesAny(string $label, array $patterns): bool
{ {
foreach ($patterns as $pattern) { return array_any($patterns, fn($pattern) => str_contains(mb_strtolower($label), mb_strtolower($pattern)));
if (mb_strpos(mb_strtolower($label), mb_strtolower($pattern)) !== false) {
return true;
}
}
return false;
} }
} }
@@ -249,9 +249,7 @@ class SearchResultService implements SingletonInterface
continue; continue;
} }
uasort($section['items'], function ($a, $b) { uasort($section['items'], fn($a, $b) => strcmp((string)$a['minPrice'], (string)$b['minPrice']));
return strcmp((string)$a['minPrice'], (string)$b['minPrice']);
});
} }
// Turn result data into simple array to ensure order in js // Turn result data into simple array to ensure order in js
@@ -23,25 +23,25 @@ class SnowreportImportService implements LoggerAwareInterface
{ {
use LoggerAwareTrait; use LoggerAwareTrait;
private const FEED_URL = 'https://www.skiresort-service.com/xml-feed/'; private const string FEED_URL = 'https://www.skiresort-service.com/xml-feed/';
// A healthy response arrives in well under a second; an outage should fail visibly // A healthy response arrives in well under a second; an outage should fail visibly
private const HTTP_CONNECT_TIMEOUT = 5; private const int HTTP_CONNECT_TIMEOUT = 5;
private const HTTP_TIMEOUT = 30; private const int HTTP_TIMEOUT = 30;
private const SNOWREPORT_TABLE = 'tx_epproducts_domain_model_snowreport'; private const string SNOWREPORT_TABLE = 'tx_epproducts_domain_model_snowreport';
private const WEBCAM_TABLE = 'tx_epproducts_domain_model_webcam'; private const string WEBCAM_TABLE = 'tx_epproducts_domain_model_webcam';
private const TYPE_INT = 'int'; private const string TYPE_INT = 'int';
private const TYPE_STRING = 'string'; private const string TYPE_STRING = 'string';
private const TYPE_BOOL = 'bool'; private const string TYPE_BOOL = 'bool';
private const TYPE_DATE = 'date'; private const string TYPE_DATE = 'date';
/** /**
* XML element => column and type. XML has no types, so each field is declared here and converted * XML element => column and type. XML has no types, so each field is declared here and converted
* strictly; unexpected values are logged and fall back to the type's empty value. * strictly; unexpected values are logged and fall back to the type's empty value.
*/ */
private const MAP = [ private const array MAP = [
'region_id' => ['column' => 'vendor_uid', 'type' => self::TYPE_INT], 'region_id' => ['column' => 'vendor_uid', 'type' => self::TYPE_INT],
'region_name' => ['column' => 'region_name', 'type' => self::TYPE_STRING], 'region_name' => ['column' => 'region_name', 'type' => self::TYPE_STRING],
'region_offen' => ['column' => 'opened', 'type' => self::TYPE_BOOL], 'region_offen' => ['column' => 'opened', 'type' => self::TYPE_BOOL],
@@ -59,7 +59,7 @@ class SnowreportImportService implements LoggerAwareInterface
'datum_saisonende' => ['column' => 'season_end', 'type' => self::TYPE_DATE], 'datum_saisonende' => ['column' => 'season_end', 'type' => self::TYPE_DATE],
]; ];
private const BOOLEANS = [ private const array BOOLEANS = [
'true' => true, '1' => true, 'ja' => true, 'yes' => true, 'true' => true, '1' => true, 'ja' => true, 'yes' => true,
'false' => false, '0' => false, 'nein' => false, 'no' => false, 'false' => false, '0' => false, 'nein' => false, 'no' => false,
]; ];
@@ -270,7 +270,7 @@ class SnowreportImportService implements LoggerAwareInterface
private function flagObsolete(string $table, array $currentVendorUids, array $validVendorUids, int $now): void private function flagObsolete(string $table, array $currentVendorUids, array $validVendorUids, int $now): void
{ {
$obsolete = array_values(array_diff(array_map('strval', $currentVendorUids), array_map('strval', $validVendorUids))); $obsolete = array_values(array_diff(array_map(strval(...), $currentVendorUids), array_map(strval(...), $validVendorUids)));
if ([] === $obsolete) { if ([] === $obsolete) {
return; return;
} }
@@ -314,7 +314,7 @@ class SnowreportImportService implements LoggerAwareInterface
$this->conversionWarnings[sprintf('<%s> expected a date, got "%s".', $key, $raw)] = true; $this->conversionWarnings[sprintf('<%s> expected a date, got "%s".', $key, $raw)] = true;
return 0; return 0;
} }
return (new \DateTime($raw))->getTimestamp(); return new \DateTime($raw)->getTimestamp();
} }
throw new \InvalidArgumentException(sprintf('Unknown field type "%s" declared for <%s>.', $type, $key), 1758000000); throw new \InvalidArgumentException(sprintf('Unknown field type "%s" declared for <%s>.', $type, $key), 1758000000);
@@ -20,9 +20,7 @@ class TypeConversionUtility
return (int) $value; return (int) $value;
} }
if (\is_array($value)) { if (\is_array($value)) {
return array_map(function($item) { return array_map(fn($item) => static::convertType($item), $value);
return static::convertType($item);
}, $value);
} }
if (empty($value)) { if (empty($value)) {
return null; return null;
@@ -1,12 +1,13 @@
<?php <?php
use EP\EpProducts\Middleware\StaticSearchParamsMiddleware;
use EP\EpProducts\Middleware\ApiMiddleware; use EP\EpProducts\Middleware\ApiMiddleware;
use EP\EpProducts\Middleware\ContentPageTypeGuardMiddleware; use EP\EpProducts\Middleware\ContentPageTypeGuardMiddleware;
return [ return [
'frontend' => [ 'frontend' => [
'ep/staticsearch' => [ 'ep/staticsearch' => [
'target' => \EP\EpProducts\Middleware\StaticSearchParamsMiddleware::class, 'target' => StaticSearchParamsMiddleware::class,
'before' => [ 'before' => [
'typo3/cms-frontend/page-resolver', 'typo3/cms-frontend/page-resolver',
], ],
+2 -1
View File
@@ -1,4 +1,5 @@
<?php <?php
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility; use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use EP\EpProducts\Controller\ProductController; use EP\EpProducts\Controller\ProductController;
use EP\EpProducts\Controller\CityController; use EP\EpProducts\Controller\CityController;
@@ -42,7 +43,7 @@ $boot = function () {
// Client-side callers pass these parameters in POST bodies, which cHash never inspects. // Client-side callers pass these parameters in POST bodies, which cHash never inspects.
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['requireCacheHashPresenceParameters'][] = '^tx_epproducts_ajax'; $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['requireCacheHashPresenceParameters'][] = '^tx_epproducts_ajax';
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask::class]['options']['tables']['tx_epproducts_log'] = [ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables']['tx_epproducts_log'] = [
'dateField' => 'time_micro', 'dateField' => 'time_micro',
'expirePeriod' => 180, 'expirePeriod' => 180,
]; ];
@@ -9,11 +9,13 @@ class Page extends AbstractEntity
/** /**
* @var int * @var int
*/ */
#[\Override]
protected ?int $uid = null; protected ?int $uid = null;
/** /**
* @var int * @var int
*/ */
#[\Override]
protected ?int $pid = null; protected ?int $pid = null;
/** /**
@@ -10,6 +10,7 @@ class PageRepository extends Repository
/** /**
* @var array * @var array
*/ */
#[\Override]
protected $defaultOrderings = [ protected $defaultOrderings = [
'sorting' => 'ASC', 'sorting' => 'ASC',
]; ];
@@ -46,7 +46,7 @@ abstract class AbstractMappedFinisher extends AbstractFinisher
$formValues = $this->finisherContext->getFormValues(); $formValues = $this->finisherContext->getFormValues();
foreach ($this->parseMapping() as $remote => $local) { foreach ($this->parseMapping() as $remote => $local) {
if (false === array_key_exists($local, $formValues)) { if (false === array_key_exists((string) $local, $formValues)) {
continue; continue;
} }
@@ -130,13 +130,13 @@ abstract class AbstractMappedFinisher extends AbstractFinisher
if ($value instanceof FileReference) { if ($value instanceof FileReference) {
$resource = $value->getOriginalResource(); $resource = $value->getOriginalResource();
return null !== $resource ? $resource->getName() : null; return $resource?->getName();
} }
if (true === is_object($value)) { if (true === is_object($value)) {
$this->logWarning('Form value skipped while collecting labeled values: unsupported type.', [ $this->logWarning('Form value skipped while collecting labeled values: unsupported type.', [
'element' => $identifier, 'element' => $identifier,
'class' => get_class($value), 'class' => $value::class,
]); ]);
return null; return null;
@@ -31,7 +31,7 @@ class ApplicationApiFinisher extends AbstractMappedFinisher
// without keeping a list of identifiers. Taken from the definition rather than // without keeping a list of identifiers. Taken from the definition rather than
// through TranslationService: a translated label would differ per site language. // through TranslationService: a translated label would differ per site language.
$payload['formLabel'] = '' !== $formLabel ? $formLabel : $formDefinition->getIdentifier(); $payload['formLabel'] = '' !== $formLabel ? $formLabel : $formDefinition->getIdentifier();
$payload['submittedAt'] = (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM); $payload['submittedAt'] = new \DateTimeImmutable()->format(\DateTimeInterface::ATOM);
$payload = array_merge($payload, $this->resolvePageContext()); $payload = array_merge($payload, $this->resolvePageContext());
$payload['details'] = $this->buildLabeledValues(); $payload['details'] = $this->buildLabeledValues();
@@ -32,11 +32,11 @@ use TYPO3\CMS\Core\Http\Stream;
* *
* Registered after the page resolver, so the /api/* responses of ep_products never pass here. * Registered after the page resolver, so the /api/* responses of ep_products never pass here.
*/ */
final class CdnImageMiddleware implements MiddlewareInterface final readonly class CdnImageMiddleware implements MiddlewareInterface
{ {
private const PATTERN = '/(["\'])\/?(fileadmin|uploads|typo3temp\/assets\/(?:_processed_|images))/'; private const string PATTERN = '/(["\'])\/?(fileadmin|uploads|typo3temp\/assets\/(?:_processed_|images))/';
public function __construct(private readonly ExtensionConfiguration $extensionConfiguration) public function __construct(private ExtensionConfiguration $extensionConfiguration)
{ {
} }
@@ -19,6 +19,7 @@ class ContainerViewHelper extends AbstractViewHelper
/** /**
* @var bool * @var bool
*/ */
#[\Override]
protected $escapeOutput = false; protected $escapeOutput = false;
/** /**
@@ -45,9 +46,7 @@ class ContainerViewHelper extends AbstractViewHelper
public function render() public function render()
{ {
$content = $this->renderChildren(); $content = $this->renderChildren();
if ($content === null) { $content ??= $this->arguments['content'];
$content = $this->arguments['content'];
}
$classes = []; $classes = [];
if (\in_array($this->arguments['data']['colPos'] ?? null, static::$columnsWithoutContainers, false)) { if (\in_array($this->arguments['data']['colPos'] ?? null, static::$columnsWithoutContainers, false)) {
$classes[] = 'container'; $classes[] = 'container';
@@ -11,6 +11,7 @@ class FilterSettingsEncoderViewHelper extends AbstractViewHelper
/** /**
* @var bool * @var bool
*/ */
#[\Override]
protected $escapeOutput = false; protected $escapeOutput = false;
public function initializeArguments() public function initializeArguments()
@@ -29,9 +30,7 @@ class FilterSettingsEncoderViewHelper extends AbstractViewHelper
public function render() public function render()
{ {
$argument = $this->renderChildren(); $argument = $this->renderChildren();
if ($argument === null) { $argument ??= $this->arguments['argument'];
$argument = $this->arguments['argument'];
}
if ($argument === null) { if ($argument === null) {
return ''; return '';
} }
@@ -12,6 +12,7 @@ class GmapsUrlViewHelper extends AbstractViewHelper
/** /**
* @var bool * @var bool
*/ */
#[\Override]
protected $escapeOutput = false; protected $escapeOutput = false;
const URLBASE = 'http://maps.google.com/maps?q=%s,%s&t=%s&z=%d'; const URLBASE = 'http://maps.google.com/maps?q=%s,%s&t=%s&z=%d';
@@ -11,6 +11,7 @@ class IconViewHelper extends AbstractTagBasedViewHelper
/** /**
* @var string * @var string
*/ */
#[\Override]
protected $tagName = 'svg'; protected $tagName = 'svg';
public function initializeArguments() public function initializeArguments()
@@ -11,6 +11,7 @@ class RatingViewHelper extends AbstractViewHelper
/** /**
* @var bool * @var bool
*/ */
#[\Override]
protected $escapeOutput = false; protected $escapeOutput = false;
public function initializeArguments() public function initializeArguments()
@@ -29,9 +30,7 @@ class RatingViewHelper extends AbstractViewHelper
public function render() public function render()
{ {
$product = $this->renderChildren(); $product = $this->renderChildren();
if ($product === null) { $product ??= $this->arguments['product'];
$product = $this->arguments['product'];
}
$ratingAverage = $product->getRatingAverage(); $ratingAverage = $product->getRatingAverage();
$ratingVotesCount = $product->getRatingVotesCount(); $ratingVotesCount = $product->getRatingVotesCount();
$fullStars = floor($ratingAverage); $fullStars = floor($ratingAverage);
@@ -24,7 +24,7 @@ class ThemeClassesViewHelper extends AbstractViewHelper
public function render() public function render()
{ {
$themeKey = $this->arguments['key']; $themeKey = $this->arguments['key'];
if (!array_key_exists($themeKey, $this->arguments['classes'])) { if (!array_key_exists((string) $themeKey, $this->arguments['classes'])) {
return ''; return '';
} }
return $this->arguments['classes'][$themeKey]; return $this->arguments['classes'][$themeKey];
@@ -10,6 +10,7 @@ class VideoViewHelper extends AbstractViewHelper
/** /**
* @var bool * @var bool
*/ */
#[\Override]
protected $escapeOutput = false; protected $escapeOutput = false;
public function initializeArguments() public function initializeArguments()
@@ -1,15 +1,19 @@
<?php <?php
use EP\EpTheme\Middleware\CdnImageMiddleware;
use EP\EpTheme\Middleware\ReferrerMiddleware;
use EP\EpTheme\Middleware\HtmxRedirectMiddleware;
return [ return [
'frontend' => [ 'frontend' => [
'ep/cdn-images' => [ 'ep/cdn-images' => [
'target' => \EP\EpTheme\Middleware\CdnImageMiddleware::class, 'target' => CdnImageMiddleware::class,
'after' => [ 'after' => [
'typo3/cms-frontend/page-resolver', 'typo3/cms-frontend/page-resolver',
], ],
], ],
'ep/referrer' => [ 'ep/referrer' => [
'target' => \EP\EpTheme\Middleware\ReferrerMiddleware::class, 'target' => ReferrerMiddleware::class,
'after' => [ 'after' => [
'typo3/cms-frontend/authentication', 'typo3/cms-frontend/authentication',
], ],
@@ -20,7 +24,7 @@ return [
// Outermost of the frontend stack after the site is known, so it also sees redirects // Outermost of the frontend stack after the site is known, so it also sees redirects
// that core middlewares or exceptions (PropagateResponseException) produce // that core middlewares or exceptions (PropagateResponseException) produce
'ep/htmx-redirect' => [ 'ep/htmx-redirect' => [
'target' => \EP\EpTheme\Middleware\HtmxRedirectMiddleware::class, 'target' => HtmxRedirectMiddleware::class,
'after' => [ 'after' => [
'typo3/cms-frontend/site', 'typo3/cms-frontend/site',
], ],
@@ -55,7 +55,7 @@ call_user_func(function () {
$row[] = $config; $row[] = $config;
} }
GeneralUtility::makeInstance(Registry::class)->configureContainer( GeneralUtility::makeInstance(Registry::class)->configureContainer(
(new ContainerConfiguration($cType, $label, $description, [$row])) new ContainerConfiguration($cType, $label, $description, [$row])
->setIcon('tx_eptheme_gridelements_' . $icon) ->setIcon('tx_eptheme_gridelements_' . $icon)
); );
} }
@@ -113,7 +113,8 @@ return [
'config' => [ 'config' => [
'type' => 'input', 'type' => 'input',
'size' => 30, 'size' => 30,
'eval' => 'trim,required' 'eval' => 'trim',
'required' => true
], ],
], ],
'description' => [ 'description' => [
+1
View File
@@ -27,6 +27,7 @@ return RectorConfig::configure()
MigratePluginContentElementAndPluginSubtypesTCARector::class, MigratePluginContentElementAndPluginSubtypesTCARector::class,
]) ])
->withPhpVersion(PhpVersion::PHP_85) ->withPhpVersion(PhpVersion::PHP_85)
->withPhpSets(php85: true)
->withSets([ ->withSets([
Typo3SetList::GENERAL, Typo3SetList::GENERAL,
Typo3LevelSetList::UP_TO_TYPO3_13, Typo3LevelSetList::UP_TO_TYPO3_13,