Skip to content

Commit

Permalink
Merge pull request #14969 from craftcms/feature/cache-response-tag
Browse files Browse the repository at this point in the history
`expires` tag
  • Loading branch information
brandonkelly authored May 27, 2024
2 parents 3074153 + 56647bf commit fd721ca
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 38 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- GraphQL schema edit pages now have a “Save and continue editing” alternate action.

### Development
- Added the `{% expires %}` tag, which simplifies setting cache headers on the response. ([#14969](https://github.com/craftcms/cms/pull/14969))
- Added `craft\elements\ElementCollection::find()`, which can return an element or elements in the collection based on a given element or ID. ([#15023](https://github.com/craftcms/cms/discussions/15023))
- Added `craft\elements\ElementCollection::fresh()`, which reloads each of the collection elements from the database. ([#15023](https://github.com/craftcms/cms/discussions/15023))
- `craft\elements\ElementCollection::contains()` now returns `true` if an element is passed in and the collection contains an element with the same ID and site ID; or if an integer is passed in and the collection contains an element with the same ID. ([#15023](https://github.com/craftcms/cms/discussions/15023))
Expand All @@ -31,6 +32,8 @@
- Added `craft\db\setRestoreFormat()`.
- Added `craft\events\InvalidateElementcachesEvent::$element`.
- Added `craft\fields\BaseRelationField::existsQueryCondition()`.
- Added `craft\helpers\DateTimeHelper::relativeTimeStatement()`.
- Added `craft\helpers\DateTimeHelper::relativeTimeToSeconds()`.
- Added `craft\helpers\StringHelper::indent()`.
- Added `craft\queue\Queue::getJobId()`.
- `craft\elements\ElementCollection::with()` now supports collections made up of multiple element types.
Expand Down
70 changes: 70 additions & 0 deletions src/helpers/DateTimeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,37 @@ class DateTimeHelper
*/
public const SECONDS_YEAR = 31556874;

/**
* @var string[] Supported relative time units.
* @see relativeTimeStatement()
* @see relativeTimeToSeconds()
* @since 4.10.0
*/
public const RELATIVE_TIME_UNITS = [
'sec',
'secs',
'second',
'seconds',
'min',
'mins',
'minute',
'minutes',
'hour',
'hours',
'day',
'days',
'fortnight',
'fortnights',
'forthnight',
'forthnights',
'month',
'months',
'year',
'years',
'week',
'weeks',
];

/**
* @var DateTime[]
* @see pause()
Expand Down Expand Up @@ -809,6 +840,45 @@ public static function humanDurationFromInterval(DateInterval $dateInterval, boo
return static::humanDuration($dateInterval, $showSeconds);
}

/**
* Returns a [relative time statement](https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative)
* based on the given number and unit.
*
* @param int $number
* @param string $unit
* @return string
* @since 4.10.0
*/
public static function relativeTimeStatement(int $number, string $unit): string
{
// PHP doesn't support "+1 week"
if ($unit === 'week') {
if ($number == 1) {
$number = 7;
$unit = 'days';
} else {
$unit = 'weeks';
}
}

return "+$number $unit";
}

/**
* Converts a relative time (number and unit) to seconds.
*
* @param int $number
* @param string $unit
* @return int
* @since 4.10.0
*/
public static function relativeTimeToSeconds(int $number, string $unit): int
{
$now = new DateTimeImmutable();
$then = $now->modify(static::relativeTimeStatement($number, $unit));
return $then->getTimestamp() - $now->getTimestamp();
}

/**
* Normalizes and returns a date string along with the format it was set in.
*
Expand Down
2 changes: 1 addition & 1 deletion src/services/TemplateCaches.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public function startTemplateCache(bool $withResources = false, bool $global = f
*
* @param string $key The template cache key.
* @param bool $global Whether the cache should be stored globally.
* @param string|null $duration How long the cache should be stored for. Should be a [relative time format](https://php.net/manual/en/datetime.formats.relative.php).
* @param string|null $duration How long the cache should be stored for. Should be a [relative time statement](https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative).
* @param mixed $expiration When the cache should expire.
* @param string $body The contents of the cache.
* @param bool $withResources Whether JS and CSS code registered with [[\craft\web\View::registerJs()]],
Expand Down
2 changes: 2 additions & 0 deletions src/web/twig/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
use craft\web\twig\tokenparsers\DeprecatedTokenParser;
use craft\web\twig\tokenparsers\DumpTokenParser;
use craft\web\twig\tokenparsers\ExitTokenParser;
use craft\web\twig\tokenparsers\ExpiresTokenParser;
use craft\web\twig\tokenparsers\HeaderTokenParser;
use craft\web\twig\tokenparsers\HookTokenParser;
use craft\web\twig\tokenparsers\NamespaceTokenParser;
Expand Down Expand Up @@ -135,6 +136,7 @@ public function getTokenParsers(): array
new DdTokenParser(),
new DumpTokenParser(),
new ExitTokenParser(),
new ExpiresTokenParser(),
new HeaderTokenParser(),
new HookTokenParser(),
new RegisterResourceTokenParser('css', TemplateHelper::class . '::css', [
Expand Down
15 changes: 3 additions & 12 deletions src/web/twig/nodes/CacheNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace craft\web\twig\nodes;

use Craft;
use craft\helpers\DateTimeHelper;
use craft\helpers\StringHelper;
use Twig\Compiler;
use Twig\Node\Node;
Expand Down Expand Up @@ -95,18 +96,8 @@ public function compile(Compiler $compiler): void
->write("\$cacheService->endTemplateCache(\$cacheKey$n, $global, ");

if ($durationNum) {
// So silly that PHP doesn't support "+1 week" http://www.php.net/manual/en/datetime.formats.relative.php

if ($durationUnit === 'week') {
if ($durationNum == 1) {
$durationNum = 7;
$durationUnit = 'days';
} else {
$durationUnit = 'weeks';
}
}

$compiler->raw("'+$durationNum $durationUnit'");
$duration = DateTimeHelper::relativeTimeStatement($durationNum, $durationUnit);
$compiler->raw("'$duration'");
} else {
$compiler->raw('null');
}
Expand Down
47 changes: 47 additions & 0 deletions src/web/twig/nodes/ExpiresNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\web\twig\nodes;

use craft\helpers\DateTimeHelper;
use Twig\Compiler;
use Twig\Node\Node;

/**
* Class ExpiresNode
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 4.10.0
*/
class ExpiresNode extends Node
{
/**
* @inheritdoc
*/
public function compile(Compiler $compiler): void
{
$expiration = $this->hasNode('expiration') ? $this->getNode('expiration') : null;

if ($expiration) {
$compiler
->write('$expiration = ')
->subcompile($expiration)
->raw(";\n")
->write('$duration = \craft\helpers\DateTimeHelper::toDateTime($expiration)->getTimestamp() - time();');
} else {
$duration = DateTimeHelper::relativeTimeToSeconds(
$this->getAttribute('durationNum'),
$this->getAttribute('durationUnit'),
);
$compiler->write("\$duration = $duration;\n");
}

$compiler
->write('\Craft::$app->getResponse()->setCacheHeaders($duration);')
->raw("\n");
}
}
27 changes: 2 additions & 25 deletions src/web/twig/tokenparsers/CacheTokenParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace craft\web\twig\tokenparsers;

use craft\helpers\DateTimeHelper;
use craft\web\twig\nodes\CacheNode;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
Expand Down Expand Up @@ -58,31 +59,7 @@ public function parse(Token $token): CacheNode
if ($stream->test(Token::NAME_TYPE, 'for')) {
$stream->next();
$attributes['durationNum'] = $stream->expect(Token::NUMBER_TYPE)->getValue();
$attributes['durationUnit'] = $stream->expect(Token::NAME_TYPE,
[
'sec',
'secs',
'second',
'seconds',
'min',
'mins',
'minute',
'minutes',
'hour',
'hours',
'day',
'days',
'fortnight',
'fortnights',
'forthnight',
'forthnights',
'month',
'months',
'year',
'years',
'week',
'weeks',
])->getValue();
$attributes['durationUnit'] = $stream->expect(Token::NAME_TYPE, DateTimeHelper::RELATIVE_TIME_UNITS)->getValue();
} elseif ($stream->test(Token::NAME_TYPE, 'until')) {
$stream->next();
$nodes['expiration'] = $parser->getExpressionParser()->parseExpression();
Expand Down
60 changes: 60 additions & 0 deletions src/web/twig/tokenparsers/ExpiresTokenParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\web\twig\tokenparsers;

use craft\helpers\DateTimeHelper;
use craft\web\twig\nodes\ExpiresNode;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;

/**
* Class ExpiresTokenParser
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 4.10.0
*/
class ExpiresTokenParser extends AbstractTokenParser
{
/**
* @inheritdoc
*/
public function parse(Token $token): ExpiresNode
{
$lineno = $token->getLine();
$parser = $this->parser;
$stream = $parser->getStream();

$nodes = [];

$attributes = [
'durationNum' => 0,
'durationUnit' => 'seconds',
];

if ($stream->test(Token::OPERATOR_TYPE, 'in')) {
$stream->next();
$attributes['durationNum'] = $stream->expect(Token::NUMBER_TYPE)->getValue();
$attributes['durationUnit'] = $stream->expect(Token::NAME_TYPE, DateTimeHelper::RELATIVE_TIME_UNITS)->getValue();
} elseif ($stream->test(Token::NAME_TYPE, 'on')) {
$stream->next();
$nodes['expiration'] = $parser->getExpressionParser()->parseExpression();
}

$stream->expect(Token::BLOCK_END_TYPE);

return new ExpiresNode($nodes, $attributes, $lineno, $this->getTag());
}

/**
* @inheritdoc
*/
public function getTag(): string
{
return 'expires';
}
}
46 changes: 46 additions & 0 deletions tests/unit/helpers/DateTimeHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,28 @@ public function testHumanDuration(string $expected, string|int $duration, ?bool
self::assertSame($expected, DateTimeHelper::humanDuration($duration, $showSeconds));
}

/**
* @dataProvider relativeTimeStatementDataProvider
* @param string $expected
* @param int $number
* @param string $unit
*/
public function testRelativeTimeStatement(string $expected, int $number, string $unit): void
{
self::assertSame($expected, DateTimeHelper::relativeTimeStatement($number, $unit));
}

/**
* @dataProvider relativeTimeToSecondsDataProvider
* @param int $expected
* @param int $number
* @param string $unit
*/
public function testRelativeTimeToSeconds(int $expected, int $number, string $unit): void
{
self::assertSame($expected, DateTimeHelper::relativeTimeToSeconds($number, $unit));
}

/**
* @throws Exception
*/
Expand Down Expand Up @@ -847,6 +869,30 @@ public function humanDurationDataProvider(): array
];
}

/**
* @return array
*/
public function relativeTimeStatementDataProvider(): array
{
return [
['+1 day', 1, 'day'],
['+7 days', 1, 'week'],
['+1 weeks', 1, 'weeks'],
['+2 weeks', 2, 'weeks'],
];
}

/**
* @return array
*/
public function relativeTimeToSecondsDataProvider(): array
{
return [
[3600, 1, 'hour'],
[604800, 1, 'week'],
];
}

/**
* @return array
*/
Expand Down

0 comments on commit fd721ca

Please sign in to comment.