Skip to content

Commit b18edb9

Browse files
committed
Initialise bundle
0 parents  commit b18edb9

12 files changed

Lines changed: 1008 additions & 0 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vendor/
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Webfactory\HttpCacheBundle\DependencyInjection;
4+
5+
use Symfony\Component\Config\FileLocator;
6+
use Symfony\Component\DependencyInjection\ContainerBuilder;
7+
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
8+
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
9+
10+
/**
11+
* Symfony Bundle Extension class.
12+
*/
13+
class WebfactoryHttpCacheExtension extends Extension
14+
{
15+
public function load(array $configs, ContainerBuilder $container)
16+
{
17+
$locator = new FileLocator(__DIR__ . '/../NotModified');
18+
$yamlLoader = new XmlFileLoader($container, $locator);
19+
$yamlLoader->load('services.xml');
20+
}
21+
}

LICENSE

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2013 webfactory GmbH, Bonn (info@webfactory.de)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to
7+
deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* (c) webfactory GmbH <info@webfactory.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Webfactory\HttpCacheBundle\NotModified\Annotation;
11+
12+
use Symfony\Component\DependencyInjection\ContainerInterface;
13+
use Symfony\Component\HttpFoundation\Request;
14+
use Webfactory\HttpCacheBundle\NotModified\VoterInterface;
15+
16+
/**
17+
* This Annotation determines the last modified date over all of it's parameterising voters. This date is used by the
18+
* \Webfactory\HttpCacheBundle\NotModified\EventListener to possibly replace the execution of a controller with
19+
* sending a Not Modified HTTP response.
20+
*
21+
* @Annotation
22+
*/
23+
class ReplaceWithNotModifiedResponse
24+
{
25+
/** @var array */
26+
private $parameters;
27+
28+
/** @var VoterInterface[] */
29+
private $voters;
30+
31+
/** @var ContainerInterface */
32+
private $container;
33+
34+
/** @var \DateTime|null */
35+
private $lastModified;
36+
37+
/**
38+
* @param array $parameters
39+
*/
40+
public function __construct(array $parameters)
41+
{
42+
$this->parameters = $parameters;
43+
}
44+
45+
/**
46+
* @param Request $request
47+
* @return \DateTime|null
48+
*/
49+
public function determineLastModified(Request $request)
50+
{
51+
$this->initialiseVoters();
52+
53+
foreach ($this->voters as $voter) {
54+
$lastModifiedOfCurrentVoter = $voter->getLastModified($request);
55+
if ($this->lastModified === null || $this->lastModified < $lastModifiedOfCurrentVoter) {
56+
$this->lastModified = $lastModifiedOfCurrentVoter;
57+
}
58+
}
59+
60+
return $this->lastModified;
61+
}
62+
63+
/**
64+
* @param ContainerInterface $container
65+
*/
66+
public function setContainer(ContainerInterface $container)
67+
{
68+
$this->container = $container;
69+
}
70+
71+
private function initialiseVoters()
72+
{
73+
if (!array_key_exists('voters', $this->parameters) || count($this->parameters['voters']) === 0) {
74+
throw new \RuntimeException('The annotation ' . get_class($this) . ' has to be parametrised with voters.');
75+
}
76+
77+
$runningIndex = 1;
78+
foreach ($this->parameters['voters'] as $voterDescription) {
79+
$voter = null;
80+
81+
if (is_string($voterDescription)) {
82+
if ($voterDescription[0] === '@') {
83+
$voter = $this->container->get($voterDescription);
84+
} else {
85+
$voter = new $voterDescription;
86+
}
87+
}
88+
89+
if (is_array($voterDescription)) {
90+
if (count($voterDescription) !== 1) {
91+
throw new \RuntimeException('Voter #' . $runningIndex . ' (starting with 1) is misconfigured.');
92+
}
93+
94+
$voterClass = key($voterDescription);
95+
$voterParameter = current($voterDescription);
96+
$voter = new $voterClass($voterParameter);
97+
}
98+
99+
if (!($voter instanceof VoterInterface)) {
100+
throw new \RuntimeException(
101+
'The voter class "' . get_class($voter) . '" does not implement ' . VoterInterface::class . '.'
102+
);
103+
}
104+
105+
$this->voters[] = $voter;
106+
}
107+
}
108+
}

