Completed
Push — master ( 99e856...e5db02 )
by David
29s queued 27s
created

CacheInvalidator::dispatch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 7
Ratio 63.64 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 7
loc 11
ccs 0
cts 0
cp 0
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 6
1
<?php
2
3
/*
4
 * This file is part of the FOSHttpCache package.
5
 *
6
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
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 FOS\HttpCache;
13
14
use FOS\HttpCache\Exception\ExceptionCollection;
15
use FOS\HttpCache\Exception\InvalidArgumentException;
16
use FOS\HttpCache\Exception\ProxyResponseException;
17
use FOS\HttpCache\Exception\ProxyUnreachableException;
18
use FOS\HttpCache\Exception\UnsupportedProxyOperationException;
19
use FOS\HttpCache\ProxyClient\Invalidation\BanCapable;
20
use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable;
21
use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable;
22
use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable;
23
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;
24
use FOS\HttpCache\ProxyClient\ProxyClient;
25
use FOS\HttpCache\ProxyClient\Symfony;
26
use Symfony\Component\EventDispatcher\EventDispatcher;
27
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
28
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
29
use Toflar\Psr6HttpCacheStore\Psr6Store;
30
31
/**
32
 * Manages HTTP cache invalidation.
33
 *
34
 * @author David de Boer <[email protected]>
35
 * @author David Buchmann <[email protected]>
36
 * @author André Rømcke <[email protected]>
37
 */
38
class CacheInvalidator
39
{
40
    /**
41
     * Value to check support of invalidatePath operation.
42
     */
43
    const PATH = 'path';
44
45
    /**
46
     * Value to check support of refreshPath operation.
47
     */
48
    const REFRESH = 'refresh';
49
50
    /**
51
     * Value to check support of invalidate and invalidateRegex operations.
52
     */
53
    const INVALIDATE = 'invalidate';
54
55
    /**
56
     * Value to check support of invalidateTags operation.
57
     */
58
    const TAGS = 'tags';
59
60
    /**
61
     * Value to check support of clearCache operation.
62
     */
63
    const CLEAR = 'clear';
64
65
    /**
66
     * @var ProxyClient
67
     */
68
    private $cache;
69
70
    /**
71 12
     * @var EventDispatcherInterface
72
     */
73 12
    private $eventDispatcher;
74 12
75
    /**
76
     * Constructor.
77
     *
78
     * @param ProxyClient $cache HTTP cache
79
     */
80
    public function __construct(ProxyClient $cache)
81
    {
82
        $this->cache = $cache;
83
    }
84
85
    /**
86
     * Check whether this invalidator instance supports the specified
87
     * operation.
88
     *
89
     * Support for PATH means invalidatePath will work, REFRESH means
90 3
     * refreshPath works, TAGS means that invalidateTags works and
91
     * INVALIDATE is for the invalidate and invalidateRegex methods.
92
     *
93 3
     * @param string $operation one of the class constants
94 2
     *
95 3
     * @return bool
96 2
     *
97 3
     * @throws InvalidArgumentException
98 2
     */
99 3
    public function supports($operation)
100 2
    {
101
        switch ($operation) {
102 1
            case self::PATH:
103
                return $this->cache instanceof PurgeCapable;
104
            case self::REFRESH:
105
                return $this->cache instanceof RefreshCapable;
106
            case self::INVALIDATE:
107
                return $this->cache instanceof BanCapable;
108
            case self::TAGS:
109
                $supports = $this->cache instanceof TagCapable;
110
                if ($supports && $this->cache instanceof Symfony) {
111
                    return class_exists(Psr6Store::class);
112
                }
113
114
                return $supports;
115 2
            case self::CLEAR:
116
                return $this->cache instanceof ClearCapable;
117 2
            default:
118
                throw new InvalidArgumentException('Unknown operation '.$operation);
119
        }
120 1
    }
121
122 2
    /**
123 2
     * Set event dispatcher - may only be called once.
124
     *
125
     * @param EventDispatcherInterface $eventDispatcher
126
     *
127
     * @throws \Exception when trying to override the event dispatcher
128
     */
129
    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher)
130 2
    {
131
        if ($this->eventDispatcher) {
132 2
            // if you want to set a custom event dispatcher, do so right after instantiating
133 1
            // the invalidator.
134
            throw new \Exception('You may not change the event dispatcher once it is set.');
135
        }
136 2
        $this->eventDispatcher = $eventDispatcher;
137
    }
138
139
    /**
140
     * Get the event dispatcher used by the cache invalidator.
141
     *
142
     * @return EventDispatcherInterface
143
     */
144 View Code Duplication
    public function getEventDispatcher()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
145
    {
146
        if (!$this->eventDispatcher) {
147
            if (class_exists(LegacyEventDispatcherProxy::class)) {
148
                $this->eventDispatcher = LegacyEventDispatcherProxy::decorate(new EventDispatcher());
149 1
            } else {
150
                $this->eventDispatcher = new EventDispatcher();
151 1
            }
152
        }
153
154
        return $this->eventDispatcher;
155 1
    }
156
157 1
    /**
158
     * Invalidate a path or URL.
159
     *
160
     * @param string $path    Path or URL
161
     * @param array  $headers HTTP headers (optional)
162
     *
163
     * @throws UnsupportedProxyOperationException
164
     *
165
     * @return $this
166
     */
167
    public function invalidatePath($path, array $headers = [])
168
    {
169
        if (!$this->cache instanceof PurgeCapable) {
170
            throw UnsupportedProxyOperationException::cacheDoesNotImplement('PURGE');
171
        }
172 1
173
        $this->cache->purge($path, $headers);
174 1
175
        return $this;
176
    }
