Skip to content

Commit 9f6748b

Browse files
author
alex-pex
committed
Initial commit
0 parents  commit 9f6748b

7 files changed

Lines changed: 341 additions & 0 deletions

File tree

Cache/KeyProvider.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace StadLine\ExecutionCacheBundle\Cache;
4+
5+
use Symfony\Component\HttpFoundation\HeaderBag;
6+
use Symfony\Component\HttpFoundation\Request;
7+
8+
/**
9+
* Default cache_key provider implementation
10+
*/
11+
class KeyProvider
12+
{
13+
/**
14+
* Hash a request URL into a string that returns cache metadata
15+
*
16+
* @param Request $request
17+
*
18+
* @return string
19+
*/
20+
public function getCacheKey(Request $request)
21+
{
22+
$reducedRequest = $request->duplicate();
23+
$this->persistHeaders($reducedRequest->headers);
24+
25+
return hash('sha256', (string) $reducedRequest);
26+
}
27+
28+
/**
29+
* Creates an array of cacheable and normalized message headers
30+
*
31+
* @param HeaderBag $headers
32+
*
33+
* @return array
34+
*/
35+
private function persistHeaders(HeaderBag $headers)
36+
{
37+
// Headers are excluded from the caching (see RFC 2616:13.5.1)
38+
static $noCache = array(
39+
'age',
40+
'connection',
41+
'keep-alive',
42+
'proxy-authenticate',
43+
'proxy-authorization',
44+
'te',
45+
'trailers',
46+
'transfer-encoding',
47+
'upgrade',
48+
'set-cookie',
49+
'set-cookie2'
50+
);
51+
52+
foreach ($headers as $key => $value) {
53+
if (in_array($key, $noCache) || strpos($key, 'x-') === 0) {
54+
$headers->remove($key);
55+
}
56+
}
57+
58+
// Postman client sends a dynamic token to bypass a Chrome bug
59+
$headers->remove('postman-token');
60+
61+
return $headers;
62+
}
63+
}

