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

AbstractRequester::parseMultiPartForm()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.7857
c 0
b 0
f 0
cc 6
nc 5
nop 2
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 = $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
        $uri = $this->psr7Request->getUri()->withPath($path);
101
        $this->psr7Request = $this->psr7Request->withUri($uri);
102
103
        return $this;
104
    }
105
106
    /**
107
     * @param array $requestHeader
108
     * @return $this
109
     */
110
    public function withRequestHeader($requestHeader)
111
    {
112
        foreach ((array)$requestHeader as $name => $value) {
113
            $this->psr7Request = $this->psr7Request->withHeader($name, $value);
114
        }
115
116
        return $this;
117
    }
118
119
    /**
120
     * @param array $query
121
     * @return $this
122
     */
123
    public function withQuery($query = null)
124
    {
125
        if (is_null($query)) {
126
            $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...
127
            return $this;
128
        }
129
130
        $currentQuery = [];
131
        $uri = $this->psr7Request->getUri();
132
        parse_str($uri->getQuery(), $currentQuery);
133
134
        $uri = $uri->withQuery(http_build_query(array_merge($currentQuery, $query)));
135
        $this->psr7Request = $this->psr7Request->withUri($uri);
136
137
        return $this;
138
    }
139
140
    /**
141
     * @param null $requestBody
142
     * @return $this
143
     */
144
    public function withRequestBody($requestBody)
145
    {
146
        $contentType = $this->psr7Request->getHeaderLine("Content-Type");
147
        if (is_array($requestBody) && (empty($contentType) || strpos($contentType, "application/json") !== false)) {
148
            $requestBody = json_encode($requestBody);
149
        }
150
        $this->psr7Request = $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...
151
152
        return $this;
153
    }
154
155
    public function withPsr7Request(RequestInterface $requesInterface)
156
    {
157
        $this->psr7Request = clone $requesInterface;
158
        $this->psr7Request = $this->psr7Request->withHeader("Accept", "application/json");
159
160
        return $this;
161
    }
162
163
    public function assertResponseCode($code)
164
    {
165
        $this->statusExpected = $code;
166
167
        return $this;
168
    }
169
170
    public function assertHeaderContains($header, $contains)
171
    {
172
        $this->assertHeader[$header] = $contains;
173
174
        return $this;
175
    }
176
177
    public function assertBodyContains($contains)
178
    {
179
        $this->assertBody[] = $contains;
180
181
        return $this;
182
    }
183
184
    /**
185
     * @return Response|ResponseInterface
186
     * @throws Exception\DefinitionNotFoundException
187
     * @throws Exception\GenericSwaggerException
188
     * @throws Exception\HttpMethodNotFoundException
189
     * @throws Exception\InvalidDefinitionException
190
     * @throws Exception\PathNotFoundException
191
     * @throws NotMatchedException
192
     * @throws StatusCodeNotMatchedException
193
     * @throws MessageException
194
     * @throws InvalidRequestException
195
     */
196
    public function send()
197
    {
198
        // Process URI based on the OpenAPI schema
199
        $uriSchema = new Uri($this->schema->getServerUrl());
200
201
        $uri = $this->psr7Request->getUri()
202
            ->withScheme($uriSchema->getScheme())
203
            ->withHost($uriSchema->getHost())
204
            ->withPort($uriSchema->getPort())
205
            ->withPath($uriSchema->getPath() . $this->psr7Request->getUri()->getPath());
206
207
        if (!preg_match("~^{$this->schema->getBasePath()}~",  $uri->getPath())) {
208
            $uri = $uri->withPath($this->schema->getBasePath() . $uri->getPath());
209
        }
210
211
        $this->psr7Request = $this->psr7Request->withUri($uri);
212
213
        // Prepare Body to Match Against Specification
214
        $requestBody = $this->psr7Request->getBody();
215
        if (!empty($requestBody)) {
216
            $requestBody = $requestBody->getContents();
217
218
            $contentType = $this->psr7Request->getHeaderLine("content-type");
219
            if (empty($contentType) || strpos($contentType, "application/json") !== false) {
220
                $requestBody = json_decode($requestBody, true);
221
            } elseif (strpos($contentType, "multipart/") !== false) {
222
                $requestBody = $this->parseMultiPartForm($contentType, $requestBody);
223
            } else {
224
                throw new InvalidRequestException("Cannot handle Content Type '{$contentType}'");
225
            }
226
        }
227
228
        // Check if the body is the expected before request
229
        $bodyRequestDef = $this->schema->getRequestParameters($this->psr7Request->getUri()->getPath(), $this->psr7Request->getMethod());
230
        $bodyRequestDef->match($requestBody);
231
232
        // Handle Request
233
        $response = $this->handleRequest($this->psr7Request);
234
        $responseHeader = $response->getHeaders();
235
        $responseBodyStr = (string) $response->getBody();
236
        $responseBody = json_decode($responseBodyStr, true);
237
        $statusReturned = $response->getStatusCode();
238
239
        // Assert results
240
        if ($this->statusExpected != $statusReturned) {
241
            throw new StatusCodeNotMatchedException(
242
                "Status code not matched: Expected {$this->statusExpected}, got {$statusReturned}",
243
                $responseBody
244
            );
245
        }
246
247
        $bodyResponseDef = $this->schema->getResponseParameters(
248
            $this->psr7Request->getUri()->getPath(),
249
            $this->psr7Request->getMethod(),
250
            $this->statusExpected
251
        );
252
        $bodyResponseDef->match($responseBody);
253
254
        foreach ($this->assertHeader as $key => $value) {
255
            if (!isset($responseHeader[$key]) || strpos($responseHeader[$key][0], $value) === false) {
256
                throw new NotMatchedException(
257
                    "Does not exists header '$key' with value '$value'",
258
                    $responseHeader
259
                );
260
            }
261
        }
262
263
        if (!empty($responseBodyStr)) {
264
            foreach ($this->assertBody as $item) {
265
                if (strpos($responseBodyStr, $item) === false) {
266
                    throw new NotMatchedException("Body does not contain '{$item}'");
267
                }
268
            }
269
        }
270
271
        return $response;
272
    }
273
274
    protected function parseMultiPartForm($contentType, $body)
275
    {
276
        $matchRequest = [];
277
278
        if (empty($contentType) || strpos($contentType, "multipart/") === false) {
279
            return null;
280
        }
281
282
        $matches = [];
283
284
        preg_match('/boundary=(.*)$/', $contentType, $matches);
285
        $boundary = $matches[1];
286
287
        // split content by boundary and get rid of last -- element
288
        $blocks = preg_split("/-+$boundary/", $body);
289
        array_pop($blocks);
290
291
        // loop data blocks
292
        foreach ($blocks as $id => $block) {
293
            if (empty($block))
294
                continue;
295
296
            if (strpos($block, 'application/octet-stream') !== false) {
297
                preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
298
            } else {
299
                preg_match('/\bname=\"([^\"]*)\"\s*;.*?[\n|\r]+([^\n\r].*)?[\r|\n]$/s', $block, $matches);
300
            }
301
            $matchRequest[$matches[1]] = $matches[2];
302
        }
303
304
        return $matchRequest;
305
    }
306
}
307