Passed
Pull Request — master (#85)
by
unknown
01:43
created

AbstractRequester::withPsr7Request()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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