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

expires tag #14969

Merged
merged 9 commits into from
May 27, 2024
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
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