Skip to content

Commit f613d44

Browse files
authored
Read attributes as provided by the event object, and pass on attributes to other event handlers (Case 177753) (#15)
This adapts webfactory/WebfactoryWfdMetaBundle#53: > This PR uses the new API introduced in symfony/symfony#46001 to read controller attributes through the `ControllerEvent`, and to make them available to other event handlers when replacing the controller. > > This is necessary when using the `Send304IfNotModified` attribute in combination with `\Symfony\Component\HttpKernel\Attribute\Cache`. Without this change, `\Symfony\Component\HttpKernel\EventListener\CacheAttributeListener` will not set `Cache` headers accordingly. The result is that you may get `304 Not Modified` responses on conditional requests with `If-Modified-Since`, but these are treated as `stale/cache` only in the HttpCache and have a `Cache-Control: must-revalidate, private` header. And adds some tests.
1 parent 8007a00 commit f613d44

File tree

3 files changed

+66
-26
lines changed

3 files changed

+66
-26
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"symfony/dependency-injection": "^5.0 | ^6.0 | ^7.0",
2525
"symfony/deprecation-contracts": "^2.0|^3.0",
2626
"symfony/http-foundation": "^5.0 | ^6.0 | ^7.0",
27-
"symfony/http-kernel": "^5.3 | ^6.0 | ^7.0"
27+
"symfony/http-kernel": "^6.4 | ^7.0"
2828
},
2929

3030
"require-dev": {

src/NotModified/EventListener.php

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
namespace Webfactory\HttpCacheBundle\NotModified;
1111

12-
use ReflectionMethod;
1312
use SplObjectStorage;
1413
use Symfony\Component\DependencyInjection\ContainerInterface;
1514
use Symfony\Component\HttpFoundation\Response;
@@ -46,14 +45,16 @@ public function __construct(
4645
*/
4746
public function onKernelController(ControllerEvent $event): void
4847
{
49-
$annotation = $this->findAnnotation($event->getController());
50-
if (!$annotation) {
48+
$attributes = $event->getAttributes(ReplaceWithNotModifiedResponse::class);
49+
50+
if (!$attributes) {
5151
return;
5252
}
5353

54+
$attribute = $attributes[0];
5455
$request = $event->getRequest();
55-
$annotation->setContainer($this->container);
56-
$lastModified = $annotation->determineLastModified($request);
56+
$attribute->setContainer($this->container);
57+
$lastModified = $attribute->determineLastModified($request);
5758
if (!$lastModified) {
5859
return;
5960
}
@@ -70,9 +71,12 @@ public function onKernelController(ControllerEvent $event): void
7071
$response->setLastModified($lastModified);
7172

7273
if ($response->isNotModified($request)) {
73-
$event->setController(function () use ($response) {
74-
return $response;
75-
});
74+
$event->setController(
75+
function () use ($response) {
76+
return $response;
77+
},
78+
$event->getAttributes()
79+
);
7680
}
7781
}
7882

@@ -89,21 +93,4 @@ public function onKernelResponse(ResponseEvent $event): void
8993
$response->setLastModified($this->lastModified[$request]);
9094
}
9195
}
92-
93-
/**
94-
* @param $controllerCallable callable PHP callback pointing to the method to reflect on.
95-
*/
96-
private function findAnnotation(callable $controllerCallable): ?ReplaceWithNotModifiedResponse
97-
{
98-
if (!is_array($controllerCallable)) {
99-
return null;
100-
}
101-
102-
[$class, $methodName] = $controllerCallable;
103-
$method = new ReflectionMethod($class, $methodName);
104-
105-
$attributes = $method->getAttributes(ReplaceWithNotModifiedResponse::class);
106-
107-
return $attributes ? $attributes[0]->newInstance() : null;
108-
}
10996
}

tests/NotModified/EventListenerTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use Closure;
1313
use DateTime;
14+
use Error;
1415
use PHPUnit\Framework\MockObject\MockObject;
1516
use PHPUnit\Framework\TestCase;
1617
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -159,6 +160,51 @@ public function eventListenerDifferentiatesBetweenMultipleRequests(): void
159160
self::assertNull($anotherResponse->getLastModified());
160161
}
161162

163+
/** @test */
164+
public function onKernelControllerSearchesEventInsteadOfControllerForAttribute(): void
165+
{
166+
// setup an event that should lead to a NotModified response
167+
$this->request->headers->set('If-Modified-Since', '-1 hour');
168+
$this->filterControllerEvent = new ControllerEvent(
169+
$this->kernel,
170+
[DummyController::class, 'oneDayAgoModifiedLastModifiedAction'],
171+
$this->request,
172+
HttpKernelInterface::MAIN_REQUEST
173+
);
174+
175+
// now simulate another EventListener has replaced the response of the controller inside the event, but has
176+
// saved the original attributes in the event.
177+
$this->filterControllerEvent->setController(
178+
function () {
179+
return new Response();
180+
},
181+
[ReplaceWithNotModifiedResponse::class => [new ReplaceWithNotModifiedResponse([OneDayAgoModifiedLastModifiedDeterminator::class])]]
182+
);
183+
184+
$this->eventListener->onKernelController($this->filterControllerEvent);
185+
186+
self::assertNotModifiedResponse();
187+
}
188+
189+
/** @test */
190+
public function onKernelControllerSavesOriginalControllerAttributesWhenReplacingTheController(): void
191+
{
192+
$this->request->headers->set('If-Modified-Since', '-1 hour');
193+
194+
$this->exerciseOnKernelController([DummyController::class, 'oneDayAgoModifiedLastModifiedAction']);
195+
196+
self::assertNotEmpty($this->filterControllerEvent->getAttributes());
197+
}
198+
199+
/** @test */
200+
public function onKernelControllerThrowsExceptionIfAttributeIsFoundMoreThanOnce(): void
201+
{
202+
self::expectException(Error::class);
203+
self::expectExceptionMessageMatches('/ReplaceWithNotModifiedResponse/');
204+
205+
$this->exerciseOnKernelController([DummyController::class, 'actionWithMoreThanOneAttribute']);
206+
}
207+
162208
private function exerciseOnKernelController(array $callable): void
163209
{
164210
$this->callable = $callable;
@@ -213,6 +259,13 @@ public static function fixedDateAgoModifiedLastModifiedDeterminatorAction(): Res
213259
{
214260
return new Response();
215261
}
262+
263+
#[ReplaceWithNotModifiedResponse([AbstainingLastModifiedDeterminator::class])]
264+
#[ReplaceWithNotModifiedResponse([OneDayAgoModifiedLastModifiedDeterminator::class])]
265+
public static function actionWithMoreThanOneAttribute(): Response
266+
{
267+
return new Response();
268+
}
216269
}
217270

218271
final class AbstainingLastModifiedDeterminator implements LastModifiedDeterminator

0 commit comments

Comments
 (0)