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/';
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
// 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;
/**
* @var ApiKeyAuthenticator
*/
private $authenticator;
/**
* @var ContentFeedConfiguration
*/
private $configuration;
/**
* @var ManifestBuilder
*/
private $manifestBuilder;
/**
* @var RecordSerializer
*/
private $recordSerializer;
/**
* @var FrontendInterface
*/
private $cache;
public function __construct(
ApiKeyAuthenticator $authenticator,
ContentFeedConfiguration $configuration,
ManifestBuilder $manifestBuilder,
RecordSerializer $recordSerializer,
private readonly ApiKeyAuthenticator $authenticator,
private readonly ContentFeedConfiguration $configuration,
private readonly ManifestBuilder $manifestBuilder,
private readonly RecordSerializer $recordSerializer,
CacheManager $cacheManager
) {
$this->authenticator = $authenticator;
$this->configuration = $configuration;
$this->manifestBuilder = $manifestBuilder;
$this->recordSerializer = $recordSerializer;
$this->cache = $cacheManager->getCache('ep_products_cache');
}
@@ -112,9 +88,7 @@ class ContentFeedApi
$uids = array_values(array_unique(array_filter(
GeneralUtility::intExplode(',', (string) ($request->getQueryParams()['uids'] ?? ''), true),
function (int $uid): bool {
return $uid > 0;
}
fn(int $uid): bool => $uid > 0
)));
if ([] === $uids) {
@@ -16,13 +16,13 @@ class ManifestBuilder
{
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.
private const DOKTYPES_NOT_TRAVERSED = [6, 255];
private const array DOKTYPES_NOT_TRAVERSED = [6, 255];
// 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'],
'hotel' => ['uid', 'tstamp', 'detail_page'],
'region' => ['uid', 'tstamp', 'detail_page'],
@@ -32,43 +32,13 @@ class ManifestBuilder
'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(
PageTree $pageTree,
UrlResolver $urlResolver,
RecordFinder $recordFinder,
ContentFeedConfiguration $configuration,
ConnectionPool $connectionPool
private readonly PageTree $pageTree,
private readonly UrlResolver $urlResolver,
private readonly RecordFinder $recordFinder,
private readonly ContentFeedConfiguration $configuration,
private readonly ConnectionPool $connectionPool,
) {
$this->pageTree = $pageTree;
$this->urlResolver = $urlResolver;
$this->recordFinder = $recordFinder;
$this->configuration = $configuration;
$this->connectionPool = $connectionPool;
}
public function build(): array
@@ -78,7 +48,7 @@ class ManifestBuilder
return [
'generatedAt' => date('c'),
'sites' => array_map([$this, 'describeSite'], $sites),
'sites' => array_map($this->describeSite(...), $sites),
'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);
}
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'];
// "Show content from page": the elements live on another page, and so do their changes.
// The site answers 404 when that page is hidden.
@@ -15,17 +15,12 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
*/
class PageTree
{
private const FIELDS = [
private const array FIELDS = [
'uid', 'pid', 'sorting', 'doktype', 'hidden', 'starttime', 'endtime', 'fe_group',
'extendToSubpages', 'no_index', 'title', 'nav_title', 'description', 'shortcut',
'shortcut_mode', 'content_from_pid', 'tstamp', 'sys_language_uid', 'l10n_parent', 'l18n_cfg',
];
/**
* @var ConnectionPool
*/
private $connectionPool;
/**
* @var int
*/
@@ -46,9 +41,8 @@ class PageTree
*/
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');
}
@@ -26,14 +26,8 @@ class RecordFinder
'news' => 'tx_news_domain_model_news',
];
/**
* @var ConnectionPool
*/
private $connectionPool;
public function __construct(ConnectionPool $connectionPool)
public function __construct(private readonly ConnectionPool $connectionPool)
{
$this->connectionPool = $connectionPool;
}
/**
@@ -18,36 +18,12 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
*/
class RecordSerializer
{
/**
* @var RecordFinder
*/
private $recordFinder;
/**
* @var UrlResolver
*/
private $urlResolver;
/**
* @var RteLinkResolver
*/
private $rteLinkResolver;
/**
* @var ConnectionPool
*/
private $connectionPool;
public function __construct(
RecordFinder $recordFinder,
UrlResolver $urlResolver,
RteLinkResolver $rteLinkResolver,
ConnectionPool $connectionPool
private readonly RecordFinder $recordFinder,
private readonly UrlResolver $urlResolver,
private readonly RteLinkResolver $rteLinkResolver,
private readonly ConnectionPool $connectionPool,
) {
$this->recordFinder = $recordFinder;
$this->urlResolver = $urlResolver;
$this->rteLinkResolver = $rteLinkResolver;
$this->connectionPool = $connectionPool;
}
/**
@@ -62,18 +38,12 @@ class RecordSerializer
return [];
}
switch ($type) {
case 'product':
return $this->serializeProducts($rows);
case 'hotel':
return $this->serializeHotels($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));
}
return match ($type) {
'product' => $this->serializeProducts($rows),
'hotel' => $this->serializeHotels($rows),
'news' => $this->serializeNews($rows),
default => array_values(array_map(fn(array $row): array => $this->serializeDestination($type, $row), $rows)),
};
}
private function serializeProducts(array $rows): array
@@ -137,7 +107,7 @@ class RecordSerializer
foreach ($rows as $uid => $row) {
$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)) + [
'code' => '' === $code ? null : $code,
@@ -152,9 +122,7 @@ class RecordSerializer
'countryUid' => (int) $row['country'] ?: null,
'regionUid' => (int) $row['region'] ?: null,
'cityUid' => (int) $row['city'] ?: null,
'productUids' => array_values(array_filter($products[$uid] ?? [], function (int $productUid) use ($eligibleProducts): bool {
return isset($eligibleProducts[$productUid]);
})),
'productUids' => array_values(array_filter($products[$uid] ?? [], fn(int $productUid): bool => isset($eligibleProducts[$productUid]))),
'address' => $this->text($row['address']),
'latitude' => $this->text($row['latitude']),
'longitude' => $this->text($row['longitude']),
@@ -192,64 +160,61 @@ class RecordSerializer
'headline' => $this->text($row['headline']),
];
switch ($type) {
case 'region':
return $item + [
'season' => $this->text($row['season']),
'keywords' => $this->text($row['keywords']),
'countryUid' => (int) $row['country'] ?: null,
'facts' => [
'length' => $this->text($row['length']),
'height' => $this->text($row['height']),
'lifts' => $this->text($row['lifts']),
],
'webcam' => $this->findWebcamUrl((int) $row['webcam']),
'text' => [
'teaser' => $this->html($row['teaser']),
'description' => $this->html($row['description']),
'feature' => $this->text($row['feature']),
'skiArea' => $this->html($row['ski_area']),
'skiAreaExtended' => $this->html($row['ski_area_extended']),
'skiPass' => $this->html($row['ski_pass']),
'news' => $this->html($row['news']),
],
];
case 'country':
return $item + [
'code' => $this->text($row['code']),
'season' => $this->text($row['season']),
'keywords' => $this->text($row['keywords']),
'text' => [
'teaser' => $this->html($row['teaser']),
'description' => $this->html($row['description']),
'descriptionExtended' => $this->html($row['description_extended']),
],
];
case 'city':
return $item + [
'season' => $this->text($row['season']),
'keywords' => $this->text($row['keywords']),
'countryUid' => (int) $row['country'] ?: null,
'regionUid' => (int) $row['region'] ?: null,
'text' => [
'teaser' => $this->html($row['teaser']),
'description' => $this->html($row['description']),
'descriptionExtended' => $this->html($row['description_extended']),
'party' => $this->html($row['party']),
'leisure' => $this->html($row['leisure']),
],
];
default: // concept
return $item + [
'code' => $this->text($row['code']),
'text' => [
'teaser' => $this->html($row['teaser']),
'description' => $this->html($row['description']),
'descriptionExtended' => $this->html($row['description_extended']),
'popupText' => $this->html($row['popup_text']),
],
];
}
return match ($type) {
'region' => $item + [
'season' => $this->text($row['season']),
'keywords' => $this->text($row['keywords']),
'countryUid' => (int) $row['country'] ?: null,
'facts' => [
'length' => $this->text($row['length']),
'height' => $this->text($row['height']),
'lifts' => $this->text($row['lifts']),
],
'webcam' => $this->findWebcamUrl((int) $row['webcam']),
'text' => [
'teaser' => $this->html($row['teaser']),
'description' => $this->html($row['description']),
'feature' => $this->text($row['feature']),
'skiArea' => $this->html($row['ski_area']),
'skiAreaExtended' => $this->html($row['ski_area_extended']),
'skiPass' => $this->html($row['ski_pass']),
'news' => $this->html($row['news']),
],
],
'country' => $item + [
'code' => $this->text($row['code']),
'season' => $this->text($row['season']),
'keywords' => $this->text($row['keywords']),
'text' => [
'teaser' => $this->html($row['teaser']),
'description' => $this->html($row['description']),
'descriptionExtended' => $this->html($row['description_extended']),
],
],
'city' => $item + [
'season' => $this->text($row['season']),
'keywords' => $this->text($row['keywords']),
'countryUid' => (int) $row['country'] ?: null,
'regionUid' => (int) $row['region'] ?: null,
'text' => [
'teaser' => $this->html($row['teaser']),
'description' => $this->html($row['description']),
'descriptionExtended' => $this->html($row['description_extended']),
'party' => $this->html($row['party']),
'leisure' => $this->html($row['leisure']),
],
],
// concept
default => $item + [
'code' => $this->text($row['code']),
'text' => [
'teaser' => $this->html($row['teaser']),
'description' => $this->html($row['description']),
'descriptionExtended' => $this->html($row['description_extended']),
'popupText' => $this->html($row['popup_text']),
],
],
};
}
private function serializeNews(array $rows): array
@@ -302,8 +267,13 @@ class RecordSerializer
* @param int[] $uids
* @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);
$rows = $queryBuilder
->select($keyField, $valueField)
@@ -386,7 +356,7 @@ class RecordSerializer
*/
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) {
return [];
@@ -15,25 +15,13 @@ use TYPO3\CMS\Core\Resource\File;
*/
class RteLinkResolver
{
/**
* @var LinkService
*/
private $linkService;
/**
* @var UrlResolver
*/
private $urlResolver;
public function __construct(LinkService $linkService, UrlResolver $urlResolver)
public function __construct(private readonly LinkService $linkService, private readonly UrlResolver $urlResolver)
{
$this->linkService = $linkService;
$this->urlResolver = $urlResolver;
}
public function resolve(string $html): string
{
if (false === strpos($html, 't3://')) {
if (!str_contains($html, 't3://')) {
return $html;
}
@@ -52,7 +40,7 @@ class RteLinkResolver
{
try {
$data = $this->linkService->resolve($link);
} catch (\Throwable $e) {
} catch (\Throwable) {
return null;
}
@@ -13,22 +13,12 @@ use TYPO3\CMS\Core\Utility\MathUtility;
*/
class UrlResolver
{
private const DOKTYPE_DEFAULT = 1;
private const DOKTYPE_SHORTCUT = 4;
private const SHORTCUT_MODE_NONE = 0;
private const SHORTCUT_MODE_FIRST_SUBPAGE = 1;
private const SHORTCUT_MODE_PARENT_PAGE = 3;
private const MAX_SHORTCUT_DEPTH = 5;
/**
* @var SiteFinder
*/
private $siteFinder;
/**
* @var PageTree
*/
private $pageTree;
private const int DOKTYPE_DEFAULT = 1;
private const int DOKTYPE_SHORTCUT = 4;
private const int SHORTCUT_MODE_NONE = 0;
private const int SHORTCUT_MODE_FIRST_SUBPAGE = 1;
private const int SHORTCUT_MODE_PARENT_PAGE = 3;
private const int MAX_SHORTCUT_DEPTH = 5;
/**
* @var string[]
@@ -45,10 +35,11 @@ class UrlResolver
*/
private $sitesByPage = [];
public function __construct(SiteFinder $siteFinder, PageTree $pageTree, ContentFeedConfiguration $configuration)
{
$this->siteFinder = $siteFinder;
$this->pageTree = $pageTree;
public function __construct(
private readonly SiteFinder $siteFinder,
private readonly PageTree $pageTree,
ContentFeedConfiguration $configuration,
) {
$this->includedSites = $configuration->getSites();
$this->newsDetailPages = $configuration->getNewsDetailPages();
}
@@ -63,7 +54,7 @@ class UrlResolver
foreach ($this->includedSites as $identifier) {
try {
$sites[] = $this->siteFinder->getSiteByIdentifier($identifier);
} catch (SiteNotFoundException $e) {
} catch (SiteNotFoundException) {
// A misspelt identifier must not take the whole feed down; it just contributes nothing.
}
}
@@ -80,7 +71,7 @@ class UrlResolver
try {
$site = $this->siteFinder->getSiteByPageId($pageUid);
$this->sitesByPage[$pageUid] = in_array($site->getIdentifier(), $this->includedSites, true) ? $site : null;
} catch (SiteNotFoundException $e) {
} catch (SiteNotFoundException) {
$this->sitesByPage[$pageUid] = null;
}
}
@@ -100,7 +91,7 @@ class UrlResolver
$language = $site->getLanguageById($languageId);
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.
return null;
}
@@ -2,6 +2,7 @@
namespace EP\EpProducts\Controller;
use League\Period\Period;
use EP\EpProducts\Domain\Model\Hotel;
use EP\EpProducts\MyEP\ApiClient as MyEpClient;
use EP\EpProducts\MyEP\ApiException;
@@ -12,11 +13,8 @@ use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class AjaxCalendarController extends ActionController
{
protected MyEpClient $myEpClient;
public function __construct(MyEpClient $myEpClient)
public function __construct(protected MyEpClient $myEpClient)
{
$this->myEpClient = $myEpClient;
}
public function priceCalendarAction(Hotel $hotel, int $months = 12): ResponseInterface
@@ -91,7 +89,7 @@ class AjaxCalendarController extends ActionController
if (null !== $lastDataDate) {
$dataEnd = $lastDataDate->modify('first day of next month')->setTime(0, 0, 0);
if ($dataEnd > $period->getEndDate()) {
$period = new \League\Period\Period($period->getStartDate(), $dataEnd);
$period = new Period($period->getStartDate(), $dataEnd);
}
}
} catch (ApiException $e) {
@@ -116,7 +114,7 @@ class AjaxCalendarController extends ActionController
public function priceTableAction(Hotel $hotel, ?string $year = null): ResponseInterface
{
$year = $year ?? (new \DateTimeImmutable())->format('Y');
$year ??= new \DateTimeImmutable()->format('Y');
$hotelCode = trim($hotel->getCode() ?? '');
$this->tagPageCacheForHotel($hotelCode);
@@ -212,7 +210,7 @@ class AjaxCalendarController extends ActionController
/**
* @return array<int, array<string, mixed>>
*/
private function buildCalendarMonths(\League\Period\Period $period): array
private function buildCalendarMonths(Period $period): array
{
$months = [];
$current = new \DateTimeImmutable($period->getStartDate()->format('Y-m-01 00:00:00'));
@@ -19,18 +19,12 @@ class AjaxFilterPanelController extends ActionController
*/
protected $filterService;
/**
* @var SearchResultService
*/
private $searchResultService;
/**
* @param FilterService $filterService
*/
public function __construct(FilterService $filterService, SearchResultService $searchResultService)
public function __construct(FilterService $filterService, private SearchResultService $searchResultService)
{
$this->filterService = $filterService;
$this->searchResultService = $searchResultService;
}
public function initializeAction(): void
@@ -117,7 +117,7 @@ class AjaxTableController extends ActionController
if (1 === count($dates)) {
$date = reset($dates);
return (new ForwardResponse('pricetable'))->withExtensionName('ep_products')->withArguments([
return new ForwardResponse('pricetable')->withExtensionName('ep_products')->withArguments([
'product' => $product,
'hotel' => $hotel,
'date' => $date['dateUid'],
@@ -136,9 +136,7 @@ class AjaxTableController extends ActionController
'nonBookableDateUids' => $nonBookableDateUids,
]);
$bookableDates = array_filter($allDates, function (array $date) {
return $date['available'] > 0;
});
$bookableDates = array_filter($allDates, fn(array $date) => $date['available'] > 0);
$includedServices = $this->dateService->groupIncludedServices($bookableDates);
$altProductLink = null;
@@ -275,12 +273,10 @@ class AjaxTableController extends ActionController
public function eventPricetableAction(Product $product): ResponseInterface
{
$roomTypes = GeneralUtility::trimExplode(',', $this->settings['priceTableRoomTypes'] ?? '');
$roomTypesPax = array_map(function ($type) {
return (int) $type;
}, $roomTypes);
$roomTypesPax = array_map(fn($type) => (int) $type, $roomTypes);
$roomTypesLabels = array_map(function ($type) {
$label = (int) $type . 'er';
if (false !== strpos($type, '+')) {
if (str_contains($type, '+')) {
$label .= '+';
}
return $label;
@@ -16,19 +16,13 @@ class CityController extends ActionController
*/
protected $cityRepository;
/**
* @var ProductDataService
*/
private $productDataService;
/**
* @param CityRepository $cityRepository
* @param ProductDataService $productDataService
*/
public function __construct(CityRepository $cityRepository, ProductDataService $productDataService)
public function __construct(CityRepository $cityRepository, private readonly ProductDataService $productDataService)
{
$this->cityRepository = $cityRepository;
$this->productDataService = $productDataService;
}
/**
@@ -23,21 +23,18 @@ class ConceptController extends ActionController
*/
protected $productDataService;
/**
* @var PageRepository
*/
private $pageRepository;
/**
* @param ConceptRepository $conceptRepository
* @param ProductDataService $productDataService
* @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->productDataService = $productDataService;
$this->pageRepository = $pageRepository;
}
/**
@@ -10,22 +10,18 @@ use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class NewsletterController extends ActionController
{
private ApiClient $apiClient;
public function __construct(ApiClient $apiClient)
public function __construct(private readonly ApiClient $apiClient)
{
$this->apiClient = $apiClient;
}
public function subscriptionFormAction(?NewsletterSubscriptionRequest $subscriptionRequest = null): ResponseInterface
{
if (null === $subscriptionRequest) {
$subscriptionRequest = new NewsletterSubscriptionRequest();
}
public function subscriptionFormAction(
?NewsletterSubscriptionRequest $subscriptionRequest = null,
): ResponseInterface {
$subscriptionRequest ??= new NewsletterSubscriptionRequest();
try {
$lists = $this->apiClient->getNewsletterLists();
} catch (ApiException $e) {
} catch (ApiException) {
$lists = [];
}
@@ -50,16 +50,6 @@ class ProductController extends ActionController
*/
protected $hotelDataService;
/**
* @var InfoboxRepository
*/
private $infoboxRepository;
/**
* @var PickupOverviewService
*/
private $pickupOverviewService;
/**
* @param ProductRepository $productRepository
* @param ProductDataService $productDataService
@@ -77,8 +67,8 @@ class ProductController extends ActionController
HotelDataService $hotelDataService,
DateService $dateService,
FilterService $filterService,
InfoboxRepository $infoboxRepository,
PickupOverviewService $pickupOverviewService
private readonly InfoboxRepository $infoboxRepository,
private readonly PickupOverviewService $pickupOverviewService
) {
$this->productRepository = $productRepository;
$this->productDataService = $productDataService;
@@ -86,8 +76,6 @@ class ProductController extends ActionController
$this->hotelDataService = $hotelDataService;
$this->dateService = $dateService;
$this->filterService = $filterService;
$this->infoboxRepository = $infoboxRepository;
$this->pickupOverviewService = $pickupOverviewService;
}
/**
@@ -131,7 +119,7 @@ class ProductController extends ActionController
$arguments = [
'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
if ($product->getHotels()->count() > 0) {
@@ -42,9 +42,7 @@ class RegionController extends ActionController
$region = $this->regionRepository->findByIdentifier($regionUid);
}
$additionalRegionUids = GeneralUtility::trimExplode(',', ($this->settings['additionalRegionUids'] ?? ''), true);
$additionalRegionUids = array_map(function ($uid) {
return (int)$uid;
}, $additionalRegionUids);
$additionalRegionUids = array_map(fn($uid) => (int)$uid, $additionalRegionUids);
$teaserRegionUids = array_merge([$region->getUid()], $additionalRegionUids);
$filterSettings = [
'regionUids' => $teaserRegionUids,
@@ -27,18 +27,12 @@ class SearchController extends ActionController
*/
protected $filterService;
/**
* @var SearchResultService
*/
private $searchResultService;
/**
* @param FilterService $filterService
*/
public function __construct(FilterService $filterService, SearchResultService $searchResultService)
public function __construct(FilterService $filterService, private SearchResultService $searchResultService)
{
$this->filterService = $filterService;
$this->searchResultService = $searchResultService;
}
public function initializeSearchresultAction()
@@ -57,7 +51,7 @@ class SearchController extends ActionController
// The fields of the search bar, which sends them here by GET
$searchbarParameters = array_filter(
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
// DateUtility::formatDateRange() throws on what DateTime cannot read
@@ -14,22 +14,12 @@ use TYPO3\CMS\Frontend\Controller\ErrorController;
class TravelinfoController extends ActionController
{
private const SELECTION_ID_BOARD = 71;
private const int SELECTION_ID_BOARD = 71;
/**
* @var ApiClient
*/
private $apiClient;
/**
* @var TravelinfoRepository
*/
private $travelinfoRepository;
public function __construct(ApiClient $apiClient, TravelinfoRepository $travelinfoRepository)
{
$this->apiClient = $apiClient;
$this->travelinfoRepository = $travelinfoRepository;
public function __construct(
private readonly ApiClient $apiClient,
private readonly TravelinfoRepository $travelinfoRepository,
) {
}
public function travelCodeAction(string $travelCode): ResponseInterface
@@ -86,14 +76,12 @@ class TravelinfoController extends ActionController
try {
$pickupsPlanning = $this->apiClient->getPickupsPlanning($travelCode);
} catch (ApiException $e) {
} catch (ApiException) {
$pickupsPlanning = [];
}
// extract courses from additional services
$courses = array_filter($travelData['additionalServices'] ?? [], function ($service) {
return 'KUR' === $service['subType'];
});
$courses = array_filter($travelData['additionalServices'] ?? [], fn($service) => 'KUR' === $service['subType']);
// extract board services from included services
$includedServicesBoard = $travelData['selectionGroups'][self::SELECTION_ID_BOARD] ?? [];
@@ -101,9 +89,7 @@ class TravelinfoController extends ActionController
// extract bus departure time if applicable
$departureDayTime = null;
$service = array_filter($travelData['transportationServices'] ?? [], function ($service) {
return 'BUS' === $service['subType'] && 'RUECK' === $service['direction'];
});
$service = array_filter($travelData['transportationServices'] ?? [], fn($service) => 'BUS' === $service['subType'] && 'RUECK' === $service['direction']);
if ($serviceDetail = reset($service)) {
$departureDayTime = $serviceDetail['dayTime'];
}
@@ -15,6 +15,7 @@ class Country extends AbstractDestination implements TeaserInterface
/**
* @var string
*/
#[\Override]
protected $descriptionExtended = '';
/**
@@ -98,9 +98,7 @@ class NewsletterSubscriptionRequest implements JsonSerializable
'firstName' => $this->firstName,
'lastName' => $this->lastName,
'email' => $this->email,
'listIds' => array_map(function($value) {
return (int) $value;
}, $this->listIds),
'listIds' => array_map(fn($value) => (int) $value, $this->listIds),
];
}
}
@@ -5,11 +5,6 @@ namespace EP\EpProducts\Domain\Model;
class SearchResult
{
/**
* @var int
*/
protected $total = 0;
/**
* @var array
*/
@@ -19,10 +14,9 @@ class SearchResult
* @param array $data
* @param int $total
*/
public function __construct(array $data = [], $total = 0)
public function __construct(array $data = [], protected $total = 0)
{
$this->data = $data;
$this->total = $total;
}
/**
@@ -6,11 +6,13 @@ use EP\EpProducts\Domain\Model\AbstractDestination;
class CityRepository extends AbstractRepository
{
public function getTeasersByCountryOrRegionAndSeason(int $regionUid = 0, int $countryUid = 0, string $season = 'w', int $storagePid = 0)
{
if ($season === null) {
$season = AbstractDestination::SEASON_WINTER;
}
public function getTeasersByCountryOrRegionAndSeason(
int $regionUid = 0,
int $countryUid = 0,
string $season = 'w',
int $storagePid = 0,
) {
$season ??= AbstractDestination::SEASON_WINTER;
$query = $this->createQuery();
$constraints = [
$query->equals('season', $season),
@@ -8,9 +8,7 @@ class RegionRepository extends AbstractRepository
{
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();
$constraints = [
$query->equals('country', $countryUid),
@@ -10,6 +10,7 @@ class TeammemberRepository extends AbstractRepository
/**
* @var array
*/
#[\Override]
protected $defaultOrderings = [
'sorting' => 'ASC',
];
@@ -25,15 +25,15 @@ class ApiClient implements LoggerAwareInterface
/**
* Applied when extension configuration does not override them. Seconds.
*/
private const DEFAULT_TIMEOUT = 5;
private const DEFAULT_MAX_DURATION = 15;
private const int DEFAULT_TIMEOUT = 5;
private const int DEFAULT_MAX_DURATION = 15;
/**
* 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
* 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_CREATED,
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
* 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
@@ -126,7 +126,7 @@ class ApiClient implements LoggerAwareInterface
{
try {
$body = trim($response->getContent(false));
} catch (TransportExceptionInterface $e) {
} catch (TransportExceptionInterface) {
return '<no response body>';
}
@@ -151,7 +151,7 @@ class ApiClient implements LoggerAwareInterface
{
try {
$data = $response->toArray(false);
} catch (\Throwable $e) {
} catch (\Throwable) {
// 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.
return;
@@ -185,7 +185,7 @@ class ApiClient implements LoggerAwareInterface
if ('' === $apiKey) {
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
// 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));
@@ -220,10 +220,8 @@ class ApiClient implements LoggerAwareInterface
private function getConfiguration(): array
{
if (null === $this->configuration) {
$this->configuration = GeneralUtility::makeInstance(ExtensionConfiguration::class)
->get('ep_products');
}
$this->configuration ??= GeneralUtility::makeInstance(ExtensionConfiguration::class)
->get('ep_products');
return $this->configuration;
}
@@ -14,14 +14,8 @@ use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
*/
class ApiKeyAuthenticator
{
/**
* @var ExtensionConfiguration
*/
private $extensionConfiguration;
public function __construct(ExtensionConfiguration $extensionConfiguration)
public function __construct(private readonly ExtensionConfiguration $extensionConfiguration)
{
$this->extensionConfiguration = $extensionConfiguration;
}
public function isAuthorized(ServerRequestInterface $request): bool
@@ -19,57 +19,15 @@ use TYPO3\CMS\Extbase\Service\ImageService;
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(
MyEpImageService $hotelImageService,
MyEpImageService $productImageService,
MyEpImageService $regionImageService,
FileRepository $fileRepository,
ImageService $imageService,
ContentFeedApi $contentFeedApi,
ApiKeyAuthenticator $authenticator
private readonly MyEpImageService $hotelImageService,
private readonly MyEpImageService $productImageService,
private readonly MyEpImageService $regionImageService,
private readonly FileRepository $fileRepository,
private readonly ImageService $imageService,
private readonly ContentFeedApi $contentFeedApi,
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
@@ -84,7 +42,7 @@ class ApiMiddleware implements MiddlewareInterface
return $this->handleProductRequest($request);
}
if (0 === strpos($path, ContentFeedApi::PATH_PREFIX)) {
if (str_starts_with($path, ContentFeedApi::PATH_PREFIX)) {
return $this->contentFeedApi->handle($request);
}
@@ -309,12 +267,10 @@ class ApiMiddleware implements MiddlewareInterface
private function serializeFileReferences(array $files): array
{
return array_map(function ($file) {
return [
'url' => $this->imageService->getImageUri($file, true),
'alt' => $file->getProperty('alternative'),
'title' => $file->getProperty('title'),
];
}, $files);
return array_map(fn($file) => [
'url' => $this->imageService->getImageUri($file, true),
'alt' => $file->getProperty('alternative'),
'title' => $file->getProperty('title'),
], $files);
}
}
@@ -24,20 +24,10 @@ use TYPO3\CMS\Frontend\Controller\ErrorController;
*/
class ContentPageTypeGuardMiddleware implements MiddlewareInterface
{
/**
* @var ApiKeyAuthenticator
*/
private $authenticator;
/**
* @var ContentFeedConfiguration
*/
private $configuration;
public function __construct(ApiKeyAuthenticator $authenticator, ContentFeedConfiguration $configuration)
{
$this->authenticator = $authenticator;
$this->configuration = $configuration;
public function __construct(
private readonly ApiKeyAuthenticator $authenticator,
private readonly ContentFeedConfiguration $configuration,
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
@@ -27,7 +27,7 @@ class StaticSearchParamsMiddleware implements MiddlewareInterface
$params = $request->getAttribute('normalizedParams');
$uri = $params->getRequestUri();
if (strpos($uri, self::PREFIX_PATH_SEGMENT) === false) {
if (!str_contains($uri, self::PREFIX_PATH_SEGMENT)) {
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.
*/
private const DEFAULT_TIMEOUT = 5;
private const DEFAULT_MAX_DURATION = 15;
private const int DEFAULT_TIMEOUT = 5;
private const int DEFAULT_MAX_DURATION = 15;
/**
* 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
* 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
* 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.
*/
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.
*/
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;
@@ -192,8 +192,11 @@ class ApiClient implements LoggerAwareInterface
*
* @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', [
'query' => [
'hotelCode' => $hotelCode,
@@ -219,8 +222,12 @@ class ApiClient implements LoggerAwareInterface
/**
* @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);
$this->assertStatusCode($response, $expectedStatusCodes, $method, $uri);
@@ -271,8 +278,12 @@ class ApiClient implements LoggerAwareInterface
*
* @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);
if (true === in_array($statusCode, $expectedStatusCodes, true)) {
@@ -312,7 +323,7 @@ class ApiClient implements LoggerAwareInterface
{
try {
$response->getContent(false);
} catch (TransportExceptionInterface $e) {
} catch (TransportExceptionInterface) {
// The status has already been asserted, so a failure here costs nothing but the
// connection reuse this method exists for.
}
@@ -375,19 +386,15 @@ class ApiClient implements LoggerAwareInterface
*/
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;
}
private function getConfiguration(): array
{
if (null === $this->configuration) {
$this->configuration = GeneralUtility::makeInstance(ExtensionConfiguration::class)
->get('ep_products');
}
$this->configuration ??= GeneralUtility::makeInstance(ExtensionConfiguration::class)
->get('ep_products');
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.
*/
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
* 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 BASELINE_FILE = 'api-imported.info';
private const string STAGING_SUBDIR = 'ep_products_import';
private const string BASELINE_FILE = 'api-imported.info';
/**
* @var ApiClient|null
*/
private $apiClient;
public function __construct(?ApiClient $apiClient = null)
public function __construct(private ?ApiClient $apiClient = null)
{
$this->apiClient = $apiClient;
}
/**
@@ -202,16 +196,14 @@ final class ApiImportSource implements ProductImportSource, SingletonInterface,
try {
return new \DateTimeImmutable($value);
} catch (\Exception $e) {
} catch (\Exception) {
return null;
}
}
private function getApiClient(): ApiClient
{
if ($this->apiClient === null) {
$this->apiClient = GeneralUtility::makeInstance(ApiClient::class);
}
$this->apiClient ??= GeneralUtility::makeInstance(ApiClient::class);
return $this->apiClient;
}
@@ -157,9 +157,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$products = [];
foreach ($rows as $row) {
$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;
}
@@ -401,9 +399,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$servicesTransportationOptional = [];
if (array_key_exists('BUS', $servicesTransportation)) {
$servicesTransportationOptional = array_filter($servicesTransportation['BUS'], function ($service) {
return $service['price'] !== 0;
});
$servicesTransportationOptional = array_filter($servicesTransportation['BUS'], fn($service) => $service['price'] !== 0);
}
if (array_key_exists('BAH', $servicesTransportation)) {
$servicesTransportationOptional = array_merge(
@@ -433,9 +429,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
foreach ($combinedServices as $key => $list)
{
uasort($list, function ($a, $b) {
return strcmp($a['label'], $b['label']);
});
uasort($list, fn($a, $b) => strcmp($a['label'], $b['label']));
$combinedServices[$key] = $list;
}
$date['services_general'] = serialize($combinedServices);
@@ -488,7 +482,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$code = $row['hotel']['code'];
$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]);
return false;
@@ -503,9 +497,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$this->addServices($generalServices, $services);
$this->addServices($hotelServices, $services);
foreach ($services as $key => $list) {
uasort($list, function ($a, $b) {
return strcmp($a['label'], $b['label']);
});
uasort($list, fn($a, $b) => strcmp($a['label'], $b['label']));
$services[$key] = $list;
}
@@ -696,7 +688,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$label = static::LABEL_TRAIN;
}
if (!array_key_exists($type, $data)) {
if (!array_key_exists((string) $type, $data)) {
$data[$type] = [];
}
@@ -841,7 +833,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
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')];
}
@@ -97,9 +97,7 @@ class DateService implements SingletonInterface
$myEpUrl = $resolvedOptions['myEpUrl'];
$paCode = $resolvedOptions['paCode'] ?? null;
if ($filterSettings === null) {
$filterSettings = $this->filterService->getNormalizedFilterSettings();
}
$filterSettings ??= $this->filterService->getNormalizedFilterSettings();
$nonBookableDateUids = GeneralUtility::trimExplode(',', $product->getNonBookableDates(), true);
@@ -137,9 +135,7 @@ class DateService implements SingletonInterface
$hotel = $resolvedOptions['hotel'];
$filterSettings = $resolvedOptions['filterSettings'] ?? null;
if ($filterSettings === null) {
$filterSettings = $this->filterService->getNormalizedFilterSettings();
}
$filterSettings ??= $this->filterService->getNormalizedFilterSettings();
return $this->productRepository->getAvailableDates($product, $hotel, $filterSettings);
}
@@ -231,16 +227,14 @@ class DateService implements SingletonInterface
$processed = [];
foreach ($servicesFlat as $key => $services) {
$servicesWithCosts = array_filter($services, function ($service) {
return '0,00' !== $service['price'];
});
$servicesWithCosts = array_filter($services, fn($service) => '0,00' !== $service['price']);
if (0 === count($servicesWithCosts)) {
continue;
}
if (array_key_exists($key, $this->serviceSectionsMapping)) {
$key = $this->serviceSectionsMapping[$key];
}
if (!array_key_exists($key, $processed)) {
if (!array_key_exists((string) $key, $processed)) {
$processed[$key] = [
'label' => $this->serviceSections[$key] ?? null,
'services' => [],
@@ -345,7 +339,7 @@ class DateService implements SingletonInterface
}
$hotelUid = $row['hotelUid'];
if (!array_key_exists($hotelUid, $priceTable[$categoryUid])) {
if (!array_key_exists((string) $hotelUid, $priceTable[$categoryUid])) {
$priceTable[$categoryUid][$hotelUid] = [
'category' => $categoryUid,
@@ -375,7 +369,7 @@ class DateService implements SingletonInterface
// Add new entry or replace possible existing entry that is not
// bookable with current record in case of matching category
// and pax
if (!array_key_exists($roomPax, $entry)) {
if (!array_key_exists((string) $roomPax, $entry)) {
$entry[$roomPax] = [
'price' => $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
// one compare as '' (what PHP 7.4 read from the missing offset)
uasort($rows, function ($a, $b) {
return strcmp(
$a['rooms'][Hotel::CATEGORY_SUPER_DELUXE]['price'] ?? '',
$b['rooms'][Hotel::CATEGORY_SUPER_DELUXE]['price'] ?? ''
);
});
uasort($rows, fn($a, $b) => strcmp(
$a['rooms'][Hotel::CATEGORY_SUPER_DELUXE]['price'] ?? '',
$b['rooms'][Hotel::CATEGORY_SUPER_DELUXE]['price'] ?? ''
));
$priceTable[$category] = $rows;
}
@@ -412,13 +404,7 @@ class DateService implements SingletonInterface
public function hasIncludedServices(array $tableData): bool
{
foreach ($tableData as $row) {
if (($row['busIncluded'] ?? null) || ($row['skiPassIncluded'] ?? null)) {
return true;
}
}
return false;
return array_any($tableData, fn($row) => ($row['busIncluded'] ?? null) || ($row['skiPassIncluded'] ?? null));
}
/**
@@ -39,21 +39,17 @@ class FilterService implements SingletonInterface
'bus' => null,
]);
foreach (['hotelUid', 'productUid', 'nights', 'pax'] as $key) {
$optionsResolver->setNormalizer($key, function (Options $options, $value) {
return null !== $value ? (int)$value : null;
});
$optionsResolver->setNormalizer($key, fn(Options $options, $value) => null !== $value ? (int)$value : null);
}
foreach (['productsPid', 'priceRanges', 'countryUids', 'regionUids', 'cityUids', 'conceptUids', 'boardTypes', 'hotelTypes', 'roomTypes'] as $key) {
$optionsResolver->setNormalizer($key, function (Options $options, $value) {
if (empty($value)) {
return [];
}
return array_map('intval', (array) $value);
return array_map(intval(...), (array) $value);
});
}
$optionsResolver->setNormalizer('bus', function (Options $options, $value) {
return null !== $value ? (bool)$value : null;
});
$optionsResolver->setNormalizer('bus', fn(Options $options, $value) => null !== $value ? (bool)$value : null);
$filterSettings = $optionsResolver->resolve($filterSettings);
@@ -250,9 +246,7 @@ class FilterService implements SingletonInterface
}
foreach ($destinations as $key => $value) {
usort($value['regions'], function ($a, $b) {
return strcmp($a['label'], $b['label']);
});
usort($value['regions'], fn($a, $b) => strcmp($a['label'], $b['label']));
$destinations[$key] = $value;
}
sort($destinations);
@@ -260,9 +254,7 @@ class FilterService implements SingletonInterface
sort($roomTypes);
ksort($priceRanges);
usort($boardTypes, function ($a, $b) {
return strcmp($a['label'], $b['label']);
});
usort($boardTypes, fn($a, $b) => strcmp($a['label'], $b['label']));
$filterOptions = new FilterOptions();
$filterOptions->setPriceRanges($priceRanges);
@@ -22,11 +22,11 @@ class GroupsSwissService implements LoggerAwareInterface
{
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 STATE_ON_REQUEST = 1;
private const STATE_BLOCKED = 2;
private const int STATE_FREE = 0;
private const int STATE_ON_REQUEST = 1;
private const int STATE_BLOCKED = 2;
private HttpClientInterface $httpClient;
@@ -116,7 +116,7 @@ class HotelDataService implements SingletonInterface
$hotelCategory = Hotel::$categoryLabels[$row['hotelCategory']] ?? null;
if (!array_key_exists($hotelUid, $teasers)) {
if (!array_key_exists((string) $hotelUid, $teasers)) {
$teasers[$hotelUid] = [
'minDateStart' => null,
'maxDateEnd' => null,
@@ -6,6 +6,7 @@ class HotelImageService extends AbstractImageService
/**
* @var string
*/
#[\Override]
protected $table = 'tx_epproducts_domain_model_hotel';
/**
@@ -19,14 +19,8 @@ use EP\EpProducts\Domain\Repository\DateRepository;
*/
class PickupOverviewService
{
/**
* @var DateRepository
*/
private $dateRepository;
public function __construct(DateRepository $dateRepository)
public function __construct(private readonly DateRepository $dateRepository)
{
$this->dateRepository = $dateRepository;
}
/**
@@ -6,6 +6,7 @@ class ProductImageService extends AbstractImageService
/**
* @var string
*/
#[\Override]
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
* 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 \DateTimeImmutable|null $sourceGeneratedAt
* @param string|null $location local directory of an XML export, null otherwise
*/
public function __construct(iterable $rows, ?\DateTimeImmutable $sourceGeneratedAt, ?string $location = null)
{
$this->rows = $rows;
$this->sourceGeneratedAt = $sourceGeneratedAt;
$this->location = $location;
public function __construct(
private iterable $rows,
private ?\DateTimeImmutable $sourceGeneratedAt,
private ?string $location = null,
) {
}
/**
@@ -93,12 +93,6 @@ class RoomTypeResolver
*/
protected function matchesAny(string $label, array $patterns): bool
{
foreach ($patterns as $pattern) {
if (mb_strpos(mb_strtolower($label), mb_strtolower($pattern)) !== false) {
return true;
}
}
return false;
return array_any($patterns, fn($pattern) => str_contains(mb_strtolower($label), mb_strtolower($pattern)));
}
}
@@ -249,9 +249,7 @@ class SearchResultService implements SingletonInterface
continue;
}
uasort($section['items'], function ($a, $b) {
return strcmp((string)$a['minPrice'], (string)$b['minPrice']);
});
uasort($section['items'], fn($a, $b) => strcmp((string)$a['minPrice'], (string)$b['minPrice']));
}
// Turn result data into simple array to ensure order in js
@@ -23,25 +23,25 @@ class SnowreportImportService implements LoggerAwareInterface
{
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
private const HTTP_CONNECT_TIMEOUT = 5;
private const HTTP_TIMEOUT = 30;
private const int HTTP_CONNECT_TIMEOUT = 5;
private const int HTTP_TIMEOUT = 30;
private const SNOWREPORT_TABLE = 'tx_epproducts_domain_model_snowreport';
private const WEBCAM_TABLE = 'tx_epproducts_domain_model_webcam';
private const string SNOWREPORT_TABLE = 'tx_epproducts_domain_model_snowreport';
private const string WEBCAM_TABLE = 'tx_epproducts_domain_model_webcam';
private const TYPE_INT = 'int';
private const TYPE_STRING = 'string';
private const TYPE_BOOL = 'bool';
private const TYPE_DATE = 'date';
private const string TYPE_INT = 'int';
private const string TYPE_STRING = 'string';
private const string TYPE_BOOL = 'bool';
private const string TYPE_DATE = 'date';
/**
* 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.
*/
private const MAP = [
private const array MAP = [
'region_id' => ['column' => 'vendor_uid', 'type' => self::TYPE_INT],
'region_name' => ['column' => 'region_name', 'type' => self::TYPE_STRING],
'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],
];
private const BOOLEANS = [
private const array BOOLEANS = [
'true' => true, '1' => true, 'ja' => true, 'yes' => true,
'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
{
$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) {
return;
}
@@ -314,7 +314,7 @@ class SnowreportImportService implements LoggerAwareInterface
$this->conversionWarnings[sprintf('<%s> expected a date, got "%s".', $key, $raw)] = true;
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);
@@ -20,9 +20,7 @@ class TypeConversionUtility
return (int) $value;
}
if (\is_array($value)) {
return array_map(function($item) {
return static::convertType($item);
}, $value);
return array_map(fn($item) => static::convertType($item), $value);
}
if (empty($value)) {
return null;
@@ -1,12 +1,13 @@
<?php
use EP\EpProducts\Middleware\StaticSearchParamsMiddleware;
use EP\EpProducts\Middleware\ApiMiddleware;
use EP\EpProducts\Middleware\ContentPageTypeGuardMiddleware;
return [
'frontend' => [
'ep/staticsearch' => [
'target' => \EP\EpProducts\Middleware\StaticSearchParamsMiddleware::class,
'target' => StaticSearchParamsMiddleware::class,
'before' => [
'typo3/cms-frontend/page-resolver',
],
+2 -1
View File
@@ -1,4 +1,5 @@
<?php
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use EP\EpProducts\Controller\ProductController;
use EP\EpProducts\Controller\CityController;
@@ -42,7 +43,7 @@ $boot = function () {
// 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']['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',
'expirePeriod' => 180,
];
@@ -9,11 +9,13 @@ class Page extends AbstractEntity
/**
* @var int
*/
#[\Override]
protected ?int $uid = null;
/**
* @var int
*/
#[\Override]
protected ?int $pid = null;
/**
@@ -10,6 +10,7 @@ class PageRepository extends Repository
/**
* @var array
*/
#[\Override]
protected $defaultOrderings = [
'sorting' => 'ASC',
];
@@ -46,7 +46,7 @@ abstract class AbstractMappedFinisher extends AbstractFinisher
$formValues = $this->finisherContext->getFormValues();
foreach ($this->parseMapping() as $remote => $local) {
if (false === array_key_exists($local, $formValues)) {
if (false === array_key_exists((string) $local, $formValues)) {
continue;
}
@@ -130,13 +130,13 @@ abstract class AbstractMappedFinisher extends AbstractFinisher
if ($value instanceof FileReference) {
$resource = $value->getOriginalResource();
return null !== $resource ? $resource->getName() : null;
return $resource?->getName();
}
if (true === is_object($value)) {
$this->logWarning('Form value skipped while collecting labeled values: unsupported type.', [
'element' => $identifier,
'class' => get_class($value),
'class' => $value::class,
]);
return null;
@@ -31,7 +31,7 @@ class ApplicationApiFinisher extends AbstractMappedFinisher
// without keeping a list of identifiers. Taken from the definition rather than
// through TranslationService: a translated label would differ per site language.
$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['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.
*/
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
*/
#[\Override]
protected $escapeOutput = false;
/**
@@ -45,9 +46,7 @@ class ContainerViewHelper extends AbstractViewHelper
public function render()
{
$content = $this->renderChildren();
if ($content === null) {
$content = $this->arguments['content'];
}
$content ??= $this->arguments['content'];
$classes = [];
if (\in_array($this->arguments['data']['colPos'] ?? null, static::$columnsWithoutContainers, false)) {
$classes[] = 'container';
@@ -11,6 +11,7 @@ class FilterSettingsEncoderViewHelper extends AbstractViewHelper
/**
* @var bool
*/
#[\Override]
protected $escapeOutput = false;
public function initializeArguments()
@@ -29,9 +30,7 @@ class FilterSettingsEncoderViewHelper extends AbstractViewHelper
public function render()
{
$argument = $this->renderChildren();
if ($argument === null) {
$argument = $this->arguments['argument'];
}
$argument ??= $this->arguments['argument'];
if ($argument === null) {
return '';
}
@@ -12,6 +12,7 @@ class GmapsUrlViewHelper extends AbstractViewHelper
/**
* @var bool
*/
#[\Override]
protected $escapeOutput = false;
const URLBASE = 'http://maps.google.com/maps?q=%s,%s&t=%s&z=%d';
@@ -11,6 +11,7 @@ class IconViewHelper extends AbstractTagBasedViewHelper
/**
* @var string
*/
#[\Override]
protected $tagName = 'svg';
public function initializeArguments()
@@ -11,6 +11,7 @@ class RatingViewHelper extends AbstractViewHelper
/**
* @var bool
*/
#[\Override]
protected $escapeOutput = false;
public function initializeArguments()
@@ -29,9 +30,7 @@ class RatingViewHelper extends AbstractViewHelper
public function render()
{
$product = $this->renderChildren();
if ($product === null) {
$product = $this->arguments['product'];
}
$product ??= $this->arguments['product'];
$ratingAverage = $product->getRatingAverage();
$ratingVotesCount = $product->getRatingVotesCount();
$fullStars = floor($ratingAverage);
@@ -24,7 +24,7 @@ class ThemeClassesViewHelper extends AbstractViewHelper
public function render()
{
$themeKey = $this->arguments['key'];
if (!array_key_exists($themeKey, $this->arguments['classes'])) {
if (!array_key_exists((string) $themeKey, $this->arguments['classes'])) {
return '';
}
return $this->arguments['classes'][$themeKey];
@@ -10,6 +10,7 @@ class VideoViewHelper extends AbstractViewHelper
/**
* @var bool
*/
#[\Override]
protected $escapeOutput = false;
public function initializeArguments()
@@ -1,15 +1,19 @@
<?php
use EP\EpTheme\Middleware\CdnImageMiddleware;
use EP\EpTheme\Middleware\ReferrerMiddleware;
use EP\EpTheme\Middleware\HtmxRedirectMiddleware;
return [
'frontend' => [
'ep/cdn-images' => [
'target' => \EP\EpTheme\Middleware\CdnImageMiddleware::class,
'target' => CdnImageMiddleware::class,
'after' => [
'typo3/cms-frontend/page-resolver',
],
],
'ep/referrer' => [
'target' => \EP\EpTheme\Middleware\ReferrerMiddleware::class,
'target' => ReferrerMiddleware::class,
'after' => [
'typo3/cms-frontend/authentication',
],
@@ -20,7 +24,7 @@ return [
// Outermost of the frontend stack after the site is known, so it also sees redirects
// that core middlewares or exceptions (PropagateResponseException) produce
'ep/htmx-redirect' => [
'target' => \EP\EpTheme\Middleware\HtmxRedirectMiddleware::class,
'target' => HtmxRedirectMiddleware::class,
'after' => [
'typo3/cms-frontend/site',
],
@@ -55,7 +55,7 @@ call_user_func(function () {
$row[] = $config;
}
GeneralUtility::makeInstance(Registry::class)->configureContainer(
(new ContainerConfiguration($cType, $label, $description, [$row]))
new ContainerConfiguration($cType, $label, $description, [$row])
->setIcon('tx_eptheme_gridelements_' . $icon)
);
}
@@ -113,7 +113,8 @@ return [
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim,required'
'eval' => 'trim',
'required' => true
],
],
'description' => [
+1
View File
@@ -27,6 +27,7 @@ return RectorConfig::configure()
MigratePluginContentElementAndPluginSubtypesTCARector::class,
])
->withPhpVersion(PhpVersion::PHP_85)
->withPhpSets(php85: true)
->withSets([
Typo3SetList::GENERAL,
Typo3LevelSetList::UP_TO_TYPO3_13,