Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set cache durations based on entries' expiry dates #11901

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Added the `|string` Twig filter. ([#11792](https://github.com/craftcms/cms/pull/11792))
- Added support for the `CRAFT_DOTENV_PATH` PHP constant. ([#11894](https://github.com/craftcms/cms/discussions/11894))
- Added the `version` database connection setting. ([#11900](https://github.com/craftcms/cms/pull/11900))
- Added `craft\base\ExpirableElementInterface`. ([#11901](https://github.com/craftcms/cms/pull/11901))
- Added `craft\db\ActiveQuery::collect()`. ([#11842](https://github.com/craftcms/cms/pull/11842))
- Added `craft\db\SchemaTrait`.
- Added `craft\elements\actions\Restore::$restorableElementsOnly`.
Expand Down Expand Up @@ -57,6 +58,10 @@
- Added `craft\services\Elements::canDuplicate()`.
- Added `craft\services\Elements::canSave()`.
- Added `craft\services\Elements::canView()`.
- Added `craft\services\Elements::getIsCollectingCacheInfo()`. ([#11901](https://github.com/craftcms/cms/pull/11901))
- Added `craft\services\Elements::setCacheExpiryDate()`. ([#11901](https://github.com/craftcms/cms/pull/11901))
- Added `craft\services\Elements::startCollectingCacheInfo()`. ([#11901](https://github.com/craftcms/cms/pull/11901))
- Added `craft\services\Elements::stopCollectingCacheInfo()`. ([#11901](https://github.com/craftcms/cms/pull/11901))
- Added `craft\services\Search::EVENT_BEFORE_SCORE_RESULTS`. ([#11882](https://github.com/craftcms/cms/discussions/11882))
- Added `craft\web\Controller::getCurrentUser()`. ([#11754](https://github.com/craftcms/cms/pull/11754))
- Added `craft\web\View::EVENT_AFTER_CREATE_TWIG`. ([#11774](https://github.com/craftcms/cms/pull/11774))
Expand All @@ -73,6 +78,7 @@
- Element query date params now support passing `today`, `tomorrow`, and `yesterday`. ([#10485](https://github.com/craftcms/cms/issues/10485))
- Element queries now support passing ambiguous column names (e.g. `dateCreated`) and field handles into `select()`. ([#11790](https://github.com/craftcms/cms/pull/11790), [#11800](https://github.com/craftcms/cms/pull/11800))
- `{% cache %}` tags now store any HTML registered with `{% html %}` tags. ([#11811](https://github.com/craftcms/cms/discussions/11811))
- `{% cache %}` tags and GraphQL query caches now get a max cache duration based on the fetched/referenced entries’ expiry dates. ([#8525](https://github.com/craftcms/cms/discussions/8525), [#11901](https://github.com/craftcms/cms/pull/11901))
- Control panel `.twig` templates are now prioritized over `.html`. ([#11809](https://github.com/craftcms/cms/discussions/11809), [#11840](https://github.com/craftcms/cms/pull/11840))
- `craft\helpers\Component::iconSvg()` now namespaces the SVG contents, and adds `aria-hidden="true"`. ([#11703](https://github.com/craftcms/cms/pull/11703))
- `craft\services\Search::EVENT_AFTER_SEARCH` now includes the computed search result scores, set to `craft\events\SearchEvent::$scores`, and any changes made to it will be returned by `searchElements()`. ([#11882](https://github.com/craftcms/cms/discussions/11882))
Expand All @@ -88,3 +94,6 @@
- Deprecated `craft\base\Element::EVENT_AUTHORIZE_SAVE`. `craft\services\Elements::EVENT_AUTHORIZE_SAVE` should be used instead.
- Deprecated `craft\base\Element::EVENT_AUTHORIZE_VIEW`. `craft\services\Elements::EVENT_AUTHORIZE_VIEW` should be used instead.
- Deprecated `craft\elements\Address::addressAttributeLabel()`. `craft\services\Addresses::getFieldLabel()` should be used instead.
- Deprecated `craft\services\Elements::getIsCollectingCacheTags()`. `getIsCollectingCacheInfo()` should be used instead. ([#11901](https://github.com/craftcms/cms/pull/11901))
- Deprecated `craft\services\Elements::startCollectingCacheTags()`. `startCollectingCacheInfo()` should be used instead. ([#11901](https://github.com/craftcms/cms/pull/11901))
- Deprecated `craft\services\Elements::stopCollectingCacheTags()`. `stopCollectingCacheInfo()` should be used instead. ([#11901](https://github.com/craftcms/cms/pull/11901))
26 changes: 26 additions & 0 deletions src/base/ExpirableElementInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\base;

use DateTime;

/**
* ExpirableElementInterface defines the common interface to be implemented by element classes that can expire.
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 4.3.0
*/
interface ExpirableElementInterface
{
/**
* Returns the element’s expiration date/time.
*
* @return DateTime|null
*/
public function getExpiryDate(): ?DateTime;
}
11 changes: 10 additions & 1 deletion src/elements/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Craft;
use craft\base\Element;
use craft\base\ElementInterface;
use craft\base\ExpirableElementInterface;
use craft\base\Field;
use craft\behaviors\DraftBehavior;
use craft\behaviors\RevisionBehavior;
Expand Down Expand Up @@ -67,7 +68,7 @@
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 3.0.0
*/
class Entry extends Element
class Entry extends Element implements ExpirableElementInterface
{
public const STATUS_LIVE = 'live';
public const STATUS_PENDING = 'pending';
Expand Down Expand Up @@ -1014,6 +1015,14 @@ public function getFieldLayout(): ?FieldLayout
return $entryType->getFieldLayout();
}

/**
* @inheritdoc
*/
public function getExpiryDate(): ?DateTime
{
return $this->expiryDate;
}

/**
* Returns the entry’s section.
*
Expand Down
13 changes: 12 additions & 1 deletion src/elements/db/ElementQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Craft;
use craft\base\Element;
use craft\base\ElementInterface;
use craft\base\ExpirableElementInterface;
use craft\base\FieldInterface;
use craft\behaviors\CustomFieldBehavior;
use craft\behaviors\DraftBehavior;
Expand Down Expand Up @@ -1918,7 +1919,7 @@ protected function afterPrepare(): bool
}

$elementsService = Craft::$app->getElements();
if ($elementsService->getIsCollectingCacheTags()) {
if ($elementsService->getIsCollectingCacheInfo()) {
$elementsService->collectCacheTags($this->getCacheTags());
}

Expand Down Expand Up @@ -2926,6 +2927,7 @@ private function _fieldColumn(FieldInterface $field): array|string|null
*/
private function _createElements(array $rows): array
{
$elementsService = Craft::$app->getElements();
$elements = [];

if ($this->asArray === true) {
Expand Down Expand Up @@ -2958,6 +2960,15 @@ private function _createElements(array $rows): array

$elements[$key] = $element;
}

// If we're collecting cache info and the element is expirable, register its expiry date
if (
$element instanceof ExpirableElementInterface &&
$elementsService->getIsCollectingCacheInfo() &&
($expiryDate = $element->getExpiryDate()) !== null
) {
$elementsService->setCacheExpiryDate($expiryDate);
}
}

ElementHelper::setNextPrevOnElements($elements);
Expand Down
11 changes: 10 additions & 1 deletion src/helpers/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Craft;
use craft\base\ElementInterface;
use craft\base\ExpirableElementInterface;
use craft\db\Paginator;
use craft\web\twig\variables\Paginate;
use craft\web\View;
Expand Down Expand Up @@ -72,13 +73,21 @@ public static function attribute(Environment $env, Source $source, mixed $object
// Include this element in any active caches
if ($object instanceof ElementInterface) {
$elementsService = Craft::$app->getElements();
if ($elementsService->getIsCollectingCacheTags()) {
if ($elementsService->getIsCollectingCacheInfo()) {
$class = get_class($object);
$elementsService->collectCacheTags([
'element',
"element::$class",
"element::$class::$object->id",
]);

// If the element is expirable, register its expiry date
if (
$object instanceof ExpirableElementInterface &&
($expiryDate = $object->getExpiryDate()) !== null
) {
$elementsService->setCacheExpiryDate($expiryDate);
}
}
}

Expand Down
136 changes: 123 additions & 13 deletions src/services/Elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use craft\base\ElementActionInterface;
use craft\base\ElementExporterInterface;
use craft\base\ElementInterface;
use craft\base\ExpirableElementInterface;
use craft\behaviors\DraftBehavior;
use craft\behaviors\RevisionBehavior;
use craft\db\Query;
Expand Down Expand Up @@ -59,6 +60,7 @@
use craft\validators\HandleValidator;
use craft\validators\SlugValidator;
use craft\web\Application;
use DateTime;
use Throwable;
use yii\base\Behavior;
use yii\base\Component;
Expand Down Expand Up @@ -486,36 +488,71 @@ public function createElementQuery(string $elementType): ElementQueryInterface
* @var array[]
*/
private array $_cacheTagBuffers = [];

/**
* @var string[]|null
*/
private ?array $_cacheTags = null;

/**
* @var array
* @phpstan-var array<int|null>
*/
private array $_cacheDurationBuffers = [];

private ?int $_cacheDuration = null;

/**
* Returns whether we are currently collecting element cache invalidation info.
*
* @return bool
* @since 4.3.0
* @see startCollectingCacheInfo()
* @see stopCollectingCacheInfo()
*/
public function getIsCollectingCacheInfo(): bool
{
return isset($this->_cacheTags);
}

/**
* Returns whether we are currently collecting element cache invalidation tags.
*
* @return bool
* @since 3.5.0
* @see startCollectingCacheTags()
* @see stopCollectingCacheTags()
* @deprecated in 4.3.0. [[getIsCollectingCacheInfo()]] should be used instead.
*/
public function getIsCollectingCacheTags(): bool
{
return isset($this->_cacheTags);
return $this->getIsCollectingCacheInfo();
}

/**
* Starts collecting element cache invalidation tags.
* Starts collecting element cache invalidation info.
*
* @since 3.5.0
* @since 4.3.0
*/
public function startCollectingCacheTags(): void
public function startCollectingCacheInfo(): void
{
// Save any currently-collected tags into a new buffer, and reset the array
// Save any currently-collected info into new buffers
if (isset($this->_cacheTags)) {
$this->_cacheTagBuffers[] = $this->_cacheTags;
$this->_cacheDurationBuffers[] = $this->_cacheDuration;
}

$this->_cacheTags = [];
$this->_cacheDuration = null;
}

/**
* Starts collecting element cache invalidation tags.
*
* @since 3.5.0
* @deprecated in 4.3.0. [[startCollectingCacheInfo()]] should be used instead.
*/
public function startCollectingCacheTags(): void
{
$this->startCollectingCacheInfo();
}

/**
Expand All @@ -538,29 +575,90 @@ public function collectCacheTags(array $tags): void
}

/**
* Stops collecting element cache invalidation tags, and returns a cache dependency object.
* Sets a possible cache expiration date that [[stopCollectingCacheInfo()]] should return.
*
* @return TagDependency
* @since 3.5.0
* The value will only be used if it is less than the currently stored expiration date.
*
* @param DateTime $expiryDate
* @since 4.3.0
*/
public function stopCollectingCacheTags(): TagDependency
public function setCacheExpiryDate(DateTime $expiryDate): void
{
if (!isset($this->_cacheTags)) {
return;
}

$duration = $expiryDate->getTimestamp() - time();

if ($duration > 0 && (!$this->_cacheDuration || $duration < $this->_cacheDuration)) {
$this->_cacheDuration = $duration;
}
}

/**
* Stops collecting element invalidation info, and returns a [[TagDependency]] and recommended max cache duration
* that should be used when saving the cache data.
*
* If no cache tags were registered, `[null, null]` will be returned.
*
* @return array
* @phpstan-return array{TagDependency|null,int|null}
*/
public function stopCollectingCacheInfo(): array
{
if (!isset($this->_cacheTags)) {
throw new InvalidCallException('Element cache invalidation tags are not currently being collected.');
}

$tags = $this->_cacheTags;
$duration = $this->_cacheDuration;

// Was there another active collection?
if (!empty($this->_cacheTagBuffers)) {
$this->_cacheTags = array_merge(array_pop($this->_cacheTagBuffers), $tags);

// Override the parent duration if ours is shorter
$this->_cacheDuration = array_pop($this->_cacheDurationBuffers);
if ($duration && $duration < $this->_cacheDuration) {
$this->_cacheDuration = $duration;
}
} else {
$this->_cacheTags = null;
$this->_cacheDuration = null;
}

if (empty($tags)) {
return [null, null];
}

return new TagDependency([
// Only use the duration if it's less than the cacheDuration config setting
$generalConfig = Craft::$app->getConfig()->getGeneral();
if ($generalConfig->cacheDuration) {
if ($duration) {
$duration = min($duration, $generalConfig->cacheDuration);
} else {
$duration = $generalConfig->cacheDuration;
}
}

$dep = new TagDependency([
'tags' => array_keys($tags),
]);

return [$dep, $duration];
}

/**
* Stops collecting element cache invalidation tags, and returns a cache dependency object.
*
* @return TagDependency
* @since 3.5.0
* @deprecated in 4.3.0. [[stopCollectingCacheInfo()]] should be used instead.
*/
public function stopCollectingCacheTags(): TagDependency
{
[$dep] = $this->stopCollectingCacheInfo();
return $dep ?? new TagDependency();
}

/**
Expand Down Expand Up @@ -2422,6 +2520,8 @@ public function eagerLoadElements(string $elementType, array $elements, array|st
*/
private function _eagerLoadElementsInternal(string $elementType, array $elementsBySite, array $with): void
{
$elementsService = Craft::$app->getElements();

foreach ($elementsBySite as $siteId => $elements) {
// In case the elements were
$elements = array_values($elements);
Expand Down Expand Up @@ -2574,7 +2674,17 @@ private function _eagerLoadElementsInternal(string $elementType, array $elements
if (!isset($targetElements[$targetSiteId][$elementId])) {
$targetElements[$targetSiteId][$elementId] = $query->createElement($result);
}
$targetElementsForSource[] = $targetElements[$targetSiteId][$elementId];
$targetElementsForSource[] = $element = $targetElements[$targetSiteId][$elementId];

// If we're collecting cache info and the element is expirable, register its expiry date
if (
$element instanceof ExpirableElementInterface &&
$elementsService->getIsCollectingCacheInfo() &&
($expiryDate = $element->getExpiryDate()) !== null
) {
$elementsService->setCacheExpiryDate($expiryDate);
}

if ($limit && ++$count == $limit) {
break 2;
}
Expand Down
Loading