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:
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -27,6 +27,7 @@ return RectorConfig::configure()
|
||||
MigratePluginContentElementAndPluginSubtypesTCARector::class,
|
||||
])
|
||||
->withPhpVersion(PhpVersion::PHP_85)
|
||||
->withPhpSets(php85: true)
|
||||
->withSets([
|
||||
Typo3SetList::GENERAL,
|
||||
Typo3LevelSetList::UP_TO_TYPO3_13,
|
||||
|
||||
Reference in new issue
Block a user