Passed
Pull Request — master (#50)
by Joao
01:52
created

AbstractRequester::withQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
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\InvalidRequestException;
7
use ByJG\ApiTools\Exception\NotMatchedException;
8
use ByJG\ApiTools\Exception\StatusCodeNotMatchedException;
9
use ByJG\Util\Psr7\MessageException;
10
use ByJG\Util\Psr7\Request;
11
use ByJG\Util\Psr7\Response;
12
use ByJG\Util\Uri;
13
use MintWare\Streams\MemoryStream;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Http\Message\ResponseInterface;
16
17
/**
18
 * Abstract baseclass for request handlers.
19
 *
20
 * The baseclass provides processing and verification of request and response.
21
 * It only delegates the actual message exchange to the derived class. For the
22
 * messages, it uses the PSR-7 implementation from Guzzle.
23
 *
24
 * This is an implementation of the Template Method Patttern
25
 * (https://en.wikipedia.org/wiki/Template_method_pattern).
26
 */
27
abstract class AbstractRequester
28
{
29
    /**
30
     * @var Schema
31
     */
32
    protected $schema = null;
33
34
    protected $statusExpected = 200;
35
    protected $assertHeader = [];
36
    protected $assertBody = [];
37
38
    /**
39
     * @var RequestInterface
40
     */
41
    protected $psr7Request;
42
43
    /**
44
     * AbstractRequester constructor.
45
     * @throws MessageException
46
     */
47
    public function __construct()
48
    {
49
        $this->withPsr7Request(Request::getInstance(new Uri("/"))->withMethod("get"));
50
    }
51
52
    /**
53
     * abstract function to be implemented by derived classes
54
     *
55
     * This function must be implemented by derived classes. It should process
56
     * the given request and return an according response.
57
     *
58
     * @param RequestInterface $request
59
     * @return ResponseInterface
60
     */
61
    abstract protected function handleRequest(RequestInterface $request);
62
63
    /**
64
     * @param Schema $schema
65
     * @return $this
66
     */
67
    public function withSchema($schema)
68
    {
69
        $this->schema = $schema;
70
71
        return $this;
72
    }
73
74
    /**
75
     * @return bool
76
     */
77
    public function hasSchema()
78
    {
79
        return !empty($this->schema);
80
    }
81
82
    /**
83
     * @param string $method
84
     * @return $this
85
     * @throws MessageException
86
     */
87
    public function withMethod($method)
88
    {
89
        $this->psr7Request->withMethod($method);
90
91
        return $this;
92
    }
93
94
    /**
95
     * @param string $path
96
     * @return $this
97
     */
98
    public function withPath($path)
99
    {
100
        $this->psr7Request->getUri()->withPath($path);
101
102
        return $this;
103
    }
104
105
    /**
106
     * @param array $requestHeader
107
     * @return $this
108
     */
109
    public function withRequestHeader($requestHeader)
110
    {
111
        foreach ((array)$requestHeader as $name => $value) {
112
            $this->psr7Request->withHeader($name, $value);
113
        }
114
115
        return $this;
116
    }
117
118
    /**
119
     * @param array $query
120
     * @return $this
121
     */
122
    public function withQuery($query = null)
123
    {
124
        if (is_null($query)) {
125
            $this->query = [];
0 ignored issues
show
Bug introduced by
The property query does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
126
            return $this;
127
        }
128
129
        $currentQuery = [];
130
        parse_str($this->psr7Request->getUri()->getQuery(), $currentQuery);
131
132
        $this->psr7Request->getUri()->withQuery(http_build_query(array_merge($currentQuery, $query)));
133
134
        return $this;
135
    }
136
137
    /**
138
     * @param null $requestBody
139
     * @return $this
140
     */
141
    public function withRequestBody($requestBody)
142
    {
143
        $contentType = $this->psr7Request->getHeaderLine("Content-Type");
144
        if (is_array($requestBody) && (empty($contentType) || strpos($contentType, "application/json") !== false)) {
145
            $requestBody = json_encode($requestBody);
146
        }
147
        $this->psr7Request->withBody(new MemoryStream($requestBody));
0 ignored issues
show
Bug introduced by
It seems like $requestBody can also be of type array; however, MintWare\Streams\MemoryStream::__construct() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
148
149
        return $this;
150
    }
151
152
    public function withPsr7Request(RequestInterface $requesInterface)
153
    {
154
        $this->psr7Request = clone $requesInterface;
155
        $this->psr7Request->withHeader("Accept", "application/json");
156
157
        return $this;
158
    }
159
160
    public function assertResponseCode($code)
161
    {
162
        $this->statusExpected = $code;
163
164
        return $this;
165
    }
166
167
    public function assertHeaderContains($header, $contains)
168
    {
169
        $this->assertHeader[$header] = $contains;
170
171
        return $this;
172
    }
173
174
    public function assertBodyContains($contains)
175
    {
176
        $this->assertBody[] = $contains;
177
178
        return $this;
179
    }
180
181
    /**
182
     * @return Response|ResponseInterface
183
     * @throws Exception\DefinitionNotFoundException
184
     * @throws Exception\GenericSwaggerException
185
     * @throws Exception\HttpMethodNotFoundException
186
     * @throws Exception\InvalidDefinitionException
187
     * @throws Exception\PathNotFoundException
188
     * @throws NotMatchedException
189
     * @throws StatusCodeNotMatchedException
190
     * @throws MessageException
191
     * @throws InvalidRequestException
192
     */
193
    public function send()
194
    {
195
        // Process URI based on the OpenAPI schema
196
        $uriSchema = new Uri($this->schema->getServerUrl());
197
198
        $this->psr7Request->getUri()
199
            ->withScheme($uriSchema->getScheme())
200
            ->withHost($uriSchema->getHost())
201
            ->withPort($uriSchema->getPort())
202
            ->withPath($uriSchema->getPath() . $this->psr7Request->getUri()->getPath());
203
204
        if (!preg_match("~^{$this->schema->getBasePath()}~",  $this->psr7Request->getUri()->getPath())) {
205
            $this->psr7Request->getUri()->withPath($this->schema->getBasePath() . $this->psr7Request->getUri()->getPath());
206
        }
207
208
        // Prepare Body to Match Against Specification
209
        $requestBody = $this->psr7Request->getBody();
210
        if (!empty($requestBody)) {
211
            $requestBody = $requestBody->getContents();
212
213
            $contentType = $this->psr7Request->getHeaderLine("content-type");
214
            if (empty($contentType) || strpos($contentType, "application/json") !== false) {
215
                $requestBody = json_decode($requestBody, true);
216
            } elseif (strpos($contentType, "multipart/") !== false) {
217
                $requestBody = $this->parseMultiPartForm($contentType, $requestBody);
218
            } else {
219
                throw new InvalidRequestException("Cannot handle Content Type '{$contentType}'");
220
            }
221
        }
222
223
        // Check if the body is the expected before request
224
        $bodyRequestDef = $this->schema->getRequestParameters($this->psr7Request->getUri()->getPath(), $this->psr7Request->getMethod());
225
        $bodyRequestDef->match($requestBody);
226
227
        // Handle Request
228
        $response = $this->handleRequest($this->psr7Request);
229
        $responseHeader = $response->getHeaders();
230
        $responseBodyStr = (string) $response->getBody();
231
        $responseBody = json_decode($responseBodyStr, true);
232
        $statusReturned = $response->getStatusCode();
233
234
        // Assert results
235
        if ($this->statusExpected != $statusReturned) {
236
            throw new StatusCodeNotMatchedException(
237
                "Status code not matched: Expected {$this->statusExpected}, got {$statusReturned}",
238
                $responseBody
239
            );
240
        }
241
242
        $bodyResponseDef = $this->schema->getResponseParameters(
243
            $this->psr7Request->getUri()->getPath(),
244
            $this->psr7Request->getMethod(),
245
            $this->statusExpected
246
        );
247
        $bodyResponseDef->match($responseBody);
248
249
        foreach ($this->assertHeader as $key => $value) {
250
            if (!isset($responseHeader[$key]) || strpos($responseHeader[$key][0], $value) === false) {
251
                throw new NotMatchedException(
252
                    "Does not exists header '$key' with value '$value'",
253
                    $responseHeader
254
                );
255
            }
256
        }
257
258
        if (!empty($responseBodyStr)) {
259
            foreach ($this->assertBody as $item) {
260
                if (strpos($responseBodyStr, $item) === false) {
261
                    throw new NotMatchedException("Body does not contain '{$item}'");
262
                }
263
            }
264
        }
265
266
        return $response;
267
    }
268
269
    protected function parseMultiPartForm($contentType, $body)
270
    {
271
        $matchRequest = [];
272
273
        if (empty($contentType) || strpos($contentType, "multipart/") === false) {
274
            return null;
275
        }
276
277
        $matches = [];
278
279
        preg_match('/boundary=(.*)$/', $contentType, $matches);
280
        $boundary = $matches[1];
281
282
        // split content by boundary and get rid of last -- element
283
        $blocks = preg_split("/-+$boundary/", $body);
284
        array_pop($blocks);
285
286
        // loop data blocks
287
        foreach ($blocks as $id => $block) {
288
            if (empty($block))
289
                continue;
290
291
            if (strpos($block, 'application/octet-stream') !== false) {
292
                preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
293
            } else {
294
                preg_match('/\bname=\"([^\"]*)\"\s*;.*?[\n|\r]+([^\n\r].*)?[\r|\n]$/s', $block, $matches);
295
            }
296
            $matchRequest[$matches[1]] = $matches[2];
297
        }
298
299
        return $matchRequest;
300
    }
301
}
302