From 36a27cc2fcffa93e5fd56bde7602b499d4f312ce Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 28 Mar 2024 18:14:50 +0100 Subject: [PATCH 1/6] Add a PHP 8 attribute for configuration --- README.md | 2 +- composer.json | 5 +- .../ReplaceWithNotModifiedResponse.php | 88 +++-------------- .../ReplaceWithNotModifiedResponse.php | 97 +++++++++++++++++++ src/NotModified/EventListener.php | 20 +++- 5 files changed, 130 insertions(+), 82 deletions(-) create mode 100644 src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php diff --git a/README.md b/README.md index c124d81..9275465 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ services: and note the service name to the Annotation: - @ReplaceWithNotModifiedResponse({"app_caching_latest_posts"}) + @ReplaceWithNotModifiedResponse({"@app_caching_latest_posts"}) To combine multiple LastModifiedDeterminators, simply add all of them to the annotation: diff --git a/composer.json b/composer.json index 4349bc4..40dc77d 100644 --- a/composer.json +++ b/composer.json @@ -20,11 +20,12 @@ "require": { "php": "^7.1|8.0.*|8.1.*", + "doctrine/annotations": "^1.0", "symfony/config": "^4.4 | ^5.0 | ^6.0", "symfony/dependency-injection": "^4.4 | ^5.0 | ^6.0", + "symfony/deprecation-contracts": "^2.0|^3.0", "symfony/http-foundation": "^4.4 | ^5.0 | ^6.0", - "symfony/http-kernel": "^4.4 | ^5.0 | ^6.0", - "doctrine/annotations": "^1.0" + "symfony/http-kernel": "^4.4 | ^5.0 | ^6.0" }, "require-dev": { diff --git a/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php b/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php index 88964ad..7769979 100644 --- a/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php +++ b/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php @@ -9,88 +9,22 @@ namespace Webfactory\HttpCacheBundle\NotModified\Annotation; -use DateTime; -use RuntimeException; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; -use Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator; - /** - * This Annotation determines the latest last modified date over all of its LastModifiedDeterminators. This date is used - * by the \Webfactory\HttpCacheBundle\NotModified\EventListener to possibly replace the execution of a controller with - * sending a Not Modified HTTP response. - * * @Annotation + * @deprecated, to be replaced by attribute-based configuration */ -final class ReplaceWithNotModifiedResponse +final class ReplaceWithNotModifiedResponse extends \Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse { - /** @var array */ - private $parameters; - - /** @var LastModifiedDeterminator[] */ - private $lastModifiedDeterminators; - - /** @var ContainerInterface */ - private $container; - - /** @var DateTime|null */ - private $lastModified; - public function __construct(array $parameters) { - $this->parameters = $parameters; - } - - /** - * @return DateTime|null - */ - public function determineLastModified(Request $request) - { - $this->initialiseLastModifiedDeterminators(); - - foreach ($this->lastModifiedDeterminators as $lastModifiedDeterminator) { - $lastModifiedOfCurrentDeterminator = $lastModifiedDeterminator->getLastModified($request); - if (null === $this->lastModified || $this->lastModified < $lastModifiedOfCurrentDeterminator) { - $this->lastModified = $lastModifiedOfCurrentDeterminator; - } - } - - return $this->lastModified; - } - - public function setContainer(ContainerInterface $container) - { - $this->container = $container; - } - - private function initialiseLastModifiedDeterminators() - { - if (0 === count($this->parameters['value'])) { - throw new RuntimeException('The annotation '.get_class($this).' has to be parametrised with LastModifiedDeterminators.'); - } - - foreach ($this->parameters['value'] as $lastModifiedDeterminatorDescription) { - $lastModifiedDeterminator = null; - - if (is_string($lastModifiedDeterminatorDescription)) { - if ('@' === $lastModifiedDeterminatorDescription[0]) { - $lastModifiedDeterminator = $this->container->get(substr($lastModifiedDeterminatorDescription, 1)); - } else { - $lastModifiedDeterminator = new $lastModifiedDeterminatorDescription(); - } - } - - if (is_array($lastModifiedDeterminatorDescription)) { - $lastModifiedDeterminatorClass = key($lastModifiedDeterminatorDescription); - $lastModifiedDeterminatorParameter = current($lastModifiedDeterminatorDescription); - $lastModifiedDeterminator = new $lastModifiedDeterminatorClass($lastModifiedDeterminatorParameter); - } - - if (!($lastModifiedDeterminator instanceof LastModifiedDeterminator)) { - throw new RuntimeException('The class "'.get_class($lastModifiedDeterminator).'" does not implement '.LastModifiedDeterminator::class.'.'); - } - - $this->lastModifiedDeterminators[] = $lastModifiedDeterminator; - } + trigger_deprecation( + 'webfactory/http-cache-bundle', + '1.4.0', + 'The %s annotation has been deprecated, use the %s attribute instead.', + NotModified\Annotation\ReplaceWithNotModifiedResponse::class, + NotModified\Attribute\ReplaceWithNotModifiedResponse::class + ); + + parent::__construct($parameters['value']); } } diff --git a/src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php b/src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php new file mode 100644 index 0000000..9a0bd98 --- /dev/null +++ b/src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webfactory\HttpCacheBundle\NotModified\Attribute; + +use DateTime; +use RuntimeException; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator; + +/** + * This attribute determines the latest last modified date over all of its LastModifiedDeterminators. This date is used + * by the \Webfactory\HttpCacheBundle\NotModified\EventListener to possibly replace the execution of a controller with + * sending a Not Modified HTTP response. + * + * @final + */ +#[\Attribute(\Attribute::TARGET_METHOD)] +class ReplaceWithNotModifiedResponse +{ + /** @var array */ + private $parameters; + + /** @var LastModifiedDeterminator[] */ + private $lastModifiedDeterminators; + + /** @var ContainerInterface */ + private $container; + + /** @var DateTime|null */ + private $lastModified; + + public function __construct(array $parameters) + { + $this->parameters = $parameters; + } + + /** + * @return DateTime|null + */ + public function determineLastModified(Request $request) + { + $this->initialiseLastModifiedDeterminators(); + + foreach ($this->lastModifiedDeterminators as $lastModifiedDeterminator) { + $lastModifiedOfCurrentDeterminator = $lastModifiedDeterminator->getLastModified($request); + if (null === $this->lastModified || $this->lastModified < $lastModifiedOfCurrentDeterminator) { + $this->lastModified = $lastModifiedOfCurrentDeterminator; + } + } + + return $this->lastModified; + } + + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + } + + private function initialiseLastModifiedDeterminators() + { + if (0 === count($this->parameters)) { + throw new RuntimeException('The attribute '.get_class($this).' has to be parametrised with LastModifiedDeterminators.'); + } + + foreach ($this->parameters as $lastModifiedDeterminatorDescription) { + $lastModifiedDeterminator = null; + + if (is_string($lastModifiedDeterminatorDescription)) { + if ('@' === $lastModifiedDeterminatorDescription[0]) { + $lastModifiedDeterminator = $this->container->get(substr($lastModifiedDeterminatorDescription, 1)); + } else { + $lastModifiedDeterminator = new $lastModifiedDeterminatorDescription(); + } + } + + if (is_array($lastModifiedDeterminatorDescription)) { + $lastModifiedDeterminatorClass = key($lastModifiedDeterminatorDescription); + $lastModifiedDeterminatorParameter = current($lastModifiedDeterminatorDescription); + $lastModifiedDeterminator = new $lastModifiedDeterminatorClass($lastModifiedDeterminatorParameter); + } + + if (!($lastModifiedDeterminator instanceof LastModifiedDeterminator)) { + throw new RuntimeException('The class "'.get_class($lastModifiedDeterminator).'" does not implement '.LastModifiedDeterminator::class.'.'); + } + + $this->lastModifiedDeterminators[] = $lastModifiedDeterminator; + } + } +} diff --git a/src/NotModified/EventListener.php b/src/NotModified/EventListener.php index 24d7ddf..f6f63e5 100644 --- a/src/NotModified/EventListener.php +++ b/src/NotModified/EventListener.php @@ -16,7 +16,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Webfactory\HttpCacheBundle\NotModified\Annotation\ReplaceWithNotModifiedResponse; +use Webfactory\HttpCacheBundle\NotModified; /** * Symfony EventListener for adding a "last modified" header to the response on the one hand. On the other hand, it @@ -118,8 +118,24 @@ private function findAnnotation(callable $controllerCallable) [$class, $methodName] = $controllerCallable; $method = new ReflectionMethod($class, $methodName); + if (PHP_MAJOR_VERSION >= 8) { + $attributes = $method->getAttributes(NotModified\Attribute\ReplaceWithNotModifiedResponse::class); + + if ($attributes) { + return $attributes[0]->newInstance(); + } + } + /** @var ReplaceWithNotModifiedResponse|null $annotation */ - $annotation = $this->reader->getMethodAnnotation($method, ReplaceWithNotModifiedResponse::class); + $annotation = $this->reader->getMethodAnnotation($method, NotModified\Annotation\ReplaceWithNotModifiedResponse::class); + + if ($annotation) { + trigger_deprecation( + 'webfactory/http-cache-bundle', + '1.4.0', + 'Configuring webfactory/http-cache-bundle with annotations is deprecated, use attributes instead.' + ); + } return $annotation; } From ac7f6c9b22d173aa0401e31c87c7a119a607f960 Mon Sep 17 00:00:00 2001 From: mpdude Date: Thu, 28 Mar 2024 17:15:28 +0000 Subject: [PATCH 2/6] Fix CS with PHP-CS-Fixer --- .../Annotation/ReplaceWithNotModifiedResponse.php | 1 + src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php b/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php index 7769979..0ee5b4f 100644 --- a/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php +++ b/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php @@ -11,6 +11,7 @@ /** * @Annotation + * * @deprecated, to be replaced by attribute-based configuration */ final class ReplaceWithNotModifiedResponse extends \Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse diff --git a/src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php b/src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php index 9a0bd98..c1fa370 100644 --- a/src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php +++ b/src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php @@ -9,6 +9,7 @@ namespace Webfactory\HttpCacheBundle\NotModified\Attribute; +use Attribute; use DateTime; use RuntimeException; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -19,10 +20,10 @@ * This attribute determines the latest last modified date over all of its LastModifiedDeterminators. This date is used * by the \Webfactory\HttpCacheBundle\NotModified\EventListener to possibly replace the execution of a controller with * sending a Not Modified HTTP response. - * + * * @final */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_METHOD)] class ReplaceWithNotModifiedResponse { /** @var array */ From b582ed73301721c2c10b29b3c5b444eb9a2a29c4 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 28 Mar 2024 18:24:21 +0100 Subject: [PATCH 3/6] Update the README --- README.md | 78 +++++++++++++++++++++++++------------------------------ 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 9275465..40611b6 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ # WebfactoryHttpCacheBundle -WebfactoryHttpCacheBundle is a Symfony bundle that features a more +`WebfactoryHttpCacheBundle` is a Symfony bundle that features a more powerful [HTTP cache validation via the last modified header] than the -```@Cache``` annotation in the excellent [SensioFrameworkExtraBundle]. +`@Cache` annotation in the excellent [SensioFrameworkExtraBundle]. [HTTP cache validation via the last modified header]: https://symfony.com/doc/current/http_cache/validation.html#validation-with-the-last-modified-header [SensioFrameworkExtraBundle]: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html -While the SensioFrameworkExtraBundle's ```@Cache``` annotation restricts -you to the request parameters, the ```@ReplaceWithNotModifiedResponse``` -annotation lets you write small LastModifiedDeterminators for each one -of the underlying ressources of the requested page, They can be reused +While the SensioFrameworkExtraBundle's `@Cache` annotation restricts +you to the request parameters, the `#ReplaceWithNotModifiedResponse` +attribute lets you write small LastModifiedDeterminators for each one +of the underlying ressources of the requested page. They can be reused and combined freely and can even be defined as services. -Lets take the example from the SensioFrameworkExtraBundle docs (stripped +Let's take the example from the `SensioFrameworkExtraBundle` docs (stripped off the ETag part, which is not supported by the -WebfactoryHttpCacheBundle): +`WebfactoryHttpCacheBundle`): ```php use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; @@ -32,14 +32,12 @@ public function indexAction(Post $post) This falls short if the rendered template e.g. contains information about the x latest posts. That can be done with the -```@ReplaceWithNotModifiedResponse``` annotation: +`#[ReplaceWithNotModifiedResponse]` attribute: ```php -use Webfactory\HttpCacheBundle\NotModified\Annotation\ReplaceWithNotModifiedResponse; +use Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse; -/** - * @ReplaceWithNotModifiedResponse({"@app_caching_post", "@app_caching_latest_posts"}) - */ +#[ReplaceWithNotModifiedResponse(["@app_caching_post", "@app_caching_latest_posts"])] public function indexAction(Post $post) { // your code @@ -48,39 +46,37 @@ public function indexAction(Post $post) ``` When Symfony's routing has chosen this controller action, all of the -LastModifiedDeterminators are called to return their respective last +`LastModifiedDeterminator`s are called to return their respective last modified date. In this case, both LastModifiedDeterminators are configured as services: -```@app_caching_post``` and ```@app_caching_latest_posts```. The first +`@app_caching_post` and `@app_caching_latest_posts`. The first one returns the update date of the requests $post, the second one may use the PostRepository injected from the DI container to return the last update date of the x latest posts. -Then, ReplaceWithNotModifiedResponse combines all of the -LastModifiedDeterminators dates to determine to last modified date of +Then, `#[ReplaceWithNotModifiedResponse]` combines all of the +`LastModifiedDeterminators` dates to determine to last modified date of the overall page. Finally, if the request contains an appropriate -```if-not-modified-since``` header, the execution of the controller -action will be skipped and an empty response with a 304 Not Modified -status code will be sent. If your LastModifiedDeterminators are fast, +`if-not-modified-since` header, the execution of the controller +action will be skipped and an empty response with a "304 Not Modified" +status code will be sent. If your `LastModifiedDeterminators` are fast, this can improve your performance greatly. -What we like about the LastModifiedDeterminators is that they encourage +What we like about the `LastModifiedDeterminators` is that they encourage to separate the concerns nicely and encapsulate the tasks into small units that are easy to understand, reusable and unit test. -*Note:* `@ReplaceWithNotModifiedResponse` does not alter or add +*Note:* `#[ReplaceWithNotModifiedResponse]` does not alter or add `Cache-Control` header settings. So, by default your response will remain `private` and end up in browser caches only. If you want it to be kept in surrogate caches (like Varnish or the Symfony Http Cache), you -can add `@Cache(smaxage="0")`. This will make the response `public`, but +can add `#[Cache(smaxage: 0)]`. This will make the response `public`, but also requires a revalidation on every request as the response is *always* considered stale. [Learn more about Symonfy's HTTP caching]. [Learn more about Symonfy's HTTP caching]: http://symfony.com/doc/current/book/http_cache.html - - ## Installation Install via [composer](https://getcomposer.org/): @@ -104,12 +100,10 @@ public function registerBundles() } ``` - - ## Usage Choose a controller action you want to possibly replace with a 304 Not Modified response. Write one LastModifiedDeterminator for each -of the different underlying resources, implementing the ```Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator``` interface. +of the different underlying resources, implementing the `Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator` interface. ```php ["key1" => 1, "key2" => ["*"]]])] This would pass the array ['key1' => 1, 'key2' => ['*']] as an argument to MyLastModifiedDeterminator's constructor. If your LastModifiedDeterminator has more sophisticated dependencies, you can define the LastModifiedDeterminator as a service, e.g.: -```yaml +`yaml // services.yml services: app_caching_latest_posts: class: App\Caching\PostsLastModifiedDeterminator arguments: - @repository_post -``` +` and note the service name to the Annotation: - @ReplaceWithNotModifiedResponse({"@app_caching_latest_posts"}) + #[ReplaceWithNotModifiedResponse(["@app_caching_latest_posts"])] To combine multiple LastModifiedDeterminators, simply add all of them to the annotation: - @ReplaceWithNotModifiedResponse({ + #[ReplaceWithNotModifiedResponse([ "@app_caching_latest_posts", "\App\Caching\MySimpleLastModifiedDeterminator", - {"\App\Caching\MyLastModifiedDeterminator" = {"key1" = 1, "key2" = {"*"}}} - }) + ["\App\Caching\MyLastModifiedDeterminator" => ["key1" = 1, "key2" => ["*"]] + ])] The latest last modified date determines the last modified date of the response. @@ -212,4 +204,4 @@ This bundle was started at webfactory GmbH, Bonn. - - -Copyright 2018-2019 webfactory GmbH, Bonn. Code released under [the MIT license](LICENSE). +Copyright 2018-2024 webfactory GmbH, Bonn. Code released under [the MIT license](LICENSE). From 6c5563ac136f3d97c2039102e82751b78d992040 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 28 Mar 2024 18:26:08 +0100 Subject: [PATCH 4/6] Add UPGRADING notes --- UPGRADING.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 UPGRADING.md diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..e0eb4c3 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,6 @@ +# Upgrade notes for `WebfactoryHttpCacheBundle` + +## Version 1.4.0 + +* The `\Webfactory\HttpCacheBundle\NotModified\Annotation\ReplaceWithNotModifiedResponse` annotation has been deprecated. Use the + `\Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse` attribute for configuration instead. From 95bc37b6e051c6066be8e51afa47781dd6eeaa71 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 28 Mar 2024 18:27:42 +0100 Subject: [PATCH 5/6] Fix class references --- .../Annotation/ReplaceWithNotModifiedResponse.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php b/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php index 0ee5b4f..0cd1f3a 100644 --- a/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php +++ b/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php @@ -9,12 +9,14 @@ namespace Webfactory\HttpCacheBundle\NotModified\Annotation; +use Webfactory\HttpCacheBundle\NotModified\Attribute; + /** * @Annotation * * @deprecated, to be replaced by attribute-based configuration */ -final class ReplaceWithNotModifiedResponse extends \Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse +final class ReplaceWithNotModifiedResponse extends Attribute\ReplaceWithNotModifiedResponse { public function __construct(array $parameters) { @@ -22,8 +24,8 @@ public function __construct(array $parameters) 'webfactory/http-cache-bundle', '1.4.0', 'The %s annotation has been deprecated, use the %s attribute instead.', - NotModified\Annotation\ReplaceWithNotModifiedResponse::class, - NotModified\Attribute\ReplaceWithNotModifiedResponse::class + __CLASS__, + Attribute\ReplaceWithNotModifiedResponse::class ); parent::__construct($parameters['value']); From c69ef7f066a7b88577dbf04a7e06ecaebc207973 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 28 Mar 2024 19:05:56 +0100 Subject: [PATCH 6/6] Improve README details --- README.md | 101 +++++++++++++++++------------------------------------- 1 file changed, 31 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 40611b6..d98297d 100644 --- a/README.md +++ b/README.md @@ -2,46 +2,33 @@ `WebfactoryHttpCacheBundle` is a Symfony bundle that features a more powerful [HTTP cache validation via the last modified header] than the -`@Cache` annotation in the excellent [SensioFrameworkExtraBundle]. +`#[Cache]` attribute contained in the [symfony/http-kernel package]. [HTTP cache validation via the last modified header]: https://symfony.com/doc/current/http_cache/validation.html#validation-with-the-last-modified-header -[SensioFrameworkExtraBundle]: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html +[symfony/http-kernel package]: https://symfony.com/doc/current/http_cache.html#http-cache-expiration-intro -While the SensioFrameworkExtraBundle's `@Cache` annotation restricts -you to the request parameters, the `#ReplaceWithNotModifiedResponse` -attribute lets you write small LastModifiedDeterminators for each one -of the underlying ressources of the requested page. They can be reused -and combined freely and can even be defined as services. +The `#[ReplaceWithNotModifiedResponse]` attribute lets you write small +`LastModifiedDeterminators` for each one of the underlying resources +of the requested page. They can be reused and combined freely and can +even be defined as services. -Let's take the example from the `SensioFrameworkExtraBundle` docs (stripped -off the ETag part, which is not supported by the -`WebfactoryHttpCacheBundle`): +Consider this controller code: ```php -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; - -/** - * @Cache(lastModified="post.getUpdatedAt()") - */ -public function indexAction(Post $post) -{ - // your code - // won't be called in case of a 304 -} -``` - -This falls short if the rendered template e.g. contains information -about the x latest posts. That can be done with the -`#[ReplaceWithNotModifiedResponse]` attribute: +postRepository = $postRepository; - } + public function __construct( + private readonly BlogPostRepository $blogPostRepository, + ) { - public function getLastModified(Request $request) + public function getLastModified(Request $request): ?\DateTime { - $post = $this->postRepository->findLatest(); - return $post->getPublishingDate(); + $post = $this->blogPostRepository->findLatest(); + + return $post?->getPublishingDate(); } } ``` @@ -164,11 +125,11 @@ final class MyController The most simple form of adding a LastModifiedDeterminator is passing its fully qualfified class name: - #[ReplaceWithNotModifiedResponse(["\App\Caching\MySimpleLastModifiedDeterminator"])] + #[ReplaceWithNotModifiedResponse([\App\Caching\MySimpleLastModifiedDeterminator::class])] If your LastModifiedDeterminator needs simple constructor arguments, you can pass them in array form: - #[ReplaceWithNotModifiedResponse(["\App\Caching\MyLastModifiedDeterminator" => ["key1" => 1, "key2" => ["*"]]])] + #[ReplaceWithNotModifiedResponse([\App\Caching\MyLastModifiedDeterminator::class => ["key1" => 1, "key2" => ["*"]]])] This would pass the array ['key1' => 1, 'key2' => ['*']] as an argument to MyLastModifiedDeterminator's constructor. @@ -191,8 +152,8 @@ To combine multiple LastModifiedDeterminators, simply add all of them to the ann #[ReplaceWithNotModifiedResponse([ "@app_caching_latest_posts", - "\App\Caching\MySimpleLastModifiedDeterminator", - ["\App\Caching\MyLastModifiedDeterminator" => ["key1" = 1, "key2" => ["*"]] + \App\Caching\MySimpleLastModifiedDeterminator::class, + [\App\Caching\MyLastModifiedDeterminator::class => ["key1" = 1, "key2" => ["*"]] ])] The latest last modified date determines the last modified date of the response.