Cache/Storage.php

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace StadLine\ExecutionCacheBundle\Cache;
4+
5+
use Cache\Adapter\Common\CacheItem;
6+
use Psr\Cache\CacheItemPoolInterface;
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\HttpFoundation\Response;
9+
10+
/**
11+
* Default cache storage implementation
12+
*/
13+
class Storage
14+
{
15+
/** @var string */
16+
protected $keyPrefix;
17+
18+
/** @var CacheItemPoolInterface Cache used to store cache data */
19+
protected $cache;
20+
21+
/** @var int Default cache TTL */
22+
protected $defaultTtl;
23+
24+
/** @var KeyProvider Alternative CacheKey provider */
25+
protected $keyProvider;
26+
27+
/**
28+
* @param CacheItemPoolInterface $cache Cache used to store cache data
29+
* @param string $keyPrefix Provide an optional key prefix to prefix on all cache keys
30+
* @param int $defaultTtl Default cache TTL
31+
*/
32+
public function __construct(CacheItemPoolInterface $cache, $keyPrefix = '', $defaultTtl = 3600)
33+
{
34+
$this->cache = $cache;
35+
$this->keyPrefix = $keyPrefix;
36+
$this->defaultTtl = $defaultTtl;
37+
}
38+
39+
/**
40+
* @param KeyProvider $keyProvider
41+
*/
42+
public function setKeyProvider(KeyProvider $keyProvider)
43+
{
44+
$this->keyProvider = $keyProvider;
45+
}
46+
47+
/**
48+
* @param Request $request
49+
* @param Response $response
50+
*/
51+
public function cache(Request $request, Response $response)
52+
{
53+
if (!in_array($request->getMethod(), array('GET', 'HEAD'))) {
54+
return;
55+
}
56+
57+
if ($request->isNoCache()) {
58+
return;
59+
}
60+
61+
if ($response->headers->has('X-ServerCache-Key')) {
62+
return;
63+
}
64+
65+
$key = $this->getCacheKey($request);
66+
$expirationDate = date_create('NOW + ' . $this->defaultTtl . ' seconds');
67+
68+
$item = new CacheItem($key, true, $response, $expirationDate);
69+
$this->cache->save($item);
70+
}
71+
72+
/**
73+
* @param Request $request
74+
*/
75+
public function delete(Request $request)
76+
{
77+
$key = $this->getCacheKey($request);
78+
79+
$this->cache->deleteItem($key);
80+
}
81+
82+
/**
83+
* @param type $url
84+
* @throws \BadMethodCallException
85+
*/
86+
public function purge($url)
87+
{
88+
throw new \BadMethodCallException('Not implemented');
89+
}
90+
91+
/**
92+
* @param Request $request
93+
* @return Response
94+
*/
95+
public function fetch(Request $request)
96+
{
97+
$key = $this->getCacheKey($request);
98+
99+
$item = $this->cache->getItem($key);
100+
$response = $item->get();
101+
102+
if ($response instanceof Response) {
103+
$expirationDate = $item->getExpirationDate();
104+
$ttl = $expirationDate->getTimestamp() - date_create('NOW')->getTimestamp();
105+
106+
$response->headers->set('X-ServerCache-Key', $key);
107+
$response->headers->set('X-ServerCache-Expires', $ttl);
108+
}
109+
110+
return $response;
111+
}
112+
113+
/**
114+
* Hash a request URL into a string that returns cache metadata
115+
*
116+
* @param Request $request
117+
* @return string
118+
*/
119+
protected function getCacheKey(Request $request)
120+
{
121+
if ($this->keyProvider) {
122+
$key = $this->keyProvider->getCacheKey($request);
123+
} else {
124+
$key = md5($request->getMethod() . ' ' . $request->getUri());
125+
}
126+
127+
return $this->keyPrefix . $key;
128+
}
129+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace StadLine\ExecutionCacheBundle\DependencyInjection;
4+
5+
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
6+
use Symfony\Component\Config\Definition\ConfigurationInterface;
7+
8+
/**
9+
* This is the class that validates and merges configuration from your app/config files
10+
*
11+
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
12+
*/
13+
class Configuration implements ConfigurationInterface
14+
{
15+
/**
16+
* {@inheritdoc}
17+
*/
18+
public function getConfigTreeBuilder()
19+
{
20+
$treeBuilder = new TreeBuilder();
21+
$rootNode = $treeBuilder->root('stadline_execution_cache');
22+
23+
// Here you should define the parameters that are allowed to
24+
// configure your bundle. See the documentation linked above for
25+
// more information on that topic.
26+
27+
return $treeBuilder;
28+
}
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace StadLine\ExecutionCacheBundle\DependencyInjection;
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
use Symfony\Component\Config\FileLocator;
7+
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
8+
use Symfony\Component\DependencyInjection\Loader;
9+
10+
/**
11+
* This is the class that loads and manages your bundle configuration
12+
*
13+
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
14+
*/
15+
class StadLineExecutionCacheExtension extends Extension
16+
{
17+
/**
18+
* {@inheritdoc}
19+
*/
20+
public function load(array $configs, ContainerBuilder $container)
21+
{
22+
$configuration = new Configuration();
23+
$config = $this->processConfiguration($configuration, $configs);
24+
25+
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
26+
$loader->load('services.yml');
27+
}
28+
}

Listener/KernelListener.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace StadLine\ExecutionCacheBundle\Listener;
4+
5+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
6+
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
7+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
8+
use Symfony\Component\HttpKernel\KernelEvents;
9+
use StadLine\ExecutionCacheBundle\Cache\Storage;
10+
11+
/**
12+
* KernelListener handles execution cache.
13+
* Its purpose is to prevent controller execution when response is cached.
14+
*/
15+
class KernelListener implements EventSubscriberInterface
16+
{
17+
/**
18+
* @var Storage
19+
*/
20+
private $storage;
21+
22+
/**
23+
* Constructor.
24+
*
25+
* @param Storage $storage
26+
*/
27+
public function __construct(Storage $storage)
28+
{
29+
$this->storage = $storage;
30+
}
31+
32+
/**
33+
* Handles cache lookup.
34+
*/
35+
public function onKernelController(FilterControllerEvent $event)
36+
{
37+
$request = $event->getRequest();
38+
$response = $this->storage->fetch($request);
39+
40+
if ($response) {
41+
$event->setController(function () use ($response) {
42+
return $response;
43+
});
44+
}
45+
}
46+
47+
/**
48+
* Handles cache storage.
49+
*/
50+
public function onKernelResponse(FilterResponseEvent $event)
51+
{
52+
$request = $event->getRequest();
53+
$response = $event->getResponse();
54+
55+
if ($response->isSuccessful()) {
56+
$this->storage->cache($request, $response);
57+
}
58+
}
59+
60+
public static function getSubscribedEvents()
61+
{
62+
return array(
63+
KernelEvents::CONTROLLER => 'onKernelController',
64+
KernelEvents::RESPONSE => 'onKernelResponse',
65+
);
66+
}
67+
}

Resources/config/services.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
services:
2+
execution_cache.kernel_listener:
3+
class: StadLine\ExecutionCacheBundle\Listener\KernelListener
4+
arguments: [ @execution_cache.default_storage ]
5+
tags:
6+
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController, priority: -255 }
7+
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -255 }
8+
9+
execution_cache.default_storage:
10+
class: StadLine\ExecutionCacheBundle\Cache\Storage
11+
arguments: [ @cache, 'exc_' ]
12+
calls:
13+
- [ setKeyProvider, [ @execution_cache.default_key_provider ]]
14+
15+
execution_cache.default_key_provider:
16+
class: StadLine\ExecutionCacheBundle\Cache\KeyProvider

StadLineExecutionCacheBundle.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace StadLine\ExecutionCacheBundle;
4+
5+
use Symfony\Component\HttpKernel\Bundle\Bundle;
6+
7+
class StadLineExecutionCacheBundle extends Bundle
8+
{
9+
}

0 commit comments

Comments
 (0)