Completed
Pull Request — master (#444)
by Yanick
11:58 queued 08:24
created

HttpDispatcher::flush()   B

Complexity

Conditions 9
Paths 40

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 9.0117

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 18
cts 19
cp 0.9474
rs 7.7404
c 0
b 0
f 0
cc 9
nc 40
nop 0
crap 9.0117
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\ProxyClient;
13
14
use FOS\HttpCache\Exception\ExceptionCollection;
15
use FOS\HttpCache\Exception\InvalidArgumentException;
16
use FOS\HttpCache\Exception\InvalidUrlException;
17
use FOS\HttpCache\Exception\MissingHostException;
18
use FOS\HttpCache\Exception\ProxyResponseException;
19
use FOS\HttpCache\Exception\ProxyUnreachableException;
20
use Http\Client\Common\Plugin\ErrorPlugin;
21
use Http\Client\Common\PluginClient;
22
use Http\Client\Exception\HttpException;
23
use Http\Client\Exception\RequestException;
24
use Http\Client\HttpAsyncClient;
25
use Http\Discovery\HttpAsyncClientDiscovery;
26
use Http\Discovery\UriFactoryDiscovery;
27
use Http\Message\UriFactory;
28
use Http\Promise\Promise;
29
use Psr\Http\Message\RequestInterface;
30
use Psr\Http\Message\UriInterface;
31
32
/**
33
 * Queue and send HTTP requests with a Httplug asynchronous client.
34
 *
35
 * @author David Buchmann <[email protected]>
36
 */
37
class HttpDispatcher implements Dispatcher
38
{
39
    /**
40
     * @var HttpAsyncClient
41
     */
42
    private $httpClient;
43
44
    /**
45
     * @var UriFactory
46
     */
47
    private $uriFactory;
48
49
    /**
50
     * Queued requests.
51
     *
52
     * @var RequestInterface[]
53
     */
54
    private $queue = [];
55
56
    /**
57
     * Caching proxy server host names or IP addresses.
58
     *
59
     * @var UriInterface[]
60
     */
61
    private $servers;
62
63
    /**
64
     * Application host name and optional base URL.
65
     *
66
     * @var UriInterface[]
67
     */
68
    private $baseUris;
69
70
    /**
71
     * If you specify a custom HTTP client, make sure that it converts HTTP
72
     * errors to exceptions.
73
     *
74
     * If your proxy server IPs can not be statically configured, extend this
75
     * class and overwrite getServers. Be sure to have some caching in
76
     * getServers.
77
     *
78
     * @param string[]             $servers    Caching proxy server hostnames or IP
79
     *                                         addresses, including port if not port 80.
80
     *                                         E.g. ['127.0.0.1:6081']
81
     * @param string|string[]      $baseUris   Default application hostnames, optionally
82
     *                                         including base URL, for purge and refresh
83
     *                                         requests (optional). At least one is required if
84
     *                                         you purge and refresh paths instead of
85
     *                                         absolute URLs. A request will be sent for each
86
     *                                         base URL.
87
     * @param HttpAsyncClient|null $httpClient Client capable of sending HTTP requests. If no
88
     *                                         client is supplied, a default one is created
89
     * @param UriFactory|null      $uriFactory Factory for PSR-7 URIs. If not specified, a
90
     *                                         default one is created
91 38
     */
92
    public function __construct(
93
        array $servers,
94
        $baseUris = [],
95
        HttpAsyncClient $httpClient = null,
96
        UriFactory $uriFactory = null
97 38
    ) {
98 26
        if (!$httpClient) {
99 26
            $httpClient = new PluginClient(
100 26
                HttpAsyncClientDiscovery::find(),
101
                [new ErrorPlugin()]
102
            );
103 38
        }
104 38
        $this->httpClient = $httpClient;
105
        $this->uriFactory = $uriFactory ?: UriFactoryDiscovery::find();
106 38
107 35
        $this->setServers($servers);
108 34
109
        // Support both, a string or an array of strings (array_filter to kill empty base URLs)
110
        if (is_string($baseUris)) {
111
            if ('' === $baseUris) {
112
                $baseUris = [];
113
            } else {
114
                $baseUris = [$baseUris];
115
            }
116
        }
117
118 33
        $this->setBaseUris($baseUris);
119
    }
120 33
121 1
    /**
122
     * {@inheritdoc}
123
     */
124 32
    public function invalidate(RequestInterface $invalidationRequest, $validateHost = true)
125
    {
126 32
        if ($validateHost && 0 === \count($this->baseUris) && !$invalidationRequest->getUri()->getHost()) {
127 1
            throw MissingHostException::missingHost((string) $invalidationRequest->getUri());
128
        }
129
130 32
        $signature = $this->getRequestSignature($invalidationRequest);
131 32
132
        if (isset($this->queue[$signature])) {
133
            return;
134
        }
135
136
        $this->queue[$signature] = $invalidationRequest;
137
    }
138
139
    /**
140 33
     * {@inheritdoc}
141
     */
142 33
    public function flush()
143 33
    {
144
        $queue = $this->queue;
145 33
        $this->queue = [];
146
        /** @var Promise[] $promises */
147 33
        $promises = [];
148
149 33
        $exceptions = new ExceptionCollection();
150 32
151
        foreach ($queue as $request) {
152 32
            foreach ($this->fanOut($request) as $proxyRequest) {
153 1
                try {
154 32
                    $promises[] = $this->httpClient->sendAsyncRequest($proxyRequest);
155
                } catch (\Exception $e) {
156
                    $exceptions->add(new InvalidArgumentException($e));
157
                }
158
            }
159 33
        }
160
161 32
        foreach ($promises as $promise) {
162 4
            try {
163 2
                $promise->wait();
164 2
            } catch (HttpException $exception) {
165 2
                $exceptions->add(ProxyResponseException::proxyResponse($exception));
166
            } catch (RequestException $exception) {
167
                $exceptions->add(ProxyUnreachableException::proxyUnreachable($exception));
168
            } catch (\Exception $exception) {
169
                // @codeCoverageIgnoreStart
170
                $exceptions->add(new InvalidArgumentException($exception));
171
                // @codeCoverageIgnoreEnd
172
            }
173 33
        }
174 5
175
        if (count($exceptions)) {
176
            throw $exceptions;
177 31
        }
178
179
        return count($queue);
180
    }
181
182
    /**
183
     * Get the list of servers to send invalidation requests to.
184
     *
185 32
     * @return UriInterface[]
186
     */
187 32
    protected function getServers()
188
    {
189
        return $this->servers;
190
    }
191
192
    /**
193
     * Duplicate a request for each caching server.
194
     *
195
     * @param RequestInterface $request The request to duplicate for each configured server
196
     *
197 32
     * @return RequestInterface[]
198
     */
199 32
    private function fanOut(RequestInterface $request)
200
    {
201 32
        $serverRequests = [];
202
203
        if (0 !== \count($this->baseUris)) {
204
            // If base URIs are configured, try to make partial invalidation
205 32
            // requests complete and send out a request for every base URI
206 30
            $requests = $this->requestToBaseUris($request);
207
        } else {
208 5
            /** @var RequestInterface[] $requests */
209 5
            $requests = [$request];
210
        }
211
212
        // Create all requests to each caching proxy server
213 25
        foreach ($requests as $request) {
214 25
215
            $uri = $request->getUri();
216
217 25
            // Close connections to make sure invalidation (PURGE/BAN) requests
218 16
            // will not interfere with content (GET) requests.
219
            $requests[] = $request->withUri($uri)->withHeader('Connection', 'Close');
220
221
            foreach ($this->getServers() as $server) {
222 25
                $serverRequests[] = $request->withUri(
223 1
                    $uri
224 1
                        ->withScheme($server->getScheme())
225
                        ->withHost($server->getHost())
226
                        ->withPort($server->getPort()),
227
                    true    // Preserve application Host header
228
                );
229
            }
230
        }
231 32
232
        return $serverRequests;
233
    }
234 32
235 32
    /**
236
     * Looks at a given request and returns an array of requests incorporating
237 32
     * every configured base URI.
238 32
     *
239 32
     * @param RequestInterface $request The request to modify for every configured base URI
240 32
     *
241
     * @return RequestInterface[]
242
     */
243
    private function requestToBaseUris(RequestInterface $request)
244 32
    {
245
        $requests = [];
246
247
        foreach ($this->baseUris as $baseUri) {
248
            $uri = $request->getUri();
249
250
            if ($uri->getHost()) {
251
                // Absolute URI: does it already have a scheme?
252
                if (!$uri->getScheme() && '' !== $baseUri->getScheme()) {
253
                    $uri = $uri->withScheme($baseUri->getScheme());
254
                }
255
            } else {
256
                // Relative URI
257 38
                if ('' !== $baseUri->getHost()) {
258
                    $uri = $uri->withHost($baseUri->getHost());
259 38
                }
260 38
261 38
                if ($baseUri->getPort()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $baseUri->getPort() of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
262
                    $uri = $uri->withPort($baseUri->getPort());
263 35
                }
264
265
                // Base path
266
                if ('' !== $baseUri->getPath()) {
267
                    $path = $baseUri->getPath().'/'.ltrim($uri->getPath(), '/');
268
                    $uri = $uri->withPath($path);
269
                }
270
            }
271
272
            $requests[] = $request->withUri($uri);
273 35
        }
274
275 35
        return $requests;
276 3
    }
277
278 3
    /**
279
     * Set caching proxy server URI objects, validating them.
280
     *
281 32
     * @param string[] $servers Caching proxy proxy server hostnames or IP
282 31
     *                          addresses, including port if not port 80.
283
     *                          E.g. ['127.0.0.1:6081']
284
     *
285
     * @throws InvalidUrlException If server is invalid or contains URL
286
     *                             parts other than scheme, host, port
287
     */
288
    private function setServers(array $servers)
289
    {
290
        $this->servers = [];
291
        foreach ($servers as $server) {
292
            $this->servers[] = $this->filterUri($server, ['scheme', 'host', 'port']);
293
        }
294
    }
295
296
    /**
297
     * Set application base URI that will be prefixed to relative purge and
298 38
     * refresh requests, and validate it.
299
     *
300 38
     * @param array $baseUris Your application’s base URIs
301 1
     *
302 1
     * @throws InvalidUrlException If the base URI is not a valid URI
303 1
     */
304
    private function setBaseUris(array $baseUris = [])
305
    {
306
        if (0 === \count($baseUris)) {
307
            $this->baseUris = [];
308
309
            return;
310 38
        }
311 34
312
        foreach ($baseUris as $baseUri) {
313
            $this->baseUris[] =  $this->filterUri($baseUri);
314
        }
315 38
    }
316 1
317 1
    /**
318
     * Filter a URL.
319
     *
320 37
     * Prefix the URL with "http://" if it has no scheme, then check the URL
321 1
     * for validity. You can specify what parts of the URL are allowed.
322
     *
323
     * @param string   $uriString
324 36
     * @param string[] $allowedParts Array of allowed URL parts (optional)
325 36
     *
326 36
     * @return UriInterface Filtered URI (with default scheme if there was no scheme)
327 36
     *
328 1
     * @throws InvalidUrlException If URL is invalid, the scheme is not http or
329
     *                             contains parts that are not expected
330
     */
331
    private function filterUri($uriString, array $allowedParts = [])
332 35
    {
333
        if (!is_string($uriString)) {
334
            throw new \InvalidArgumentException(sprintf(
335
                'URI parameter must be a string, %s given',
336
                gettype($uriString)
337
            ));
338
        }
339
340
        // Creating a PSR-7 URI without scheme (with parse_url) results in the
341
        // original hostname to be seen as path. So first add a scheme if none
342
        // is given.
343
        if (false === strpos($uriString, '://')) {
344
            $uriString = sprintf('%s://%s', 'http', $uriString);
345 32
        }
346
347 32
        try {
348 32
            $uri = $this->uriFactory->createUri($uriString);
349
        } catch (\InvalidArgumentException $e) {
350 32
            throw InvalidUrlException::invalidUrl($uriString);
351
        }
352
353
        if (!$uri->getScheme()) {
354
            throw InvalidUrlException::invalidUrl($uriString, 'empty scheme');
355
        }
356
357
        if (count($allowedParts) > 0) {
358
            $parts = parse_url((string) $uri);
359
            $diff = array_diff(array_keys($parts), $allowedParts);
360
            if (count($diff) > 0) {
361
                throw InvalidUrlException::invalidUrlParts($uriString, $allowedParts);
362
            }
363
        }
364
365
        return $uri;
366
    }
367
368
    /**
369
     * Build a request signature based on the request data. Unique for every different request, identical
370
     * for the same requests.
371
     *
372
     * This signature is used to avoid sending the same invalidation request twice.
373
     *
374
     * @param RequestInterface $request An invalidation request
375
     *
376
     * @return string A signature for this request
377
     */
378
    private function getRequestSignature(RequestInterface $request)
379
    {
380
        $headers = $request->getHeaders();
381
        ksort($headers);
382
383
        return sha1($request->getMethod()."\n".$request->getUri()."\n".var_export($headers, true));
384
    }
385
}
386