Passed
Pull Request — master (#82)
by Joao
01:44
created

AbstractRequester   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Importance

Changes 8
Bugs 1 Features 0
Metric Value
wmc 43
eloc 112
c 8
b 1
f 0
dl 0
loc 301
rs 8.96

15 Methods

Rating   Name   Duplication   Size   Complexity  
A withPsr7Request() 0 5 1
A __construct() 0 3 1
A withMethod() 0 5 1
A withQuery() 0 17 2
A hasSchema() 0 3 1
A withRequestHeader() 0 7 2
A withPath() 0 6 1
A withSchema() 0 5 1
A withRequestBody() 0 9 4
A assertBodyContains() 0 5 1
F send() 0 89 19
A getPsr7Request() 0 3 1
A parseMultiPartForm() 0 31 6
A assertResponseCode() 0 5 1
A assertHeaderContains() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractRequester often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractRequester, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace ByJG\ApiTools;
4
5
use ByJG\ApiTools\Base\Schema;
6
use ByJG\ApiTools\Exception\DefinitionNotFoundException;
7
use ByJG\ApiTools\Exception\GenericSwaggerException;
8
use ByJG\ApiTools\Exception\HttpMethodNotFoundException;
9
use ByJG\ApiTools\Exception\InvalidDefinitionException;
10
use ByJG\ApiTools\Exception\InvalidRequestException;
11
use ByJG\ApiTools\Exception\NotMatchedException;
12
use ByJG\ApiTools\Exception\PathNotFoundException;
13
use ByJG\ApiTools\Exception\RequiredArgumentNotFound;
14
use ByJG\ApiTools\Exception\StatusCodeNotMatchedException;
15
use ByJG\Util\Uri;
16
use ByJG\WebRequest\Exception\MessageException;
17
use ByJG\WebRequest\Exception\RequestException;
18
use ByJG\WebRequest\Psr7\MemoryStream;
19
use ByJG\WebRequest\Psr7\Request;
20
use ByJG\XmlUtil\XmlDocument;
21
use Psr\Http\Message\RequestInterface;
22
use Psr\Http\Message\ResponseInterface;
23
24
/**
25
 * Abstract baseclass for request handlers.
26
 *
27
 * The baseclass provides processing and verification of request and response.
28
 * It only delegates the actual message exchange to the derived class. For the
29
 * messages, it uses the PHP PSR-7 implementation.
30
 *
31
 * This is an implementation of the Template Method Patttern
32
 * (https://en.wikipedia.org/wiki/Template_method_pattern).
33
 */
34
abstract class AbstractRequester
35
{
36
    /**
37
     * @var Schema|null
38
     */
39
    protected ?Schema $schema = null;
40
41
    protected int $statusExpected = 200;
42
    protected array $assertHeader = [];
43
    protected array $assertBody = [];
44
45
    /**
46
     * @var RequestInterface
47
     */
48
    protected RequestInterface $psr7Request;
49
50
    /**
51
     * AbstractRequester constructor.
52
     * @throws MessageException
53
     * @throws RequestException
54
     */
55
    public function __construct()
56
    {
57
        $this->withPsr7Request(Request::getInstance(new Uri("/"))->withMethod("get"));
58
    }
59
60
    /**
61
     * abstract function to be implemented by derived classes
62
     *
63
     * This function must be implemented by derived classes. It should process
64
     * the given request and return an according response.
65
     *
66
     * @param RequestInterface $request
67
     * @return ResponseInterface
68
     */
69
    abstract protected function handleRequest(RequestInterface $request): ResponseInterface;
70
71
    /**
72
     * @param Schema $schema
73
     * @return $this
74
     */
75
    public function withSchema(Schema $schema): self
76
    {
77
        $this->schema = $schema;
78
79
        return $this;
80
    }
81
82
    /**
83
     * @return bool
84
     */
85
    public function hasSchema(): bool
86
    {
87
        return !empty($this->schema);
88
    }
89
90
    /**
91
     * @param string $method
92
     * @return $this
93
     */
94
    public function withMethod(string $method): self
95
    {
96
        $this->psr7Request = $this->psr7Request->withMethod($method);
97
98
        return $this;
99
    }
100
101
    /**
102
     * @param string $path
103
     * @return $this
104
     */
105
    public function withPath(string $path): self
106
    {
107
        $uri = $this->psr7Request->getUri()->withPath($path);
108
        $this->psr7Request = $this->psr7Request->withUri($uri);
109
110
        return $this;
111
    }
112
113
    /**
114
     * @param string|array $requestHeader
115
     * @return $this
116
     */
117
    public function withRequestHeader(string|array $requestHeader): self
118
    {
119
        foreach ((array)$requestHeader as $name => $value) {
120
            $this->psr7Request = $this->psr7Request->withHeader($name, $value);
121
        }
122
123
        return $this;
124
    }
125
126
    /**
127
     * @param array|null $query
128
     * @return $this
129
     */
130
    public function withQuery(array $query = null): self
131
    {
132
        $uri = $this->psr7Request->getUri();
133
134
        if (is_null($query)) {
135
            $uri = $uri->withQuery("");
136
            $this->psr7Request = $this->psr7Request->withUri($uri);
137
            return $this;
138
        }
139
140
        $currentQuery = [];
141
        parse_str($uri->getQuery(), $currentQuery);
142
143
        $uri = $uri->withQuery(http_build_query(array_merge($currentQuery, $query)));
144
        $this->psr7Request = $this->psr7Request->withUri($uri);
145
146
        return $this;
147
    }
148
149
    /**
150
     * @param mixed $requestBody
151
     * @return $this
152
     */
153
    public function withRequestBody(array|string $requestBody): self
154
    {
155
        $contentType = $this->psr7Request->getHeaderLine("Content-Type");
156
        if (is_array($requestBody) && (empty($contentType) || str_contains($contentType, "application/json"))) {
157
            $requestBody = json_encode($requestBody);
158
        }
159
        $this->psr7Request = $this->psr7Request->withBody(new MemoryStream($requestBody));
160
161
        return $this;
162
    }
163
164
    /**
165
     * @param RequestInterface $requestInterface
166
     * @return $this
167
     */
168
    public function withPsr7Request(RequestInterface $requestInterface): self
169
    {
170
        $this->psr7Request = $requestInterface->withHeader("Accept", "application/json");
171
172
        return $this;
173
    }
174
175
    public function getPsr7Request(): RequestInterface
176
    {
177
        return $this->psr7Request;
178
    }
179
180
    public function assertResponseCode(int $code): self
181
    {
182
        $this->statusExpected = $code;
183
184
        return $this;
185
    }
186
187
    public function assertHeaderContains(string $header, string $contains): self
188
    {
189
        $this->assertHeader[$header] = $contains;
190
191
        return $this;
192
    }
193
194
    public function assertBodyContains(string $contains): self
195
    {
196
        $this->assertBody[] = $contains;
197
198
        return $this;
199
    }
200
201
    /**
202
     * @return ResponseInterface
203
     * @throws DefinitionNotFoundException
204
     * @throws GenericSwaggerException
205
     * @throws HttpMethodNotFoundException
206
     * @throws InvalidDefinitionException
207
     * @throws InvalidRequestException
208
     * @throws NotMatchedException
209
     * @throws PathNotFoundException
210
     * @throws RequiredArgumentNotFound
211
     * @throws StatusCodeNotMatchedException
212
     */
213
    public function send(bool $matchQueryParams = true): ResponseInterface
214
    {
215
        // Process URI based on the OpenAPI schema
216
        $uriSchema = new Uri($this->schema->getServerUrl());
0 ignored issues
show
Bug introduced by
The method getServerUrl() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

216
        $uriSchema = new Uri($this->schema->/** @scrutinizer ignore-call */ getServerUrl());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
217
218
        if (empty($uriSchema->getScheme())) {
219
            $uriSchema = $uriSchema->withScheme($this->psr7Request->getUri()->getScheme());
220
        }
221
222
        if (empty($uriSchema->getHost())) {
223
            $uriSchema = $uriSchema->withHost($this->psr7Request->getUri()->getHost());
224
        }
225
226
        $uri = $this->psr7Request->getUri()
227
            ->withScheme($uriSchema->getScheme())
228
            ->withHost($uriSchema->getHost())
229
            ->withPort($uriSchema->getPort())
230
            ->withPath($uriSchema->getPath() . $this->psr7Request->getUri()->getPath());
231
232
        if (!preg_match("~^{$this->schema->getBasePath()}~",  $uri->getPath())) {
233
            $uri = $uri->withPath($this->schema->getBasePath() . $uri->getPath());
234
        }
235
236
        $this->psr7Request = $this->psr7Request->withUri($uri);
237
238
        // Prepare Body to Match Against Specification
239
        $rawBody = $this->psr7Request->getBody()->getContents();
240
        $isXmlBody = false;
241
        $requestBody = null;
242
        $contentType = $this->psr7Request->getHeaderLine("content-type");
243
        if (!empty($rawBody)) {
244
            if (str_contains($contentType, 'application/xml') || str_contains($contentType, 'text/xml')) {
245
                $isXmlBody = new XmlDocument($rawBody);
246
            } elseif (empty($contentType) || str_contains($contentType, "application/json")) {
247
                $requestBody = json_decode($rawBody, true);
248
            } elseif (str_contains($contentType, "multipart/")) {
249
                $requestBody = $this->parseMultiPartForm($contentType, $rawBody);
250
            } else {
251
                throw new InvalidRequestException("Cannot handle Content Type '$contentType'");
252
            }
253
254
        }
255
256
        // Check if the body is the expected before request
257
        if ($isXmlBody === false) {
258
            $bodyRequestDef = $this->schema->getRequestParameters($this->psr7Request->getUri()->getPath(), $this->psr7Request->getMethod(), $matchQueryParams ? $this->psr7Request->getUri()->getQuery() : null);
259
            $bodyRequestDef->match($requestBody);
260
        }
261
262
        // Handle Request
263
        $response = $this->handleRequest($this->psr7Request);
264
        $responseHeader = $response->getHeaders();
265
        $responseBodyStr = (string) $response->getBody();
266
        $responseBody = json_decode($responseBodyStr, true);
267
        $statusReturned = $response->getStatusCode();
268
269
        // Assert results
270
        if ($this->statusExpected != $statusReturned) {
271
            throw new StatusCodeNotMatchedException(
272
                "Status code not matched: Expected $this->statusExpected, got $statusReturned",
273
                $responseBody
274
            );
275
        }
276
277
        $bodyResponseDef = $this->schema->getResponseParameters(
278
            $this->psr7Request->getUri()->getPath(),
279
            $this->psr7Request->getMethod(),
280
            $this->statusExpected
281
        );
282
        $bodyResponseDef->match($responseBody);
283
284
        foreach ($this->assertHeader as $key => $value) {
285
            if (!isset($responseHeader[$key]) || !str_contains($responseHeader[$key][0], $value)) {
286
                throw new NotMatchedException(
287
                    "Does not exists header '$key' with value '$value'",
288
                    $responseHeader
289
                );
290
            }
291
        }
292
293
        if (!empty($responseBodyStr)) {
294
            foreach ($this->assertBody as $item) {
295
                if (!str_contains($responseBodyStr, $item)) {
296
                    throw new NotMatchedException("Body does not contain '$item'");
297
                }
298
            }
299
        }
300
301
        return $response;
302
    }
303
304
    protected function parseMultiPartForm(?string $contentType, string $body): array|null
305
    {
306
        $matchRequest = [];
307
308
        if (empty($contentType) || !str_contains($contentType, "multipart/")) {
309
            return null;
310
        }
311
312
        $matches = [];
313
314
        preg_match('/boundary=(.*)$/', $contentType, $matches);
315
        $boundary = $matches[1];
316
317
        // split content by boundary and get rid of last -- element
318
        $blocks = preg_split("/-+$boundary/", $body);
319
        array_pop($blocks);
320
321
        // loop data blocks
322
        foreach ($blocks as $block) {
323
            if (empty($block))
324
                continue;
325
326
            if (str_contains($block, 'application/octet-stream')) {
327
                preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
328
            } else {
329
                preg_match('/\bname=\"([^\"]*)\"\s*;.*?[\n|\r]+([^\n\r].*)?[\r|\n]$/s', $block, $matches);
330
            }
331
            $matchRequest[$matches[1]] = $matches[2];
332
        }
333
334
        return $matchRequest;
335
    }
336
}
337