VarnishCache   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 182
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 10

Importance

Changes 0
Metric Value
wmc 21
lcom 2
cbo 10
dl 0
loc 182
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
A flushAll() 0 7 3
A flush() 0 11 2
A has() 0 4 1
A get() 0 14 3
A set() 0 8 1
A cacheAction() 0 24 3
A isContextual() 0 4 1
A runCommand() 0 20 3
A getUrl() 0 9 1
A computeHash() 0 6 1
A normalize() 0 4 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\CacheBundle\Adapter;
15
16
use Sonata\Cache\CacheAdapterInterface;
17
use Sonata\Cache\CacheElement;
18
use Sonata\Cache\CacheElementInterface;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
22
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
23
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
24
use Symfony\Component\Process\Process;
25
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
26
use Symfony\Component\Routing\RouterInterface;
27
28
/**
29
 * http://www.varnish-cache.org/docs/2.1/reference/varnishadm.html
30
 *  echo vcl.use foo | varnishadm -T localhost:999 -S /var/db/secret
31
 *  echo vcl.use foo | ssh vhost varnishadm -T localhost:999 -S /var/db/secret.
32
 *
33
 *  in the config.yml file :
34
 *     echo %s "%s" | varnishadm -T localhost:999 -S /var/db/secret
35
 *     echo %s "%s" | ssh vhost "varnishadm -T localhost:999 -S /var/db/secret {{ COMMAND }} '{{ EXPRESSION }}'"
36
 */
37
class VarnishCache implements CacheAdapterInterface
38
{
39
    /**
40
     * @var string
41
     */
42
    protected $token;
43
44
    /**
45
     * @var array
46
     */
47
    protected $servers;
48
49
    /**
50
     * @var RouterInterface
51
     */
52
    protected $router;
53
54
    /**
55
     * @var string
56
     */
57
    protected $purgeInstruction;
58
59
    /**
60
     * @var ControllerResolverInterface
61
     */
62
    protected $resolver;
63
64
    /**
65
     * @var ArgumentResolverInterface
66
     */
67
    private $argumentResolver;
68
69
    /**
70
     * @param string $purgeInstruction The purge instruction (purge in Varnish 2, ban in Varnish 3)
71
     */
72
    public function __construct(
73
        string $token,
74
        array $servers,
75
        RouterInterface $router,
76
        string $purgeInstruction,
77
        ControllerResolverInterface $resolver,
78
        ArgumentResolverInterface $argumentResolver
79
    ) {
80
        $this->token = $token;
81
        $this->servers = $servers;
82
        $this->router = $router;
83
        $this->purgeInstruction = $purgeInstruction;
84
        $this->resolver = $resolver;
85
        $this->argumentResolver = $argumentResolver;
86
    }
87
88
    public function flushAll(): bool
89
    {
90
        return $this->runCommand(
91
            'ban' === $this->purgeInstruction ? 'ban.url' : 'purge',
92
            'ban' === $this->purgeInstruction ? '.*' : 'req.url ~ .*'
93
        );
94
    }
95
96
    public function flush(array $keys = []): bool
97
    {
98
        $parameters = [];
99
        foreach ($keys as $key => $value) {
100
            $parameters[] = sprintf('obj.http.%s ~ %s', $this->normalize($key), $value);
101
        }
102
103
        $purge = implode(' && ', $parameters);
104
105
        return $this->runCommand($this->purgeInstruction, $purge);
106
    }
107
108
    public function has(array $keys): bool
109
    {
110
        return true;
111
    }
112
113
    /**
114
     * @throws \RuntimeException
115
     */
116
    public function get(array $keys): CacheElementInterface
117
    {
118
        if (!isset($keys['controller'])) {
119
            throw new \RuntimeException('Please define a controller key');
120
        }
121
122
        if (!isset($keys['parameters'])) {
123
            throw new \RuntimeException('Please define a parameters key');
124
        }
125
126
        $content = sprintf('<esi:include src="%s"/>', $this->getUrl($keys));
127
128
        return new CacheElement($keys, new Response($content));
129
    }
130
131
    public function set(
132
        array $keys,
133
        $data,
134
        int $ttl = CacheElement::DAY,
135
        array $contextualKeys = []
136
    ): CacheElementInterface {
137
        return new CacheElement($keys, $data, $ttl, $contextualKeys);
138
    }
139
140
    /**
141
     * @throws AccessDeniedHttpException
142
     * @throws \UnexpectedValueException
143
     *
144
     * @return mixed
145
     */
146
    public function cacheAction(Request $request)
147
    {
148
        $parameters = $request->get('parameters', []);
149
150
        if ($request->get('token') !== $this->computeHash($parameters)) {
151
            throw new AccessDeniedHttpException('Invalid token');
152
        }
153
154
        $subRequest = Request::create('', 'get', $parameters, $request->cookies->all(), [], $request->server->all());
155
156
        $controller = $this->resolver->getController($subRequest);
157
        if (!$controller) {
158
            throw new \UnexpectedValueException(
159
                'Could not find a controller for this subrequest.'
160
            );
161
        }
162
163
        $subRequest->attributes->add(['_controller' => $parameters['controller']]);
164
        $subRequest->attributes->add($parameters['parameters']);
165
166
        $arguments = $this->argumentResolver->getArguments($subRequest, $controller);
167
168
        return \call_user_func_array($controller, $arguments);
169
    }
170
171
    public function isContextual(): bool
172
    {
173
        return true;
174
    }
175
176
    protected function runCommand(string $command, string $expression): bool
177
    {
178
        $return = true;
179
180
        foreach ($this->servers as $server) {
181
            $process = new Process(str_replace(
182
                ['{{ COMMAND }}', '{{ EXPRESSION }}'],
183
                [$command, $expression],
184
                $server
185
            ));
186
187
            if (0 === $process->run()) {
188
                continue;
189
            }
190
191
            $return = false;
192
        }
193
194
        return $return;
195
    }
196
197
    protected function getUrl(array $keys): ?string
198
    {
199
        $parameters = [
200
            'token' => $this->computeHash($keys),
201
            'parameters' => $keys,
202
        ];
203
204
        return $this->router->generate('sonata_cache_esi', $parameters, UrlGeneratorInterface::ABSOLUTE_PATH);
205
    }
206
207
    protected function computeHash(array $keys): string
208
    {
209
        ksort($keys);
210
211
        return hash('sha256', $this->token.serialize($keys));
212
    }
213
214
    protected function normalize(string $key): string
215
    {
216
        return sprintf('x-sonata-cache-%s', str_replace(['_', '\\'], '-', strtolower($key)));
217
    }
218
}
219