NotModified/EventListener.php

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
/*
4+
* (c) webfactory GmbH <info@webfactory.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Webfactory\HttpCacheBundle\NotModified;
11+
12+
use Doctrine\Common\Annotations\Reader;
13+
use Symfony\Component\DependencyInjection\ContainerInterface;
14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
16+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
17+
use Webfactory\HttpCacheBundle\NotModified\Annotation\ReplaceWithNotModifiedResponse;
18+
19+
/**
20+
* Symfony EventListener for adding a "last modified" header to the response on the one hand. On the other hand, it
21+
* replaces the execution of a controller action with a Not Modified HTTP response, if no newer "last modified" date is
22+
* determined than the one in the header of a subsequent request.
23+
*/
24+
final class EventListener
25+
{
26+
/** @var Reader */
27+
private $reader;
28+
29+
/** @var ContainerInterface */
30+
private $container;
31+
32+
/**
33+
* Maps (master and sub) requests to their corresponding last modified date. This date is determined by the
34+
* ReplaceWithNotModifiedResponse annotation of the corresponding controller's action.
35+
*
36+
* @var \SplObjectStorage
37+
*/
38+
private $lastModified;
39+
40+
/**
41+
* @param Reader $reader
42+
* @param ContainerInterface $container
43+
*/
44+
public function __construct(Reader $reader, ContainerInterface $container)
45+
{
46+
$this->reader = $reader;
47+
$this->container = $container;
48+
$this->lastModified = new \SplObjectStorage();
49+
}
50+
51+
/**
52+
* When the controller action for a request is determined, check it for a ReplaceWithNotModifiedResponse annotation.
53+
* If it determines that the underlying ressources for the response were not modified after the "If-Modified-Since"
54+
* header in the request, replace the determines controller action with an minimal action that just returns an
55+
* "empty" response with a 304 Not Modified HTTP status code.
56+
*
57+
* @param FilterControllerEvent $event
58+
*/
59+
public function onKernelController(FilterControllerEvent $event)
60+
{
61+
$annotation = $this->findAnnotation($event->getController());
62+
if (!$annotation) {
63+
return;
64+
}
65+
66+
$request = $event->getRequest();
67+
$annotation->setContainer($this->container);
68+
$lastModified = $annotation->determineLastModified($request);
69+
if (!$lastModified) {
70+
return;
71+
}
72+
73+
$this->lastModified[$request] = $lastModified;
74+
75+
$response = new Response();
76+
$response->setLastModified($lastModified);
77+
78+
if ($response->isNotModified($request)) {
79+
$event->setController(function () use ($response) {
80+
return $response;
81+
});
82+
}
83+
}
84+
85+
/**
86+
* If a last modified date was determined for the current (master or sub) request, set it to the response so the
87+
* client can use it for the "If-Modified-Since" header in subsequent requests.
88+
*
89+
* @param FilterResponseEvent $event
90+
*/
91+
public function onKernelResponse(FilterResponseEvent $event)
92+
{
93+
$request = $event->getRequest();
94+
$response = $event->getResponse();
95+
96+
if (isset($this->lastModified[$request])) {
97+
$response->setLastModified($this->lastModified[$request]);
98+
}
99+
}
100+
101+
/**
102+
* @param $controllerCallable callable PHP callback pointing to the method to reflect on.
103+
* @return ReplaceWithNotModifiedResponse|null The annotation, if found. Null otherwise.
104+
*/
105+
private function findAnnotation(callable $controllerCallable)
106+
{
107+
if (!is_array($controllerCallable)) {
108+
return null;
109+
}
110+
111+
list($class, $methodName) = $controllerCallable;
112+
$method = new \ReflectionMethod($class, $methodName);
113+
114+
/** @var ReplaceWithNotModifiedResponse|null $annotation */
115+
$annotation = $this->reader->getMethodAnnotation($method, ReplaceWithNotModifiedResponse::class);
116+
return $annotation;
117+
}
118+
}

NotModified/VoterInterface.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* (c) webfactory GmbH <info@webfactory.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Webfactory\HttpCacheBundle\NotModified;
11+
12+
use Symfony\Component\HttpFoundation\Request;
13+
14+
/**
15+
* The ReplaceWithNotModifiedResponse method annotation is parameterised with Voters. Each Voter should determine the
16+
* last modified date of one of the various underlying ressources for a response. E.g. if your controller's indexAction
17+
* build a response containing News and Users, you can write a NewsVoter determining the date of the last published
18+
* News and a UserVoter determining the creation date of the last registered User.
19+
*/
20+
interface VoterInterface
21+
{
22+
/**
23+
* @param Request $request
24+
* @return \DateTime|null
25+
*/
26+
public function getLastModified(Request $request);
27+
}

NotModified/services.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns="http://symfony.com/schema/dic/services"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
<services>
7+
<service class="Webfactory\HttpCacheBundle\NotModified\EventListener" public="true">
8+
<argument type="service" id="annotation_reader" />
9+
<argument type="service" id="service_container" />
10+
<tag name="kernel.event_listener" event="kernel.controller" />
11+
<tag name="kernel.event_listener" event="kernel.response" />
12+
</service>
13+
</services>
14+
</container>

0 commit comments

Comments
 (0)