Completed
Pull Request — master (#298)
by Jonas
05:25
created

HttpAdapter   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 92.78%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 32
c 4
b 1
f 0
lcom 1
cbo 11
dl 0
loc 271
ccs 90
cts 97
cp 0.9278
rs 9.6

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A invalidate() 0 10 2
D flush() 0 37 9
A setServers() 0 7 2
A setBaseUri() 0 10 2
B filterUri() 0 29 6
A getRequestSignature() 0 7 1
C fanOut() 0 49 9
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\ProxyResponseException;
18
use FOS\HttpCache\Exception\ProxyUnreachableException;
19
use Http\Client\Exception\HttpException;
20
use Http\Client\Exception\RequestException;
21
use Http\Client\HttpAsyncClient;
22
use Http\Message\UriFactory;
23
use Http\Promise\Promise;
24
use Psr\Http\Message\RequestInterface;
25
use Psr\Http\Message\UriInterface;
26
27
/**
28
 * An adapter to work with the Httplug asynchronous client.
29
 *
30
 * @author David Buchmann <[email protected]>
31
 */
32
class HttpAdapter
33
{
34
    /**
35
     * @var HttpAsyncClient
36
     */
37
    private $httpClient;
38
39
    /**
40
     * @var UriFactory
41
     */
42
    private $uriFactory;
43
44
    /**
45
     * Queued requests.
46
     *
47
     * @var RequestInterface[]
48
     */
49
    private $queue = [];
50
51
    /**
52
     * Caching proxy server host names or IP addresses.
53
     *
54
     * @var UriInterface[]
55
     */
56
    private $servers;
57
58
    /**
59
     * Application host name and optional base URL.
60
     *
61
     * @var UriInterface
62
     */
63
    private $baseUri;
64
65
    /**
66
     * @param string[]        $servers    Caching proxy server hostnames or IP
67
     *                                    addresses, including port if not port 80.
68
     *                                    E.g. ['127.0.0.1:6081']
69
     * @param string          $baseUri    Default application hostname, optionally
70
     *                                    including base URL, for purge and refresh
71
     *                                    requests (optional). This is required if
72
     *                                    you purge and refresh paths instead of
73
     *                                    absolute URLs.
74
     * @param HttpAsyncClient $httpClient
75
     * @param UriFactory      $uriFactory
76
     */
77 32
    public function __construct(array $servers, $baseUri, HttpAsyncClient $httpClient, UriFactory $uriFactory)
78
    {
79 32
        $this->httpClient = $httpClient;
80 32
        $this->uriFactory = $uriFactory;
81
82 32
        $this->setServers($servers);
83 29
        $this->setBaseUri($baseUri);
84 29
    }
85
86
    /**
87
     * Queue invalidation request.
88
     *
89
     * @param RequestInterface $invalidationRequest
90
     */
91 21
    public function invalidate(RequestInterface $invalidationRequest)
92
    {
93 21
        $signature = $this->getRequestSignature($invalidationRequest);
94
95 21
        if (isset($this->queue[$signature])) {
96 1
            return;
97
        }
98
99 21
        $this->queue[$signature] = $invalidationRequest;
100 21
    }
101
102
    /**
103
     * Send all pending invalidation requests and make sure the requests have terminated and gather exceptions.
104
     *
105
     * @return int The number of cache invalidations performed per caching server.
106
     *
107
     * @throws ExceptionCollection If any errors occurred during flush.
108
     */
109 22
    public function flush()
110
    {
111 22
        $queue = $this->queue;
112 22
        $this->queue = [];
113
        /** @var Promise[] $promises */
114 22
        $promises = [];
115
116 22
        $exceptions = new ExceptionCollection();
117
118 22
        foreach ($queue as $request) {
119 21
            foreach ($this->fanOut($request) as $proxyRequest) {
120 21
                $promises[] = $this->httpClient->sendAsyncRequest($proxyRequest);
121 21
            }
122 22
        }
123
124 22
        if (count($exceptions)) {
125
            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...
126
        }
127
128 22
        foreach ($promises as $promise) {
129
            try {
130 21
                $promise->wait();
131 21
            } catch (HttpException $exception) {
132
                $exceptions->add(ProxyResponseException::proxyResponse($exception->getResponse()));
133 1
            } catch (RequestException $exception) {
134 1
                $exceptions->add(ProxyUnreachableException::proxyUnreachable($exception));
135 1
            } catch (\Exception $exception) {
136
                $exceptions->add(new InvalidArgumentException($exception));
137
            }
138 22
        }
139
140 22
        if (count($exceptions)) {
141 1
            throw $exceptions;
142
        }
143
144 22
        return count($queue);
145
    }
146
147
    /**
148
     * Duplicate a request for each caching server.
149
     *
150
     * @param RequestInterface $request The request to duplicate for each configured server
151
     *
152
     * @return RequestInterface[]
153
     */
154 21
    private function fanOut(RequestInterface $request)
155
    {
156 21
        $requests = [];
157
158 21
        $uri = $request->getUri();
159
160
        // If a base URI is configured, try to make partial invalidation
161
        // requests complete.
162 21
        if ($this->baseUri) {
163 18
            if ($uri->getHost()) {
164
                // Absolute URI: does it already have a scheme?
165
                if (!$uri->getScheme() && $this->baseUri->getScheme() !== '') {
166
                    $uri = $uri->withScheme($this->baseUri->getScheme());
167
                }
168
            } else {
169
                // Relative URI
170 18
                if ($this->baseUri->getHost() !== '') {
171 18
                    $uri = $uri->withHost($this->baseUri->getHost());
172 18
                }
173
174 18
                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...
175 2
                    $uri = $uri->withPort($this->baseUri->getPort());
176 2
                }
177
178
                // Base path
179 18
                if ($this->baseUri->getPath() !== '') {
180 1
                    $path = $this->baseUri->getPath().'/'.ltrim($uri->getPath(), '/');
181 1
                    $uri = $uri->withPath($path);
182 1
                }
183
            }
184 18
        }
185
186
        // Close connections to make sure invalidation (PURGE/BAN) requests
187
        // will not interfere with content (GET) requests.
188 21
        $request = $request->withUri($uri)->withHeader('Connection', 'Close');
189
190
        // Create a request to each caching proxy server
191 21
        foreach ($this->servers as $server) {
192 21
            $requests[] = $request->withUri(
193
                $uri
194 21
                    ->withScheme($server->getScheme())
195 21
                    ->withHost($server->getHost())
196 21
                    ->withPort($server->getPort()),
197
                true    // Preserve application Host header
198 21
            );
199 21
        }
200
201 21
        return $requests;
202
    }
203
204
    /**
205
     * Set caching proxy server URI objects, validating them.
206
     *
207
     * @param string[] $servers Caching proxy proxy server hostnames or IP
208
     *                          addresses, including port if not port 80.
209
     *                          E.g. ['127.0.0.1:6081']
210
     *
211
     * @throws InvalidUrlException If server is invalid or contains URL
212
     *                             parts other than scheme, host, port
213
     */
214 32
    private function setServers(array $servers)
215
    {
216 32
        $this->servers = [];
217 32
        foreach ($servers as $server) {
218 32
            $this->servers[] = $this->filterUri($server, ['scheme', 'host', 'port']);
219 29
        }
220 29
    }
221
222
    /**
223
     * Set application base URI that will be prefixed to relative purge and
224
     * refresh requests, and validate it.
225
     *
226
     * @param string $uriString Your application’s base URI
227
     *
228
     * @throws InvalidUrlException If the base URI is not a valid URI.
229
     */
230 29
    private function setBaseUri($uriString = null)
231
    {
232 29
        if (null === $uriString) {
233 7
            $this->baseUri = null;
234
235 7
            return;
236
        }
237
238 22
        $this->baseUri = $this->filterUri($uriString);
239 22
    }
240
241
    /**
242
     * Filter a URL.
243
     *
244
     * Prefix the URL with "http://" if it has no scheme, then check the URL
245
     * for validity. You can specify what parts of the URL are allowed.
246
     *
247
     * @param string   $uriString
248
     * @param string[] $allowedParts Array of allowed URL parts (optional)
249
     *
250
     * @return UriInterface Filtered URI (with default scheme if there was no scheme)
251
     *
252
     * @throws InvalidUrlException If URL is invalid, the scheme is not http or
253
     *                             contains parts that are not expected.
254
     */
255 32
    private function filterUri($uriString, array $allowedParts = [])
256
    {
257
        // Creating a PSR-7 URI without scheme (with parse_url) results in the
258
        // original hostname to be seen as path. So first add a scheme if none
259
        // is given.
260 32
        if (false === strpos($uriString, '://')) {
261 27
            $uriString = sprintf('%s://%s', 'http', $uriString);
262 27
        }
263
264
        try {
265 32
            $uri = $this->uriFactory->createUri($uriString);
266 32
        } catch (\InvalidArgumentException $e) {
267 1
            throw InvalidUrlException::invalidUrl($uriString);
268
        }
269
270 31
        if (!$uri->getScheme()) {
271 1
            throw InvalidUrlException::invalidUrl($uriString, 'empty scheme');
272
        }
273
274 30
        if (count($allowedParts) > 0) {
275 30
            $parts = parse_url((string) $uri);
276 30
            $diff = array_diff(array_keys($parts), $allowedParts);
277 30
            if (count($diff) > 0) {
278 1
                throw InvalidUrlException::invalidUrlParts($uriString, $allowedParts);
279
            }
280 29
        }
281
282 29
        return $uri;
283
    }
284
285
    /**
286
     * Build a request signature based on the request data. Unique for every different request, identical
287
     * for the same requests.
288
     *
289
     * This signature is used to avoid sending the same invalidation request twice.
290
     *
291
     * @param RequestInterface $request An invalidation request.
292
     *
293
     * @return string A signature for this request.
294
     */
295 21
    private function getRequestSignature(RequestInterface $request)
296
    {
297 21
        $headers = $request->getHeaders();
298 21
        ksort($headers);
299
300 21
        return md5($request->getMethod()."\n".$request->getUri()."\n".var_export($headers, true));
301
    }
302
}
303