diff --git a/composer.json b/composer.json index 5b0142b..5c295ec 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "cakephp/cakephp": "^5.0.0", "jasny/twig-extensions": "^1.3", "twig/markdown-extra": "^3.0", - "twig/twig": "^3.4" + "twig/twig": "^3.10.3" }, "require-dev": { "cakephp/cakephp-codesniffer": "^5.0", diff --git a/src/Twig/Extension/TimeExtension.php b/src/Twig/Extension/TimeExtension.php index d76a6ac..077df5e 100644 --- a/src/Twig/Extension/TimeExtension.php +++ b/src/Twig/Extension/TimeExtension.php @@ -18,8 +18,13 @@ namespace Cake\TwigView\Twig\Extension; +use Cake\Chronos\Chronos; +use Cake\Chronos\ChronosDate; use Cake\I18n\DateTime; +use DateTimeZone; use Twig\Extension\AbstractExtension; +use Twig\Extension\CoreExtension; +use Twig\TwigFilter; use Twig\TwigFunction; /** @@ -27,6 +32,20 @@ */ class TimeExtension extends AbstractExtension { + private ?CoreExtension $coreExt; + + /** + * Get declared filters. + * + * @return array<\Twig\TwigFilter> + */ + public function getFilters(): array + { + return [ + new TwigFilter('date', [$this, 'formatDate']), + ]; + } + /** * Get declared functions. * @@ -35,10 +54,40 @@ class TimeExtension extends AbstractExtension public function getFunctions(): array { return [ + new TwigFunction('date', function ($time = null, $timezone = null) { + return new DateTime($time, $timezone); + }), new TwigFunction('time', function ($time = null, $timezone = null) { return new DateTime($time, $timezone); }), new TwigFunction('timezones', 'Cake\I18n\DateTime::listTimezones'), ]; } + + /** + * Format a date/datetime value + * + * Includes shims for \Chronos\ChronosDate as Twig doesn't. + * + * @param mixed $date The date to format. + * @param ?string $format The format to use, null to use the default. + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use system. + */ + public function formatDate( + mixed $date, + ?string $format = null, + DateTimeZone|string|false|null $timezone = null + ): string { + if (!isset($this->coreExt)) { + $this->coreExt = new CoreExtension(); + } + if ($date instanceof ChronosDate) { + $date = $date->toDateString(); + } + if ($date instanceof Chronos) { + $date = $date->toIso8601String(); + } + + return $this->coreExt->formatDate($date, $format, $timezone); + } } diff --git a/tests/TestCase/View/TwigViewTest.php b/tests/TestCase/View/TwigViewTest.php index bc60cf1..80c0e98 100644 --- a/tests/TestCase/View/TwigViewTest.php +++ b/tests/TestCase/View/TwigViewTest.php @@ -19,6 +19,9 @@ namespace Cake\TwigView\Test\TestCase\View; use Cake\Core\Configure; +use Cake\I18n\Date; +use Cake\I18n\DateTime; +use Cake\I18n\I18n; use Cake\TestSuite\TestCase; use TestApp\View\AppView; use Twig\Error\RuntimeError; @@ -136,6 +139,27 @@ public function testCellsShareTwig() $this->assertSame($this->view->getTwig(), $cell->createView(AppView::class)->getTwig()); } + /** + * Test that Cake date/time objects are formatted correctly + */ + public function testTwigDateFormat() + { + $restore = I18n::getLocale(); + I18n::setLocale('fr'); + + $this->view->set('date', new Date('2024-06-24')); + $this->view->set('datetime', new DateTime('2024-06-24 12:13:14')); + + $output = $this->view->render('date_format', false); + I18n::setLocale($restore); + + $expected = <<assertSame($expected, $output); + } + /** * Tests rendering with markdown. * diff --git a/tests/test_app/templates/date_format.twig b/tests/test_app/templates/date_format.twig new file mode 100644 index 0000000..2781b5f --- /dev/null +++ b/tests/test_app/templates/date_format.twig @@ -0,0 +1,2 @@ +Date: {{ date|date('Y/m/d') }} +Datetime: {{ datetime|date('Y/m/d H:i:s') }} \ No newline at end of file