From 3efba6e3c937ad2e2384b759565392b195af5c57 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 28 Mar 2024 19:07:29 +0100 Subject: [PATCH] Add a PHP 8 attribute for configuration (#10) This adds a PHP 8 attribute for configuration, and deprecates using the old annotation class. --- README.md | 153 ++++++------------ UPGRADING.md | 6 + composer.json | 5 +- .../ReplaceWithNotModifiedResponse.php | 89 ++-------- .../ReplaceWithNotModifiedResponse.php | 98 +++++++++++ src/NotModified/EventListener.php | 20 ++- 6 files changed, 191 insertions(+), 180 deletions(-) create mode 100644 UPGRADING.md create mode 100644 src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php diff --git a/README.md b/README.md index c124d81..d98297d 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,73 @@ # 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]` 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``` -annotation 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. -Lets 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``` annotation: +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(); } } ``` -You can use the ```$request``` in the getLastModified e.g. to get route parameters, which is necessary e.g. if you have +You can use the `$request` in the getLastModified e.g. to get route parameters, which is necessary e.g. if you have some filters coded in the requested URL. If your LastModifiedDeterminator has dependencies you'd like to be injected, configure it as a service. -Then, simply add the ```ReplaceWithNotModifiedResponse``` annotation to the chosen controller method and parameterise it +Then, add the `#[ReplaceWithNotModifiedResponse]` attribute to the chosen controller method and parameterize it with your LastModifiedDeterminators: ```php @@ -155,13 +110,11 @@ with your LastModifiedDeterminators: namespace src\Controller; use Symfony\Component\HttpFoundation\Response; -use Webfactory\HttpCacheBundle\NotModified\Annotation\ReplaceWithNotModifiedResponse; +use Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse; final class MyController { - /** - * @ReplaceWithNotModifiedResponse({...}) - */ + #[ReplaceWithNotModifiedResponse([...])] public function indexAction() { // ... @@ -172,36 +125,36 @@ 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. 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\MySimpleLastModifiedDeterminator::class, + [\App\Caching\MyLastModifiedDeterminator::class => ["key1" = 1, "key2" => ["*"]] + ])] The latest last modified date determines the last modified date of the response. @@ -212,4 +165,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). 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. 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..0cd1f3a 100644 --- a/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php +++ b/src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php @@ -9,88 +9,25 @@ namespace Webfactory\HttpCacheBundle\NotModified\Annotation; -use DateTime; -use RuntimeException; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; -use Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator; +use Webfactory\HttpCacheBundle\NotModified\Attribute; /** - * 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 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.', + __CLASS__, + 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..c1fa370 --- /dev/null +++ b/src/NotModified/Attribute/ReplaceWithNotModifiedResponse.php @@ -0,0 +1,98 @@ + + * + * 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 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; }