Skip to content

Commit 3efba6e

Browse files
authored
Add a PHP 8 attribute for configuration (#10)
This adds a PHP 8 attribute for configuration, and deprecates using the old annotation class.
1 parent 2d153e5 commit 3efba6e

File tree

6 files changed

+191
-180
lines changed

6 files changed

+191
-180
lines changed

README.md

Lines changed: 53 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,73 @@
11
# WebfactoryHttpCacheBundle
22

3-
WebfactoryHttpCacheBundle is a Symfony bundle that features a more
3+
`WebfactoryHttpCacheBundle` is a Symfony bundle that features a more
44
powerful [HTTP cache validation via the last modified header] than the
5-
```@Cache``` annotation in the excellent [SensioFrameworkExtraBundle].
5+
`#[Cache]` attribute contained in the [symfony/http-kernel package].
66

77
[HTTP cache validation via the last modified header]: https://symfony.com/doc/current/http_cache/validation.html#validation-with-the-last-modified-header
8-
[SensioFrameworkExtraBundle]: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html
8+
[symfony/http-kernel package]: https://symfony.com/doc/current/http_cache.html#http-cache-expiration-intro
99

10-
While the SensioFrameworkExtraBundle's ```@Cache``` annotation restricts
11-
you to the request parameters, the ```@ReplaceWithNotModifiedResponse```
12-
annotation lets you write small LastModifiedDeterminators for each one
13-
of the underlying ressources of the requested page, They can be reused
14-
and combined freely and can even be defined as services.
10+
The `#[ReplaceWithNotModifiedResponse]` attribute lets you write small
11+
`LastModifiedDeterminators` for each one of the underlying resources
12+
of the requested page. They can be reused and combined freely and can
13+
even be defined as services.
1514

16-
Lets take the example from the SensioFrameworkExtraBundle docs (stripped
17-
off the ETag part, which is not supported by the
18-
WebfactoryHttpCacheBundle):
15+
Consider this controller code:
1916

2017
```php
21-
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
22-
23-
/**
24-
* @Cache(lastModified="post.getUpdatedAt()")
25-
*/
26-
public function indexAction(Post $post)
27-
{
28-
// your code
29-
// won't be called in case of a 304
30-
}
31-
```
32-
33-
This falls short if the rendered template e.g. contains information
34-
about the x latest posts. That can be done with the
35-
```@ReplaceWithNotModifiedResponse``` annotation:
18+
<?php
3619

37-
```php
38-
use Webfactory\HttpCacheBundle\NotModified\Annotation\ReplaceWithNotModifiedResponse;
20+
// ...
21+
use Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse;
3922

40-
/**
41-
* @ReplaceWithNotModifiedResponse({"@app_caching_post", "@app_caching_latest_posts"})
42-
*/
43-
public function indexAction(Post $post)
44-
{
45-
// your code
46-
// won't be called in case of a 304
23+
class MyController {
24+
// Routing etc. configuration skipped for brevity
25+
26+
#[ReplaceWithNotModifiedResponse(["@app_caching_post", "@app_caching_latest_posts"])]
27+
public function indexAction(Post $post): Response
28+
{
29+
// your code
30+
// won't be called in case of a 304
31+
}
4732
}
4833
```
4934

5035
When Symfony's routing has chosen this controller action, all of the
51-
LastModifiedDeterminators are called to return their respective last
36+
`LastModifiedDeterminator`s are called to return their respective last
5237
modified date.
5338

5439
In this case, both LastModifiedDeterminators are configured as services:
55-
```@app_caching_post``` and ```@app_caching_latest_posts```. The first
56-
one returns the update date of the requests $post, the second one may
40+
`@app_caching_post` and `@app_caching_latest_posts`. The first
41+
one returns the update date of the requests `$post`, the second one may
5742
use the PostRepository injected from the DI container to return the last
5843
update date of the x latest posts.
5944

60-
Then, ReplaceWithNotModifiedResponse combines all of the
61-
LastModifiedDeterminators dates to determine to last modified date of
45+
`#[ReplaceWithNotModifiedResponse]` combines all of the
46+
`LastModifiedDeterminators` dates to determine to last modified date of
6247
the overall page. Finally, if the request contains an appropriate
63-
```if-not-modified-since``` header, the execution of the controller
64-
action will be skipped and an empty response with a 304 Not Modified
65-
status code will be sent. If your LastModifiedDeterminators are fast,
48+
`if-not-modified-since` header, the execution of the controller
49+
action will be skipped and an empty response with a "304 Not Modified"
50+
status code will be sent. If your `LastModifiedDeterminators` are fast,
6651
this can improve your performance greatly.
6752

68-
What we like about the LastModifiedDeterminators is that they encourage
53+
What we like about the `LastModifiedDeterminators` is that they encourage
6954
to separate the concerns nicely and encapsulate the tasks into small
7055
units that are easy to understand, reusable and unit test.
7156

72-
*Note:* `@ReplaceWithNotModifiedResponse` does not alter or add
57+
*Note:* `#[ReplaceWithNotModifiedResponse]` does not alter or add
7358
`Cache-Control` header settings. So, by default your response will
7459
remain `private` and end up in browser caches only. If you want it to be
7560
kept in surrogate caches (like Varnish or the Symfony Http Cache), you
76-
can add `@Cache(smaxage="0")`. This will make the response `public`, but
61+
can add `#[Cache(smaxage: 0)]`. This will make the response `public`, but
7762
also requires a revalidation on every request as the response is
7863
*always* considered stale. [Learn more about Symonfy's HTTP caching].
7964

8065
[Learn more about Symonfy's HTTP caching]: http://symfony.com/doc/current/book/http_cache.html
8166

82-
83-
84-
## Installation
85-
86-
Install via [composer](https://getcomposer.org/):
87-
88-
composer require webfactory/http-cache-bundle
89-
90-
Register the bundle in your application:
91-
92-
```php
93-
<?php
94-
// app/AppKernel.php
95-
96-
public function registerBundles()
97-
{
98-
$bundles = array(
99-
// ...
100-
new Webfactory\HttpCacheBundle\WebfactoryHttpCacheBundle(),
101-
// ...
102-
);
103-
// ...
104-
}
105-
```
106-
107-
108-
10967
## Usage
11068

11169
Choose a controller action you want to possibly replace with a 304 Not Modified response. Write one LastModifiedDeterminator for each
112-
of the different underlying resources, implementing the ```Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator``` interface.
70+
of the different underlying resources, implementing the `Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator` interface.
11371

11472
```php
11573
<?php
@@ -125,28 +83,25 @@ use Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator;
12583
*/
12684
final class PostsLastModifiedDeterminator implements LastModifiedDeterminator
12785
{
128-
/** @var EntityRepository */
129-
private $postRepository;
130-
131-
public function __construct(PostRepository $postRepository)
132-
{
133-
$this->postRepository = $postRepository;
134-
}
86+
public function __construct(
87+
private readonly BlogPostRepository $blogPostRepository,
88+
) {
13589

136-
public function getLastModified(Request $request)
90+
public function getLastModified(Request $request): ?\DateTime
13791
{
138-
$post = $this->postRepository->findLatest();
139-
return $post->getPublishingDate();
92+
$post = $this->blogPostRepository->findLatest();
93+
94+
return $post?->getPublishingDate();
14095
}
14196
}
14297
```
14398

144-
You can use the ```$request``` in the getLastModified e.g. to get route parameters, which is necessary e.g. if you have
99+
You can use the `$request` in the getLastModified e.g. to get route parameters, which is necessary e.g. if you have
145100
some filters coded in the requested URL.
146101

147102
If your LastModifiedDeterminator has dependencies you'd like to be injected, configure it as a service.
148103

149-
Then, simply add the ```ReplaceWithNotModifiedResponse``` annotation to the chosen controller method and parameterise it
104+
Then, add the `#[ReplaceWithNotModifiedResponse]` attribute to the chosen controller method and parameterize it
150105
with your LastModifiedDeterminators:
151106

152107
```php
@@ -155,13 +110,11 @@ with your LastModifiedDeterminators:
155110
namespace src\Controller;
156111

157112
use Symfony\Component\HttpFoundation\Response;
158-
use Webfactory\HttpCacheBundle\NotModified\Annotation\ReplaceWithNotModifiedResponse;
113+
use Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse;
159114

160115
final class MyController
161116
{
162-
/**
163-
* @ReplaceWithNotModifiedResponse({...})
164-
*/
117+
#[ReplaceWithNotModifiedResponse([...])]
165118
public function indexAction()
166119
{
167120
// ...
@@ -172,36 +125,36 @@ final class MyController
172125

173126
The most simple form of adding a LastModifiedDeterminator is passing its fully qualfified class name:
174127

175-
@ReplaceWithNotModifiedResponse({"\App\Caching\MySimpleLastModifiedDeterminator"})
128+
#[ReplaceWithNotModifiedResponse([\App\Caching\MySimpleLastModifiedDeterminator::class])]
176129

177130
If your LastModifiedDeterminator needs simple constructor arguments, you can pass them in array form:
178131

179-
@ReplaceWithNotModifiedResponse({ {"\App\Caching\MyLastModifiedDeterminator" = {"key1" = 1, "key2" = {"*"} } } })
132+
#[ReplaceWithNotModifiedResponse([\App\Caching\MyLastModifiedDeterminator::class => ["key1" => 1, "key2" => ["*"]]])]
180133

181134
This would pass the array ['key1' => 1, 'key2' => ['*']] as an argument to MyLastModifiedDeterminator's constructor.
182135

183136
If your LastModifiedDeterminator has more sophisticated dependencies, you can define the LastModifiedDeterminator as a service, e.g.:
184137

185-
```yaml
138+
`yaml
186139
// services.yml
187140
services:
188141
app_caching_latest_posts:
189142
class: App\Caching\PostsLastModifiedDeterminator
190143
arguments:
191144
- @repository_post
192-
```
145+
`
193146

194147
and note the service name to the Annotation:
195148

196-
@ReplaceWithNotModifiedResponse({"app_caching_latest_posts"})
149+
#[ReplaceWithNotModifiedResponse(["@app_caching_latest_posts"])]
197150

198151
To combine multiple LastModifiedDeterminators, simply add all of them to the annotation:
199152

200-
@ReplaceWithNotModifiedResponse({
153+
#[ReplaceWithNotModifiedResponse([
201154
"@app_caching_latest_posts",
202-
"\App\Caching\MySimpleLastModifiedDeterminator",
203-
{"\App\Caching\MyLastModifiedDeterminator" = {"key1" = 1, "key2" = {"*"}}}
204-
})
155+
\App\Caching\MySimpleLastModifiedDeterminator::class,
156+
[\App\Caching\MyLastModifiedDeterminator::class => ["key1" = 1, "key2" => ["*"]]
157+
])]
205158

206159
The latest last modified date determines the last modified date of the response.
207160

@@ -212,4 +165,4 @@ This bundle was started at webfactory GmbH, Bonn.
212165
- <https://www.webfactory.de>
213166
- <https://twitter.com/webfactory>
214167

215-
Copyright 2018-2019 webfactory GmbH, Bonn. Code released under [the MIT license](LICENSE).
168+
Copyright 2018-2024 webfactory GmbH, Bonn. Code released under [the MIT license](LICENSE).

UPGRADING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Upgrade notes for `WebfactoryHttpCacheBundle`
2+
3+
## Version 1.4.0
4+
5+
* The `\Webfactory\HttpCacheBundle\NotModified\Annotation\ReplaceWithNotModifiedResponse` annotation has been deprecated. Use the
6+
`\Webfactory\HttpCacheBundle\NotModified\Attribute\ReplaceWithNotModifiedResponse` attribute for configuration instead.

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@
2020

2121
"require": {
2222
"php": "^7.1|8.0.*|8.1.*",
23+
"doctrine/annotations": "^1.0",
2324
"symfony/config": "^4.4 | ^5.0 | ^6.0",
2425
"symfony/dependency-injection": "^4.4 | ^5.0 | ^6.0",
26+
"symfony/deprecation-contracts": "^2.0|^3.0",
2527
"symfony/http-foundation": "^4.4 | ^5.0 | ^6.0",
26-
"symfony/http-kernel": "^4.4 | ^5.0 | ^6.0",
27-
"doctrine/annotations": "^1.0"
28+
"symfony/http-kernel": "^4.4 | ^5.0 | ^6.0"
2829
},
2930

3031
"require-dev": {

src/NotModified/Annotation/ReplaceWithNotModifiedResponse.php

Lines changed: 13 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,88 +9,25 @@
99

1010
namespace Webfactory\HttpCacheBundle\NotModified\Annotation;
1111

12-
use DateTime;
13-
use RuntimeException;
14-
use Symfony\Component\DependencyInjection\ContainerInterface;
15-
use Symfony\Component\HttpFoundation\Request;
16-
use Webfactory\HttpCacheBundle\NotModified\LastModifiedDeterminator;
12+
use Webfactory\HttpCacheBundle\NotModified\Attribute;
1713

1814
/**
19-
* This Annotation determines the latest last modified date over all of its LastModifiedDeterminators. This date is used
20-
* by the \Webfactory\HttpCacheBundle\NotModified\EventListener to possibly replace the execution of a controller with
21-
* sending a Not Modified HTTP response.
22-
*
2315
* @Annotation
16+
*
17+
* @deprecated, to be replaced by attribute-based configuration
2418
*/
25-
final class ReplaceWithNotModifiedResponse
19+
final class ReplaceWithNotModifiedResponse extends Attribute\ReplaceWithNotModifiedResponse
2620
{
27-
/** @var array */
28-
private $parameters;
29-
30-
/** @var LastModifiedDeterminator[] */
31-
private $lastModifiedDeterminators;
32-
33-
/** @var ContainerInterface */
34-
private $container;
35-
36-
/** @var DateTime|null */
37-
private $lastModified;
38-
3921
public function __construct(array $parameters)
4022
{
41-
$this->parameters = $parameters;
42-
}
43-
44-
/**
45-
* @return DateTime|null
46-
*/
47-
public function determineLastModified(Request $request)
48-
{
49-
$this->initialiseLastModifiedDeterminators();
50-
51-
foreach ($this->lastModifiedDeterminators as $lastModifiedDeterminator) {
52-
$lastModifiedOfCurrentDeterminator = $lastModifiedDeterminator->getLastModified($request);
53-
if (null === $this->lastModified || $this->lastModified < $lastModifiedOfCurrentDeterminator) {
54-
$this->lastModified = $lastModifiedOfCurrentDeterminator;
55-
}
56-
}
57-
58-
return $this->lastModified;
59-
}
60-
61-
public function setContainer(ContainerInterface $container)
62-
{
63-
$this->container = $container;
64-
}
65-
66-
private function initialiseLastModifiedDeterminators()
67-
{
68-
if (0 === count($this->parameters['value'])) {
69-
throw new RuntimeException('The annotation '.get_class($this).' has to be parametrised with LastModifiedDeterminators.');
70-
}
71-
72-
foreach ($this->parameters['value'] as $lastModifiedDeterminatorDescription) {
73-
$lastModifiedDeterminator = null;
74-
75-
if (is_string($lastModifiedDeterminatorDescription)) {
76-
if ('@' === $lastModifiedDeterminatorDescription[0]) {
77-
$lastModifiedDeterminator = $this->container->get(substr($lastModifiedDeterminatorDescription, 1));
78-
} else {
79-
$lastModifiedDeterminator = new $lastModifiedDeterminatorDescription();
80-
}
81-
}
82-
83-
if (is_array($lastModifiedDeterminatorDescription)) {
84-
$lastModifiedDeterminatorClass = key($lastModifiedDeterminatorDescription);
85-
$lastModifiedDeterminatorParameter = current($lastModifiedDeterminatorDescription);
86-
$lastModifiedDeterminator = new $lastModifiedDeterminatorClass($lastModifiedDeterminatorParameter);
87-
}
88-
89-
if (!($lastModifiedDeterminator instanceof LastModifiedDeterminator)) {
90-
throw new RuntimeException('The class "'.get_class($lastModifiedDeterminator).'" does not implement '.LastModifiedDeterminator::class.'.');
91-
}
92-
93-
$this->lastModifiedDeterminators[] = $lastModifiedDeterminator;
94-
}
23+
trigger_deprecation(
24+
'webfactory/http-cache-bundle',
25+
'1.4.0',
26+
'The %s annotation has been deprecated, use the %s attribute instead.',
27+
__CLASS__,
28+
Attribute\ReplaceWithNotModifiedResponse::class
29+
);
30+
31+
parent::__construct($parameters['value']);
9532
}
9633
}

0 commit comments

Comments
 (0)