TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Frontend\Cache;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent;
|
||||
use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForRowEvent;
|
||||
|
||||
/**
|
||||
* Calculates the max lifetime the given page should be stored in TYPO3's page cache.
|
||||
*
|
||||
* The "lifetime" is the number of seconds from the current time, it is not a full time/timestamp
|
||||
* Example: If the lifetime is "3600" (=1h), the page will be cached for 1h.
|
||||
*
|
||||
* @internal This class is not part of the TYPO3 Core API
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class CacheLifetimeCalculator
|
||||
{
|
||||
protected const defaultCacheTimeout = 365 * 86400; // 1 year
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
protected readonly FrontendInterface $runtimeCache,
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly ConnectionPool $connectionPool,
|
||||
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the cache lifetime in seconds for the given record.
|
||||
*/
|
||||
public function calculateLifetimeForRow(string $tableName, array $record, int $defaultCacheTimeoutInSeconds = 0): int
|
||||
{
|
||||
$cachedCacheLifetimeIdentifier = sprintf('calculateLifetimeForRow_%s_%d', $tableName, ($record['uid'] ?? 0));
|
||||
$cachedCacheLifetime = $this->runtimeCache->get($cachedCacheLifetimeIdentifier);
|
||||
if ($cachedCacheLifetime !== false) {
|
||||
return (int)$cachedCacheLifetime;
|
||||
}
|
||||
|
||||
$cacheTimeout = $defaultCacheTimeoutInSeconds ?: self::defaultCacheTimeout;
|
||||
|
||||
if ($this->tcaSchemaFactory->has($tableName)) {
|
||||
$schema = $this->tcaSchemaFactory->get($tableName);
|
||||
// If the record has a starttime or endtime, we have to adjust the cache timeout
|
||||
foreach ([TcaSchemaCapability::RestrictionStartTime, TcaSchemaCapability::RestrictionEndTime] as $capability) {
|
||||
if (!$schema->hasCapability($capability)) {
|
||||
continue;
|
||||
}
|
||||
$timeField = $schema->getCapability($capability)->getFieldName();
|
||||
if (array_key_exists($timeField, $record) && $record[$timeField] > 0 && ((int)$record[$timeField] - $GLOBALS['ACCESS_TIME']) > 0) {
|
||||
$cacheTimeout = min($cacheTimeout, (int)$record[$timeField] - $GLOBALS['ACCESS_TIME']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the time, rounded to the minute (do not pollute MySQL cache!)
|
||||
// It is ok that we do not take seconds into account here because this
|
||||
// value will be subtracted later. So we never get the time "before"
|
||||
// the cache change.
|
||||
$currentTimestamp = (int)$GLOBALS['ACCESS_TIME'];
|
||||
$cacheTimeout = min($currentTimestamp, $cacheTimeout);
|
||||
|
||||
$event = new ModifyCacheLifetimeForRowEvent(
|
||||
$cacheTimeout,
|
||||
$tableName,
|
||||
$record
|
||||
);
|
||||
$event = $this->eventDispatcher->dispatch($event);
|
||||
$cacheTimeout = $event->cacheLifetime;
|
||||
$this->runtimeCache->set($cachedCacheLifetimeIdentifier, (string)$cacheTimeout);
|
||||
return $cacheTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache lifetime in seconds for the given page.
|
||||
*/
|
||||
public function calculateLifetimeForPage(int $pageId, array $pageRecord, array $renderingInstructions, Context $context): int
|
||||
{
|
||||
$cachedCacheLifetimeIdentifier = 'cacheLifeTimeForPage_' . $pageId;
|
||||
$cachedCacheLifetime = $this->runtimeCache->get($cachedCacheLifetimeIdentifier);
|
||||
if ($cachedCacheLifetime !== false) {
|
||||
return (int)$cachedCacheLifetime;
|
||||
}
|
||||
if ($pageRecord['cache_timeout'] ?? false) {
|
||||
// Cache period was set for the page:
|
||||
$cacheTimeout = (int)$pageRecord['cache_timeout'];
|
||||
} else {
|
||||
// Cache period was set via TypoScript "config.cache_period", otherwise it's the default of 24 hours
|
||||
$cacheTimeout = (int)($renderingInstructions['cache_period'] ?? self::defaultCacheTimeout);
|
||||
}
|
||||
|
||||
$cacheTimeout = $this->calculateLifetimeForRow('pages', $pageRecord, $cacheTimeout);
|
||||
|
||||
// Calculate the timeout time for records on the page and adjust cache timeout if necessary
|
||||
// Get the configuration
|
||||
$tablesToConsider = $this->getCurrentPageCacheConfiguration($pageId, $renderingInstructions);
|
||||
|
||||
// Get the time, rounded to the minute (do not pollute MySQL cache!)
|
||||
// It is ok that we do not take seconds into account here because this
|
||||
// value will be subtracted later. So we never get the time "before"
|
||||
// the cache change.
|
||||
$currentTimestamp = (int)$GLOBALS['ACCESS_TIME'];
|
||||
$cacheTimeout = min($this->calculatePageCacheLifetime($tablesToConsider, $currentTimestamp), $cacheTimeout);
|
||||
|
||||
$event = new ModifyCacheLifetimeForPageEvent(
|
||||
$cacheTimeout,
|
||||
$pageId,
|
||||
$pageRecord,
|
||||
$renderingInstructions,
|
||||
$context
|
||||
);
|
||||
$event = $this->eventDispatcher->dispatch($event);
|
||||
$cacheTimeout = $event->getCacheLifetime();
|
||||
$this->runtimeCache->set($cachedCacheLifetimeIdentifier, (string)$cacheTimeout);
|
||||
return $cacheTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates page cache timeout according to the records with starttime/endtime on the page.
|
||||
*
|
||||
* @return int Page cache timeout or PHP_INT_MAX if the timeout cannot be determined
|
||||
*/
|
||||
protected function calculatePageCacheLifetime(array $tablesToConsider, int $currentTimestamp): int
|
||||
{
|
||||
$result = PHP_INT_MAX;
|
||||
// Find timeout by checking every table
|
||||
foreach ($tablesToConsider as $tableDef) {
|
||||
$result = min($result, $this->getFirstTimeValueForRecord($tableDef, $currentTimestamp));
|
||||
}
|
||||
// We return + 1 second just to ensure that cache is definitely regenerated
|
||||
return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $currentTimestamp + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of table/pid pairs to consider for page caching.
|
||||
*
|
||||
* TS configuration looks like this:
|
||||
*
|
||||
* The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account:
|
||||
* config.cache.all = tt_news:14
|
||||
*
|
||||
* The cache lifetime of the current page allows to take records (e.g. fe_users) into account:
|
||||
* config.cache.all = fe_users:current
|
||||
*
|
||||
* The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account:
|
||||
* config.cache.42 = tt_news:15,tt_address:16
|
||||
*
|
||||
* @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array
|
||||
* @see calculatePageCacheLifetime()
|
||||
*/
|
||||
protected function getCurrentPageCacheConfiguration(int $currentPageId, array $renderingInstructions): array
|
||||
{
|
||||
$result = ['tt_content:' . $currentPageId];
|
||||
if (isset($renderingInstructions['cache.'][$currentPageId])) {
|
||||
$result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $currentPageId, $renderingInstructions['cache.'][$currentPageId])));
|
||||
}
|
||||
if (isset($renderingInstructions['cache.']['all'])) {
|
||||
$result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $currentPageId, $renderingInstructions['cache.']['all'])));
|
||||
}
|
||||
return array_unique($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the minimum starttime or endtime value in the table and pid that is greater than the current time.
|
||||
*
|
||||
* @param string $tableDef Table definition (format tablename:pid)
|
||||
* @param int $currentTimestamp the UNIX timestamp of the current time
|
||||
* @throws \InvalidArgumentException
|
||||
* @return int Value of the next start/stop time or PHP_INT_MAX if not found
|
||||
* @see calculatePageCacheLifetime()
|
||||
*/
|
||||
protected function getFirstTimeValueForRecord(string $tableDef, int $currentTimestamp): int
|
||||
{
|
||||
$result = PHP_INT_MAX;
|
||||
[$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef);
|
||||
if (empty($tableName) || !isset($pid)) {
|
||||
throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365);
|
||||
}
|
||||
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeByType(StartTimeRestriction::class)
|
||||
->removeByType(EndTimeRestriction::class);
|
||||
$timeFields = [];
|
||||
$timeConditions = $queryBuilder->expr()->or();
|
||||
if ($this->tcaSchemaFactory->has($tableName)) {
|
||||
$schema = $this->tcaSchemaFactory->get($tableName);
|
||||
// If the record has a starttime or endtime, we have to adjust the cache timeout
|
||||
foreach ([TcaSchemaCapability::RestrictionStartTime, TcaSchemaCapability::RestrictionEndTime] as $capability) {
|
||||
if (!$schema->hasCapability($capability)) {
|
||||
continue;
|
||||
}
|
||||
$timeField = $schema->getCapability($capability)->getFieldName();
|
||||
$queryBuilder->addSelectLiteral(
|
||||
'MIN('
|
||||
. 'CASE WHEN '
|
||||
. $queryBuilder->expr()->lte(
|
||||
$timeField,
|
||||
$queryBuilder->createNamedParameter($currentTimestamp, Connection::PARAM_INT)
|
||||
)
|
||||
. ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeField) . ' END'
|
||||
. ') AS ' . $queryBuilder->quoteIdentifier($timeField)
|
||||
);
|
||||
$timeConditions = $timeConditions->with(
|
||||
$queryBuilder->expr()->gt(
|
||||
$timeField,
|
||||
$queryBuilder->createNamedParameter($currentTimestamp, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
$timeFields[] = $timeField;
|
||||
}
|
||||
}
|
||||
|
||||
// if starttime or endtime are defined, evaluate them
|
||||
if ($timeFields !== []) {
|
||||
// find the timestamp, when the current page's content changes the next time
|
||||
$row = $queryBuilder
|
||||
->from($tableName)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)
|
||||
),
|
||||
$timeConditions
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
|
||||
if ($row) {
|
||||
foreach ($timeFields as $timeField) {
|
||||
// if a MIN value is found, take it into account for the
|
||||
// cache lifetime we have to filter out start/endtimes < $currentTimestamp,
|
||||
// as the SQL query also returns rows with starttime < $currentTimestamp
|
||||
// and endtime > $currentTimestamp (and using a starttime from the past
|
||||
// would be wrong)
|
||||
if ($row[$timeField] !== null && (int)$row[$timeField] > $currentTimestamp) {
|
||||
$result = min($result, (int)$row[$timeField]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user