Completed
Pull Request — master (#271)
by
unknown
13:29
created

HttpAdapter::filterUri()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

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