177
178 1
    /**
179
     * Refresh a path or URL.
180 1
     *
181
     * @param string $path    Path or URL
182
     * @param array  $headers HTTP headers (optional)
183
     *
184
     * @see RefreshCapable::refresh()
185
     *
186
     * @throws UnsupportedProxyOperationException
187
     *
188
     * @return $this
189
     */
190
    public function refreshPath($path, array $headers = [])
191
    {
192
        if (!$this->cache instanceof RefreshCapable) {
193
            throw UnsupportedProxyOperationException::cacheDoesNotImplement('REFRESH');
194
        }
195
196
        $this->cache->refresh($path, $headers);
197 1
198
        return $this;
199 1
    }
200
201
    /**
202
     * Invalidate all cached objects matching the provided HTTP headers.
203 1
     *
204
     * Each header is a a POSIX regular expression, for example
205 1
     * ['X-Host' => '^(www\.)?(this|that)\.com$']
206
     *
207
     * @see BanCapable::ban()
208
     *
209
     * @param array $headers HTTP headers that path must match to be banned
210
     *
211
     * @throws UnsupportedProxyOperationException If HTTP cache does not support BAN requests
212
     *
213
     * @return $this
214
     */
215
    public function invalidate(array $headers)
216
    {
217
        if (!$this->cache instanceof BanCapable) {
218
            throw UnsupportedProxyOperationException::cacheDoesNotImplement('BAN');
219 2
        }
220
221 2
        $this->cache->ban($headers);
222
223
        return $this;
224 2
    }
225
226 2
    /**
227
     * Remove/Expire cache objects based on cache tags.
228
     *
229
     * @see TagCapable::tags()
230
     *
231
     * @param array $tags Tags that should be removed/expired from the cache
232
     *
233
     * @throws UnsupportedProxyOperationException If HTTP cache does not support Tags invalidation
234
     *
235
     * @return $this
236
     */
237
    public function invalidateTags(array $tags)
238
    {
239
        if (!$this->cache instanceof TagCapable) {
240
            throw UnsupportedProxyOperationException::cacheDoesNotImplement('Tags');
241
        }
242
        $this->cache->invalidateTags($tags);
243
244
        return $this;
245
    }
246
247
    /**
248
     * Invalidate URLs based on a regular expression for the URI, an optional
249
     * content type and optional limit to certain hosts.
250
     *
251 1
     * The hosts parameter can either be a regular expression, e.g.
252
     * '^(www\.)?(this|that)\.com$' or an array of exact host names, e.g.
253 1
     * ['example.com', 'other.net']. If the parameter is empty, all hosts
254
     * are matched.
255
     *
256
     * @see BanCapable::banPath()
257 1
     *
258
     * @param string       $path        Regular expression pattern for URI to
259 1
     *                                  invalidate
260
     * @param string       $contentType Regular expression pattern for the content
261
     *                                  type to limit banning, for instance 'text'
262
     * @param array|string $hosts       Regular expression of a host name or list of
263
     *                                  exact host names to limit banning
264
     *
265
     * @throws UnsupportedProxyOperationException If HTTP cache does not support BAN requests
266
     *
267
     * @return $this
268
     */
269 3
    public function invalidateRegex($path, $contentType = null, $hosts = null)
270
    {
271
        if (!$this->cache instanceof BanCapable) {
272 3
            throw UnsupportedProxyOperationException::cacheDoesNotImplement('BAN');
273 1
        }
274 1
275 1
        $this->cache->banPath($path, $contentType, $hosts);
276 1
277 1
        return $this;
278 1
    }
279 1
280 1
    /**
281
     * Clear the cache completely.
282
     *
283
     * @throws UnsupportedProxyOperationException if HTTP cache does not support clearing the cache completely
284 1
     *
285
     * @return $this
286
     */
287
    public function clearCache()
288
    {
289
        if (!$this->cache instanceof ClearCapable) {
290
            throw UnsupportedProxyOperationException::cacheDoesNotImplement('CLEAR');
291
        }
292
293
        $this->cache->clear();
294
295
        return $this;
296
    }
297
298
    /**
299
     * Send all pending invalidation requests.
300
     *
301
     * @return int the number of cache invalidations performed per caching server
302
     *
303
     * @throws ExceptionCollection if any errors occurred during flush
304
     */
305
    public function flush()
306
    {
307
        try {
308
            return $this->cache->flush();
309
        } catch (ExceptionCollection $exceptions) {
310
            foreach ($exceptions as $exception) {
311
                $event = new Event();
312
                $event->setException($exception);
313
                if ($exception instanceof ProxyResponseException) {
314
                    $this->dispatch($event, Events::PROXY_RESPONSE_ERROR);
315
                } elseif ($exception instanceof ProxyUnreachableException) {
316
                    $this->dispatch($event, Events::PROXY_UNREACHABLE_ERROR);
317
                }
318
            }
319
320
            throw $exceptions;
321
        }
322
    }
323
324
    private function dispatch(Event $event, $eventName)
325
    {
326
        // LegacyEventDispatcherProxy exists in Symfony >= 4.3
327 View Code Duplication
        if (class_exists(LegacyEventDispatcherProxy::class)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
328
            // New Symfony 4.3 EventDispatcher signature
329
            $this->getEventDispatcher()->dispatch($event, $eventName);
0 ignored issues
show
Documentation introduced by
$event is of type object<FOS\HttpCache\Event>, but the function expects a object<Symfony\Contracts\EventDispatcher\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
330
        } else {
331
            // Old EventDispatcher signature
332
            $this->getEventDispatcher()->dispatch($eventName, $event);
0 ignored issues
show
Documentation introduced by
$event is of type object<FOS\HttpCache\Event>, but the function expects a null|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
333
        }
334
    }
335
}
336