Completed
Pull Request — master (#161)
by Grégoire
01:22
created

VarnishCache::flush()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Sonata Project package.
5
 *
6
 * (c) Thomas Rabaix <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sonata\CacheBundle\Adapter;
13
14
use Sonata\Cache\CacheAdapterInterface;
15
use Sonata\Cache\CacheElement;
16
use Sonata\Cache\CacheElementInterface;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpFoundation\Response;
19
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
20
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
21
use Symfony\Component\Process\Process;
22
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
23
use Symfony\Component\Routing\RouterInterface;
24
25
/**
26
 * http://www.varnish-cache.org/docs/2.1/reference/varnishadm.html
27
 *  echo vcl.use foo | varnishadm -T localhost:999 -S /var/db/secret
28
 *  echo vcl.use foo | ssh vhost varnishadm -T localhost:999 -S /var/db/secret.
29
 *
30
 *  in the config.yml file :
31
 *     echo %s "%s" | varnishadm -T localhost:999 -S /var/db/secret
32
 *     echo %s "%s" | ssh vhost "varnishadm -T localhost:999 -S /var/db/secret {{ COMMAND }} '{{ EXPRESSION }}'"
33
 */
34
class VarnishCache implements CacheAdapterInterface
35
{
36
    /**
37
     * @var string
38
     */
39
    protected $token;
40
41
    /**
42
     * @var array
43
     */
44
    protected $servers;
45
46
    /**
47
     * @var RouterInterface
48
     */
49
    protected $router;
50
51
    /**
52
     * @var string
53
     */
54
    protected $purgeInstruction;
55
56
    /**
57
     * @var ControllerResolverInterface
58
     */
59
    protected $resolver;
60
61
    /**
62
     * Constructor.
63
     *
64
     * @param string                           $token            A token
65
     * @param array                            $servers          An array of servers
66
     * @param RouterInterface                  $router           A router instance
67
     * @param string                           $purgeInstruction The purge instruction (purge in Varnish 2, ban in Varnish 3)
68
     * @param null|ControllerResolverInterface $resolver         A controller resolver instance
69
     */
70
    public function __construct($token, array $servers, RouterInterface $router, $purgeInstruction, ControllerResolverInterface $resolver = null)
71
    {
72
        $this->token = $token;
73
        $this->servers = $servers;
74
        $this->router = $router;
75
        $this->purgeInstruction = $purgeInstruction;
76
        $this->resolver = $resolver;
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82
    public function flushAll(): bool
83
    {
84
        return $this->runCommand(
85
            $this->purgeInstruction == 'ban' ? 'ban.url' : 'purge',
86
            $this->purgeInstruction == 'ban' ? '.*' : 'req.url ~ .*'
87
        );
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    public function flush(array $keys = []): bool
94
    {
95
        $parameters = [];
96
        foreach ($keys as $key => $value) {
97
            $parameters[] = sprintf('obj.http.%s ~ %s', $this->normalize($key), $value);
98
        }
99
100
        $purge = implode(' && ', $parameters);
101
102
        return $this->runCommand($this->purgeInstruction, $purge);
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function has(array $keys): bool
109
    {
110
        return true;
111
    }
112
113
    /**
114
     * {@inheritdoc}
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
    /**
132
     * {@inheritdoc}
133
     */
134
    public function set(array $keys, $data, int $ttl = CacheElement::DAY, array $contextualKeys = []): CacheElementInterface
135
    {
136
        return new CacheElement($keys, $data, $ttl, $contextualKeys);
137
    }
138
139
    /**
140
     * Cache action.
141
     *
142
     * @param Request $request
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->resolver->getArguments($subRequest, $controller);
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\HttpKe...terface::getArguments() has been deprecated with message: This method is deprecated as of 3.1 and will be removed in 4.0. Please use the {@see ArgumentResolverInterface} instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
167
168
        // call controller
169
        return call_user_func_array($controller, $arguments);
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175
    public function isContextual(): bool
176
    {
177
        return true;
178
    }
179
180
    /**
181
     * @param string $command
182
     * @param string $expression
183
     *
184
     * @return bool
185
     */
186
    protected function runCommand($command, $expression)
187
    {
188
        $return = true;
189
190
        foreach ($this->servers as $server) {
191
            $process = new Process(str_replace(
192
                ['{{ COMMAND }}', '{{ EXPRESSION }}'],
193
                [$command, $expression],
194
                $server
195
            ));
196
197
            if ($process->run() == 0) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $process->run() of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
198
                continue;
199
            }
200
201
            $return = false;
202
        }
203
204
        return $return;
205
    }
206
207
    /**
208
     * Gets the URL by the given keys.
209
     *
210
     * @param array $keys
211
     *
212
     * @return string
213
     */
214
    protected function getUrl(array $keys)
215
    {
216
        $parameters = [
217
            'token' => $this->computeHash($keys),
218
            'parameters' => $keys,
219
        ];
220
221
        return $this->router->generate('sonata_cache_esi', $parameters, UrlGeneratorInterface::ABSOLUTE_PATH);
222
    }
223
224
    /**
225
     * Computes the given keys.
226
     *
227
     * @param array $keys
228
     *
229
     * @return string
230
     */
231
    protected function computeHash(array $keys)
232
    {
233
        ksort($keys);
234
235
        return hash('sha256', $this->token.serialize($keys));
236
    }
237
238
    /**
239
     * Normalizes the given key.
240
     *
241
     * @param string $key
242
     *
243
     * @return string
244
     */
245
    protected function normalize($key)
246
    {
247
        return sprintf('x-sonata-cache-%s', str_replace(['_', '\\'], '-', strtolower($key)));
248
    }
249
}
250