Completed
Pull Request — master (#312)
by David
19:18 queued 13:26
created

HttpDispatcher::filterUri()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7.3387

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 17
cts 21
cp 0.8095
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 19
nc 11
nop 2
crap 7.3387
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\Http;
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\Exception\HttpException;
21
use Http\Client\Exception\RequestException;
22
use Http\Client\HttpAsyncClient;
23
use Http\Discovery\HttpAsyncClientDiscovery;
24
use Http\Discovery\UriFactoryDiscovery;
25
use Http\Message\UriFactory;
26
use Http\Promise\Promise;
27
use Psr\Http\Message\RequestInterface;
28
use Psr\Http\Message\UriInterface;
29
30
/**
31
 * Queue and send HTTP requests with a Httplug asynchronous client.
32
 *
33
 * @author David Buchmann <[email protected]>
34
 */
35
class HttpDispatcher
36
{
37
    /**
38
     * @var HttpAsyncClient
39
     */
40
    private $httpClient;
41
42
    /**
43
     * @var UriFactory
44
     */
45
    private $uriFactory;
46
47
    /**
48
     * Queued requests.
49
     *
50
     * @var RequestInterface[]
51
     */
52
    private $queue = [];
53
54
    /**
55
     * Caching proxy server host names or IP addresses.
56
     *
57
     * @var UriInterface[]
58
     */
59
    private $servers;
60
61
    /**
62
     * Application host name and optional base URL.
63
     *
64
     * @var UriInterface
65
     */
66
    private $baseUri;
67
68
    /**
69
     * @param string[]             $servers    Caching proxy server hostnames or IP
70
     *                                         addresses, including port if not port 80.
71
     *                                         E.g. ['127.0.0.1:6081']
72
     * @param string               $baseUri    Default application hostname, optionally
73
     *                                         including base URL, for purge and refresh
74
     *                                         requests (optional). This is required if
75
     *                                         you purge and refresh paths instead of
76
     *                                         absolute URLs
77
     * @param HttpAsyncClient|null $httpClient Client capable of sending HTTP requests. If no
78
     *                                         client is supplied, a default one is created
79
     * @param UriFactory|null      $uriFactory Factory for PSR-7 URIs. If not specified, a
80
     *                                         default one is created
81
     */
82 33
    public function __construct(
83
        array $servers,
84
        $baseUri = '',
85
        HttpAsyncClient $httpClient = null,
86
        UriFactory $uriFactory = null
87
    ) {
88 33
        $this->httpClient = $httpClient ?: HttpAsyncClientDiscovery::find();
89 33
        $this->uriFactory = $uriFactory ?: UriFactoryDiscovery::find();
90
91 33
        $this->setServers($servers);
92 30
        $this->setBaseUri($baseUri);
93 30
    }
94
95
    /**
96
     * Queue invalidation request.
97
     *
98
     * @param RequestInterface $invalidationRequest
99
     */
100 28
    public function invalidate(RequestInterface $invalidationRequest)
101
    {
102 28
        if (!$this->baseUri && !$invalidationRequest->getUri()->getHost()) {
103 1
            throw new MissingHostException(sprintf(
104 1
                'URI "%s" is not absolute. Either configure the base URI or invalidate with absolute URLs including the host.',
105 1
                (string) $invalidationRequest->getUri()
106 1
            ));
107
        }
108
109 27
        $signature = $this->getRequestSignature($invalidationRequest);
110
111 27
        if (isset($this->queue[$signature])) {
112 1
            return;
113
        }
114
115 27
        $this->queue[$signature] = $invalidationRequest;
116 27
    }
117
118
    /**
119
     * Send all pending invalidation requests and make sure the requests have terminated and gather exceptions.
120
     *
121
     * @return int The number of cache invalidations performed per caching server
122
     *
123
     * @throws ExceptionCollection If any errors occurred during flush
124
     */
125 28
    public function flush()
126
    {
127 28
        $queue = $this->queue;
128 28
        $this->queue = [];
129
        /** @var Promise[] $promises */
130 28
        $promises = [];
131
132 28
        $exceptions = new ExceptionCollection();
133
134 28
        foreach ($queue as $request) {
135 27
            foreach ($this->fanOut($request) as $proxyRequest) {
136 27
                $promises[] = $this->httpClient->sendAsyncRequest($proxyRequest);
137 27
            }
138 28
        }
139
140 28
        if (count($exceptions)) {
141
            throw new ExceptionCollection($exceptions);
0 ignored issues
show
Documentation introduced by
$exceptions is of type object<FOS\HttpCache\Exc...on\ExceptionCollection>, but the function expects a array.

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...
142
        }
143
144 28
        foreach ($promises as $promise) {
145
            try {
146 27
                $promise->wait();
147 27
            } catch (HttpException $exception) {
148
                $exceptions->add(ProxyResponseException::proxyResponse($exception->getResponse()));
149 1
            } catch (RequestException $exception) {
150 1
                $exceptions->add(ProxyUnreachableException::proxyUnreachable($exception));
151 1
            } catch (\Exception $exception) {
152
                $exceptions->add(new InvalidArgumentException($exception));
153
            }
154 28
        }
155
156 28
        if (count($exceptions)) {
157 1
            throw $exceptions;
158
        }
159
160 28
        return count($queue);
161
    }
162
163
    /**
164
     * Duplicate a request for each caching server.
165
     *
166
     * @param RequestInterface $request The request to duplicate for each configured server
167
     *
168
     * @return RequestInterface[]
169
     */
170 27
    private function fanOut(RequestInterface $request)
171
    {
172 27
        $requests = [];
173
174 27
        $uri = $request->getUri();
175
176
        // If a base URI is configured, try to make partial invalidation
177
        // requests complete.
178 27
        if ($this->baseUri) {
179 27
            if ($uri->getHost()) {
180
                // Absolute URI: does it already have a scheme?
181 4
                if (!$uri->getScheme() && $this->baseUri->getScheme() !== '') {
182
                    $uri = $uri->withScheme($this->baseUri->getScheme());
183
                }
184 4
            } else {
185
                // Relative URI
186 23
                if ($this->baseUri->getHost() !== '') {
187 23
                    $uri = $uri->withHost($this->baseUri->getHost());
188 23
                }
189
190 23
                if ($this->baseUri->getPort()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->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...
191 17
                    $uri = $uri->withPort($this->baseUri->getPort());
192 17
                }
193
194
                // Base path
195 23
                if ($this->baseUri->getPath() !== '') {
196 1
                    $path = $this->baseUri->getPath().'/'.ltrim($uri->getPath(), '/');
197 1
                    $uri = $uri->withPath($path);
198 1
                }
199
            }
200 27
        }
201
202
        // Close connections to make sure invalidation (PURGE/BAN) requests
203
        // will not interfere with content (GET) requests.
204 27
        $request = $request->withUri($uri)->withHeader('Connection', 'Close');
205
206
        // Create a request to each caching proxy server
207 27
        foreach ($this->servers as $server) {
208 27
            $requests[] = $request->withUri(
209
                $uri
210 27
                    ->withScheme($server->getScheme())
211 27
                    ->withHost($server->getHost())
212 27
                    ->withPort($server->getPort()),
213
                true    // Preserve application Host header
214 27
            );
215 27
        }
216
217 27
        return $requests;
218
    }
219
220
    /**
221
     * Set caching proxy server URI objects, validating them.
222
     *
223
     * @param string[] $servers Caching proxy proxy server hostnames or IP
224
     *                          addresses, including port if not port 80.
225
     *                          E.g. ['127.0.0.1:6081']
226
     *
227
     * @throws InvalidUrlException If server is invalid or contains URL
228
     *                             parts other than scheme, host, port
229
     */
230 33
    private function setServers(array $servers)
231
    {
232 33
        $this->servers = [];
233 33
        foreach ($servers as $server) {
234 33
            $this->servers[] = $this->filterUri($server, ['scheme', 'host', 'port']);
235 30
        }
236 30
    }
237
238
    /**
239
     * Set application base URI that will be prefixed to relative purge and
240
     * refresh requests, and validate it.
241
     *
242
     * @param string $uriString Your application’s base URI
243
     *
244
     * @throws InvalidUrlException If the base URI is not a valid URI
245
     */
246 30
    private function setBaseUri($uriString = null)
247
    {
248 30
        if (!$uriString) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uriString of type string|null is loosely compared to false; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
249 1
            $this->baseUri = null;
250
251 1
            return;
252
        }
253
254 29
        $this->baseUri = $this->filterUri($uriString);
255 29
    }
256
257
    /**
258
     * Filter a URL.
259
     *
260
     * Prefix the URL with "http://" if it has no scheme, then check the URL
261
     * for validity. You can specify what parts of the URL are allowed.
262
     *
263
     * @param string   $uriString
264
     * @param string[] $allowedParts Array of allowed URL parts (optional)
265
     *
266
     * @return UriInterface Filtered URI (with default scheme if there was no scheme)
267
     *
268
     * @throws InvalidUrlException If URL is invalid, the scheme is not http or
269
     *                             contains parts that are not expected
270
     */
271 33
    private function filterUri($uriString, array $allowedParts = [])
272
    {
273 33
        if (!is_string($uriString)) {
274
            throw new \InvalidArgumentException(sprintf(
275
                'URI parameter must be a string, %s given',
276
                gettype($uriString)
277
            ));
278
        }
279
280
        // Creating a PSR-7 URI without scheme (with parse_url) results in the
281
        // original hostname to be seen as path. So first add a scheme if none
282
        // is given.
283 33
        if (false === strpos($uriString, '://')) {
284 30
            $uriString = sprintf('%s://%s', 'http', $uriString);
285 30
        }
286
287
        try {
288 33
            $uri = $this->uriFactory->createUri($uriString);
289 33
        } catch (\InvalidArgumentException $e) {
290 1
            throw InvalidUrlException::invalidUrl($uriString);
291
        }
292
293 32
        if (!$uri->getScheme()) {
294 1
            throw InvalidUrlException::invalidUrl($uriString, 'empty scheme');
295
        }
296
297 31
        if (count($allowedParts) > 0) {
298 31
            $parts = parse_url((string) $uri);
299 31
            $diff = array_diff(array_keys($parts), $allowedParts);
300 31
            if (count($diff) > 0) {
301 1
                throw InvalidUrlException::invalidUrlParts($uriString, $allowedParts);
302
            }
303 30
        }
304
305 30
        return $uri;
306
    }
307
308
    /**
309
     * Build a request signature based on the request data. Unique for every different request, identical
310
     * for the same requests.
311
     *
312
     * This signature is used to avoid sending the same invalidation request twice.
313
     *
314
     * @param RequestInterface $request An invalidation request
315
     *
316
     * @return string A signature for this request
317
     */
318 27
    private function getRequestSignature(RequestInterface $request)
319
    {
320 27
        $headers = $request->getHeaders();
321 27
        ksort($headers);
322
323 27
        return sha1($request->getMethod()."\n".$request->getUri()."\n".var_export($headers, true));
324
    }
325
}
326