Completed
Push — master ( 61c496...c60666 )
by David
05:00
created

HttpAdapter   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 94.9%

Importance

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