AbstractRequester::withRequestHeader()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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