feat: improved caching of api-backed content
This commit is contained in:
@@ -13,9 +13,17 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
class ApiClient
|
||||
{
|
||||
/**
|
||||
* Applied when extension configuration does not override them. Seconds.
|
||||
*/
|
||||
private const DEFAULT_TIMEOUT = 5;
|
||||
private const DEFAULT_MAX_DURATION = 15;
|
||||
|
||||
public const CALENDAR_MODE_DAYS = 'days';
|
||||
public const CALENDAR_MODE_RANGES = 'ranges';
|
||||
|
||||
private ?array $configuration = null;
|
||||
|
||||
public function getCalendar(
|
||||
$hotelCode,
|
||||
\DateTimeInterface $dateFrom,
|
||||
@@ -84,7 +92,7 @@ class ApiClient
|
||||
|
||||
private function getHttpClient(): HttpClientInterface
|
||||
{
|
||||
$config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('ep_products');
|
||||
$config = $this->getConfiguration();
|
||||
$baseUrl = trim((string)($config['bpnConnectApiBaseUrl'] ?? ''));
|
||||
$apiKey = trim((string)($config['bpnConnectApiKey'] ?? ''));
|
||||
|
||||
@@ -95,10 +103,38 @@ class ApiClient
|
||||
throw new ApiException('Bpn Connect API key is not configured.');
|
||||
}
|
||||
|
||||
return HttpClient::createForBaseUri(rtrim($baseUrl, '/') . '/', [
|
||||
$options = [
|
||||
'headers' => [
|
||||
'X-API-KEY' => $apiKey,
|
||||
],
|
||||
]);
|
||||
];
|
||||
|
||||
// Without these the client falls back to PHP's default_socket_timeout (commonly 60s),
|
||||
// so a stalled API pins a PHP worker for a minute per request. The defaults live in
|
||||
// code rather than in ext_conf_template.txt: ExtensionConfiguration::get() returns the
|
||||
// stored configuration verbatim and only syncs the template when an extension has no
|
||||
// configuration at all, so a newly added template key is absent until someone saves
|
||||
// extension configuration by hand. Falling back to 0 there would silently mean "no
|
||||
// timeout" - the opposite of what this guard is for. An explicit 0 still disables.
|
||||
$timeout = (int)($config['bpnConnectApiTimeout'] ?? self::DEFAULT_TIMEOUT);
|
||||
if ($timeout > 0) {
|
||||
$options['timeout'] = $timeout;
|
||||
}
|
||||
$maxDuration = (int)($config['bpnConnectApiMaxDuration'] ?? self::DEFAULT_MAX_DURATION);
|
||||
if ($maxDuration > 0) {
|
||||
$options['max_duration'] = $maxDuration;
|
||||
}
|
||||
|
||||
return HttpClient::createForBaseUri(rtrim($baseUrl, '/') . '/', $options);
|
||||
}
|
||||
|
||||
private function getConfiguration(): array
|
||||
{
|
||||
if (null === $this->configuration) {
|
||||
$this->configuration = GeneralUtility::makeInstance(ExtensionConfiguration::class)
|
||||
->get('ep_products');
|
||||
}
|
||||
|
||||
return $this->configuration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ use EP\EpProducts\MyEP\ApiException;
|
||||
use EP\EpProducts\Utility\DateUtility;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
|
||||
|
||||
class AjaxCalendarController extends ActionController
|
||||
{
|
||||
@@ -53,6 +54,8 @@ class AjaxCalendarController extends ActionController
|
||||
public function priceCalendarAction(Hotel $hotel, int $months = 12): void
|
||||
{
|
||||
$hotelCode = trim($hotel->getCode() ?? '');
|
||||
$this->tagPageCacheForHotel($hotelCode);
|
||||
|
||||
$dateRange = DateUtility::getDateRange(null, null, $months);
|
||||
$today = new \DateTimeImmutable('today');
|
||||
|
||||
@@ -62,16 +65,10 @@ class AjaxCalendarController extends ActionController
|
||||
|
||||
if ($hotelCode !== '') {
|
||||
try {
|
||||
$cacheIdentifier = 'myep_price_calendar_' . sha1(implode('|', [
|
||||
$hotelCode,
|
||||
$dateRange['period']->getStartDate()->format('Y-m-d'),
|
||||
$dateRange['period']->getEndDate()->format('Y-m-d'),
|
||||
]));
|
||||
$cachedData = $this->cache->get($cacheIdentifier);
|
||||
|
||||
if (false !== $cachedData) {
|
||||
$priceDataByDate = $cachedData;
|
||||
} else {
|
||||
// This action is cacheable, so the rendered markup is held in the page cache
|
||||
// for config.cache_period. An additional application-level cache here would
|
||||
// key on exactly the same granularity (hotel + date range) and only stack a
|
||||
// second TTL on top, pushing worst-case staleness past the freshness budget.
|
||||
$rawData = $this->myEpClient->getPriceConfigData(
|
||||
$hotelCode,
|
||||
$dateRange['period']->getStartDate(),
|
||||
@@ -116,16 +113,15 @@ class AjaxCalendarController extends ActionController
|
||||
'minNights' => (int)($item['minNights'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$this->cache->set($cacheIdentifier, $priceDataByDate, [], self::CACHE_TTL);
|
||||
}
|
||||
} catch (ApiException $e) {
|
||||
$error = true;
|
||||
$errorMessage = $e->getMessage();
|
||||
$this->disablePageCache();
|
||||
}
|
||||
} else {
|
||||
$error = true;
|
||||
$errorMessage = 'Missing hotel code';
|
||||
$this->disablePageCache();
|
||||
}
|
||||
|
||||
$this->view->assignMultiple([
|
||||
@@ -143,6 +139,7 @@ class AjaxCalendarController extends ActionController
|
||||
$apiDateFrom = $dateRange['period']->getStartDate();
|
||||
$apiDateTo = $dateRange['period']->getEndDate();
|
||||
$hotelCode = trim($hotel->getCode() ?? '');
|
||||
$this->tagPageCacheForHotel($hotelCode);
|
||||
|
||||
$priceConfig = [
|
||||
'data' => [],
|
||||
@@ -155,6 +152,7 @@ class AjaxCalendarController extends ActionController
|
||||
} catch (ApiException $e) {
|
||||
$priceConfig['success'] = false;
|
||||
$priceConfig['error'] = $e->getMessage();
|
||||
$this->disablePageCache();
|
||||
}
|
||||
|
||||
$this->view->assign('priceConfig', $priceConfig);
|
||||
@@ -164,6 +162,7 @@ class AjaxCalendarController extends ActionController
|
||||
{
|
||||
$year = $year ?? (new \DateTimeImmutable())->format('Y');
|
||||
$hotelCode = trim($hotel->getCode() ?? '');
|
||||
$this->tagPageCacheForHotel($hotelCode);
|
||||
|
||||
$priceTable = [
|
||||
'data' => [],
|
||||
@@ -176,6 +175,7 @@ class AjaxCalendarController extends ActionController
|
||||
} catch (ApiException $e) {
|
||||
$priceTable['success'] = false;
|
||||
$priceTable['error'] = $e->getMessage();
|
||||
$this->disablePageCache();
|
||||
}
|
||||
|
||||
$this->view->assignMultiple([
|
||||
@@ -212,7 +212,7 @@ class AjaxCalendarController extends ActionController
|
||||
$hotelCode,
|
||||
$apiDateFrom,
|
||||
$apiDateTo,
|
||||
ApiClient::CALENDAR_MODE_DAYS
|
||||
BpnConnectClient::CALENDAR_MODE_DAYS
|
||||
);
|
||||
$cachedCalendar = $this->cache->get($cacheIdentifier);
|
||||
|
||||
@@ -223,7 +223,7 @@ class AjaxCalendarController extends ActionController
|
||||
$hotelCode,
|
||||
new \DateTimeImmutable($apiDateFrom),
|
||||
new \DateTimeImmutable($apiDateTo),
|
||||
ApiClient::CALENDAR_MODE_DAYS
|
||||
BpnConnectClient::CALENDAR_MODE_DAYS
|
||||
);
|
||||
$this->cache->set($cacheIdentifier, $calendar, [], self::CACHE_TTL);
|
||||
}
|
||||
@@ -240,6 +240,43 @@ class AjaxCalendarController extends ActionController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag this page-cache entry with the hotel it renders, so the entry can be flushed for
|
||||
* that hotel alone rather than by clearing the whole page cache and re-warming every
|
||||
* calendar. Tags are stored alongside the entry by
|
||||
* TypoScriptFrontendController::setPageCacheContent().
|
||||
*
|
||||
* Used manually today, when one hotel's availability needs re-fetching before its hour
|
||||
* is up:
|
||||
*
|
||||
* typo3cms cache:flushtags ep_hotel_<code> --groups pages
|
||||
*/
|
||||
private function tagPageCacheForHotel(string $hotelCode): void
|
||||
{
|
||||
if ('' === $hotelCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
|
||||
$GLOBALS['TSFE']->addCacheTags(['ep_hotel_' . $hotelCode]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The calendar actions are cacheable, so an API failure would otherwise pin the error
|
||||
* markup in the page cache for the full cache_period. Drop caching for this request so
|
||||
* the next visitor gets a fresh attempt instead of an hour-old error message.
|
||||
*
|
||||
* The second argument must be true: set_no_cache() ignores the call otherwise, because
|
||||
* $TYPO3_CONF_VARS['FE']['disableNoCacheParameter'] defaults to true.
|
||||
*/
|
||||
private function disablePageCache(): void
|
||||
{
|
||||
if (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
|
||||
$GLOBALS['TSFE']->set_no_cache('Price calendar data could not be retrieved', true);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildTooltip(string $status, array $item): string
|
||||
{
|
||||
if ($status === 'blocked') {
|
||||
|
||||
@@ -16,6 +16,8 @@ use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
@@ -23,8 +25,36 @@ class ApiClient implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* Applied when extension configuration does not override them. Seconds.
|
||||
*/
|
||||
private const DEFAULT_TIMEOUT = 5;
|
||||
private const 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';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Fallback lifetime for tokens that do not report an expiry.
|
||||
*/
|
||||
private const ACCESS_TOKEN_DEFAULT_TTL = 300;
|
||||
|
||||
private static ?AccessToken $accessToken = null;
|
||||
|
||||
private ?FrontendInterface $cache = null;
|
||||
|
||||
private ?array $configuration = null;
|
||||
|
||||
/**
|
||||
* @throws ApiException
|
||||
*/
|
||||
@@ -209,14 +239,58 @@ class ApiClient implements LoggerAwareInterface
|
||||
|
||||
private function getHttpClient(): HttpClientInterface
|
||||
{
|
||||
$config = GeneralUtility::makeInstance(ExtensionConfiguration::class)
|
||||
->get('ep_products');
|
||||
$config = $this->getConfiguration();
|
||||
|
||||
static::$accessToken = $this->getAccessToken($config);
|
||||
|
||||
return HttpClient::createForBaseUri($config['myEpApiBaseUrl'], [
|
||||
$options = [
|
||||
'auth_bearer' => static::$accessToken->getToken(),
|
||||
]);
|
||||
];
|
||||
|
||||
// Without these the client falls back to PHP's default_socket_timeout (commonly 60s),
|
||||
// so a stalled API pins a PHP worker for a minute per request. The defaults live in
|
||||
// code rather than in ext_conf_template.txt: ExtensionConfiguration::get() returns the
|
||||
// stored configuration verbatim and only syncs the template when an extension has no
|
||||
// configuration at all, so a newly added template key is absent until someone saves
|
||||
// extension configuration by hand. Falling back to 0 there would silently mean "no
|
||||
// timeout" - the opposite of what this guard is for. An explicit 0 still disables.
|
||||
$timeout = (int)($config['myEpApiTimeout'] ?? self::DEFAULT_TIMEOUT);
|
||||
if ($timeout > 0) {
|
||||
$options['timeout'] = $timeout;
|
||||
}
|
||||
$maxDuration = (int)($config['myEpApiMaxDuration'] ?? self::DEFAULT_MAX_DURATION);
|
||||
if ($maxDuration > 0) {
|
||||
$options['max_duration'] = $maxDuration;
|
||||
}
|
||||
|
||||
return HttpClient::createForBaseUri($config['myEpApiBaseUrl'], $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved lazily rather than injected: this class is also instantiated through Extbase's
|
||||
* legacy object container (EP\EpTheme\Form\BpnApiFinisher asks for it via injectApiClient),
|
||||
* and that container resolves constructor arguments by reflection, mapping an interface to
|
||||
* a class name by stripping the "Interface" suffix (Container::getImplementationClassName()).
|
||||
* A constructor typed against FrontendInterface therefore fatals there with
|
||||
* "Class TYPO3\CMS\Core\Cache\Frontend\Frontend does not exist".
|
||||
*/
|
||||
private function getCache(): FrontendInterface
|
||||
{
|
||||
if (null === $this->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');
|
||||
}
|
||||
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,8 +302,21 @@ class ApiClient implements LoggerAwareInterface
|
||||
return static::$accessToken;
|
||||
}
|
||||
|
||||
// The static above only lives for one PHP request, so without this second level every
|
||||
// single request would re-authenticate before it could fetch any data.
|
||||
$cachedToken = $this->getCache()->get(self::ACCESS_TOKEN_CACHE_IDENTIFIER);
|
||||
if (is_array($cachedToken) && isset($cachedToken['access_token'])) {
|
||||
$token = new AccessToken($cachedToken);
|
||||
if (false === $token->hasExpired()) {
|
||||
static::$accessToken = $token;
|
||||
|
||||
return static::$accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
static::$accessToken = $this->getProvider($config)->getAccessToken('client_credentials');
|
||||
$this->cacheAccessToken(static::$accessToken);
|
||||
} catch (IdentityProviderException $e) {
|
||||
$message = sprintf('MyEP OAuth token request failed: %s', $e->getMessage());
|
||||
$this->logError($message, ['exception' => $e]);
|
||||
@@ -247,9 +334,32 @@ class ApiClient implements LoggerAwareInterface
|
||||
return static::$accessToken;
|
||||
}
|
||||
|
||||
private function cacheAccessToken(AccessToken $token): void
|
||||
{
|
||||
$expires = $token->getExpires();
|
||||
|
||||
if (null === $expires) {
|
||||
$lifetime = self::ACCESS_TOKEN_DEFAULT_TTL;
|
||||
} else {
|
||||
$lifetime = $expires - time() - self::ACCESS_TOKEN_EXPIRY_MARGIN;
|
||||
}
|
||||
|
||||
if ($lifetime <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getCache()->set(self::ACCESS_TOKEN_CACHE_IDENTIFIER, $token->jsonSerialize(), [], $lifetime);
|
||||
}
|
||||
|
||||
private function getProvider(array $config): AbstractProvider
|
||||
{
|
||||
return new GenericProvider([
|
||||
// The OAuth round trip goes through the provider's own Guzzle client, not the
|
||||
// Symfony client configured in getHttpClient(), so it needs its own timeout -
|
||||
// otherwise the token request hangs indefinitely and the data-request timeout
|
||||
// never gets a chance to apply. AbstractProvider::getAllowedClientOptions()
|
||||
// forwards 'timeout' (and 'proxy') into Guzzle; 0 means unlimited there too.
|
||||
'timeout' => (int)($config['myEpApiTimeout'] ?? self::DEFAULT_TIMEOUT),
|
||||
'clientId' => $config['myEpApiClientId'],
|
||||
'clientSecret' => $config['myEpApiClientSecret'],
|
||||
'redirectUri' => null,
|
||||
|
||||
@@ -144,6 +144,25 @@ tx_epproducts_ajax_json {
|
||||
xhtml_cleaning = 0
|
||||
admPanel = 0
|
||||
debug = 0
|
||||
|
||||
# The calendar actions are cacheable, but they render volatile availability data.
|
||||
# TSFE would otherwise fall back to cacheTimeOutDefault (86400), which would widen
|
||||
# staleness from one hour to a full day. Keep this aligned with the freshness budget.
|
||||
cache_period = 3600
|
||||
# priceCalendarAction renders relative to 'today', so an entry must not outlive the
|
||||
# day it was rendered on. Near midnight this clips the lifetime below cache_period.
|
||||
cache_clearAtMidnight = 1
|
||||
|
||||
# The server-side page cache can be flushed; a max-age already sitting in a visitor's
|
||||
# browser cannot. Availability is volatile enough that we keep the speed win on the
|
||||
# server and make clients revalidate every time - they still hit a warm page cache.
|
||||
# sendCacheHeaders is off so TSFE does not also emit a contradicting Expires/Pragma.
|
||||
sendCacheHeaders = 0
|
||||
additionalHeaders.30.header = Cache-Control: private, no-cache, must-revalidate
|
||||
|
||||
# Same reasoning: staticfilecache serves from disk without touching PHP, which is the
|
||||
# layer with the least predictable invalidation. Not worth it for volatile fragments.
|
||||
tx_staticfilecache.disableCache = 1
|
||||
}
|
||||
|
||||
10 = USER
|
||||
@@ -193,7 +212,6 @@ tx_epproducts_ajax_json {
|
||||
1 = concept
|
||||
}
|
||||
}
|
||||
features.requireCHashArgumentForActionArguments = 0
|
||||
settings =< plugin.tx_epproducts.settings
|
||||
persistence =< plugin.tx_epproducts.persistence
|
||||
view =< plugin.tx_epproducts.view
|
||||
|
||||
@@ -22,8 +22,20 @@ myEpApiUrlAuthorize =
|
||||
# cat=MyEpAPI; type=string; label=MyE&P API token URL
|
||||
myEpApiUrlToken =
|
||||
|
||||
# cat=MyEpAPI; type=int; label=MyE&P API inactivity timeout in seconds (0 = PHP default_socket_timeout)
|
||||
myEpApiTimeout = 5
|
||||
|
||||
# cat=MyEpAPI; type=int; label=MyE&P API max total request duration in seconds (0 = unlimited)
|
||||
myEpApiMaxDuration = 15
|
||||
|
||||
# cat=MyEpAPI; type=string; label=Bpn Connect API base URL
|
||||
bpnConnectApiBaseUrl =
|
||||
|
||||
# cat=MyEpAPI; type=string; label=Bpn Connect API key
|
||||
bpnConnectApiKey =
|
||||
|
||||
# cat=MyEpAPI; type=int; label=Bpn Connect API inactivity timeout in seconds (0 = PHP default_socket_timeout)
|
||||
bpnConnectApiTimeout = 5
|
||||
|
||||
# cat=MyEpAPI; type=int; label=Bpn Connect API max total request duration in seconds (0 = unlimited)
|
||||
bpnConnectApiMaxDuration = 15
|
||||
|
||||
@@ -6,6 +6,15 @@ $boot = function () {
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['ep_products_cache'] = [];
|
||||
}
|
||||
|
||||
// The cacheable AjaxCalendar actions are only distinguished in the page-cache key when a
|
||||
// cHash is present - TypoScriptFrontendController::getRelevantParametersForCachingFromPageArguments()
|
||||
// drops all arguments without one, so every hotel would otherwise collide on a single
|
||||
// entry and be served another hotel's availability, silently and with no 404 to signal it.
|
||||
// Every URL is generated by the Uri\Ajax ViewHelper and carries a cHash; requiring it here
|
||||
// turns a hand-built or truncated URL into a loud 404 instead of quietly wrong data.
|
||||
// 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']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['ep_products'] =
|
||||
\EP\EpProducts\Hooks\Tcemain::class;
|
||||
|
||||
@@ -278,7 +287,7 @@ $boot = function () {
|
||||
'AjaxDate' => 'dates',
|
||||
'AjaxContingent' => 'list,rooms',
|
||||
'AjaxTable' => 'datestable,pricetable,pricetableHtml,eventPricetable,daytripRooms',
|
||||
'AjaxCalendar' => 'range,priceConfig,priceCalendar,priceTable',
|
||||
'AjaxCalendar' => 'range',
|
||||
'AjaxWatchlist' => 'list',
|
||||
'AjaxTeaserPopup' => 'concept',
|
||||
]
|
||||
@@ -311,9 +320,13 @@ $boot = function () {
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'ref';
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'pa';
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'ep_staging';
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'tx_epproducts_ajax[months]';
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'tx_epproducts_ajax[month]';
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'tx_epproducts_ajax[year]';
|
||||
// NOTE: tx_epproducts_ajax[months|month|year] were previously excluded from the cHash.
|
||||
// That was harmless while every AjaxCalendar action was non-cacheable, but priceCalendar,
|
||||
// priceConfig and priceTable are cacheable now, and an excluded parameter is dropped from
|
||||
// the page-cache key as well - so the 2026 and 2027 price tables shared one entry and the
|
||||
// second year was served the first year's content. They must take part in the cHash.
|
||||
// The client-side callers that vary month/year (calendar_controller.js) send them in a
|
||||
// POST body, which the cHash never inspects, so nothing depends on the exclusion.
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'f';
|
||||
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects']['PassthroughMapper'] =
|
||||
|
||||
@@ -358,8 +358,13 @@
|
||||
</f:section>
|
||||
|
||||
<f:section name="PriceTable">
|
||||
<f:comment><!--
|
||||
This section renders once per year, so "load" would fire two requests on first
|
||||
paint that compete with the price calendar for PHP workers. The tables sit below
|
||||
the fold, so fetch them when they actually come into view.
|
||||
--></f:comment>
|
||||
<div hx-get="{ep:uri.ajax(action: 'priceTable', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, arguments: '{hotel: product.calendarHotel, year: year}', format: 'html')}"
|
||||
hx-trigger="load">
|
||||
hx-trigger="revealed">
|
||||
<f:render section="TableLoadingIndicator"/>
|
||||
</div>
|
||||
</f:section>
|
||||
|
||||
Reference in New Issue
Block a user