1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace WebServCo\Api; |
||
6 | |||
7 | use WebServCo\Api\Exceptions\ApiException; |
||
8 | use WebServCo\Api\JsonApi\Document; |
||
9 | use WebServCo\Framework\Interfaces\RequestInterface; |
||
10 | |||
11 | abstract class AbstractClientRequest |
||
12 | { |
||
13 | public const MSG_TPL_INVALID = 'Invalid data: %s'; |
||
14 | public const MSG_TPL_MAXIMUM_LENGTH = 'Maximum length exceeded: %s: %s'; |
||
15 | public const MSG_TPL_REQUIRED = 'Missing required data: %s'; |
||
16 | |||
17 | protected bool $allowMultipleDataObjects; |
||
18 | protected RequestInterface $request; |
||
19 | protected bool $processRequestData; |
||
20 | |||
21 | /** |
||
22 | * Request data. |
||
23 | * |
||
24 | * @var array<mixed> |
||
25 | */ |
||
26 | protected array $requestData; |
||
27 | |||
28 | public function __construct(RequestInterface $request) |
||
29 | { |
||
30 | $this->allowMultipleDataObjects = false; |
||
31 | $this->request = $request; |
||
32 | $requestMethod = $this->request->getMethod(); |
||
33 | |||
34 | if (\WebServCo\Framework\Http\Method::POST !== $requestMethod) { |
||
35 | return; |
||
36 | } |
||
37 | $this->processRequestData = true; |
||
38 | try { |
||
39 | // @throws \JsonException |
||
40 | $this->requestData = \json_decode( |
||
41 | $this->request->getBody(), |
||
42 | true, // associative |
||
43 | 512, // depth |
||
44 | \JSON_THROW_ON_ERROR, // flags |
||
45 | ) |
||
46 | ?? []; |
||
47 | } catch (\JsonException $e) { |
||
48 | $this->throwInvalidException('root object'); |
||
49 | } |
||
50 | } |
||
51 | |||
52 | protected function throwInvalidException(string $item): void |
||
53 | { |
||
54 | throw new ApiException(\sprintf(self::MSG_TPL_INVALID, $item)); |
||
55 | } |
||
56 | |||
57 | protected function throwMaximumLengthException(string $item, int $maximumLength): void |
||
58 | { |
||
59 | throw new ApiException(\sprintf(self::MSG_TPL_MAXIMUM_LENGTH, $item, $maximumLength)); |
||
60 | } |
||
61 | |||
62 | protected function throwRequiredException(string $item): void |
||
63 | { |
||
64 | throw new ApiException(\sprintf(self::MSG_TPL_REQUIRED, $item)); |
||
65 | } |
||
66 | |||
67 | protected function verify(): bool |
||
68 | { |
||
69 | $this->verifyContentType(); |
||
70 | if ($this->processRequestData) { |
||
71 | $this->verifyRequestData(); |
||
72 | } |
||
73 | return true; |
||
74 | } |
||
75 | |||
76 | protected function verifyContentType(): bool |
||
77 | { |
||
78 | $contentType = $this->request->getContentType(); |
||
79 | $parts = \explode(';', (string) $contentType); |
||
80 | if (Document::CONTENT_TYPE !== $parts[0]) { |
||
81 | throw new \WebServCo\Framework\Exceptions\UnsupportedMediaTypeException( |
||
82 | \sprintf('Unsupported request content type: %s.', (string) $contentType), |
||
83 | ); |
||
84 | } |
||
85 | return true; |
||
86 | } |
||
87 | |||
88 | protected function verifyRequestData(): bool |
||
89 | { |
||
90 | if (!\is_array($this->requestData)) { |
||
91 | $this->throwInvalidException('root object'); |
||
92 | } |
||
93 | if (!$this->requestData) { // check if empty array, could also mean the json vas invalid |
||
0 ignored issues
–
show
|
|||
94 | $this->throwRequiredException('root object'); |
||
95 | } |
||
96 | foreach (['jsonapi', 'data'] as $item) { |
||
97 | if (isset($this->requestData[$item])) { |
||
98 | continue; |
||
99 | } |
||
100 | |||
101 | $this->throwRequiredException($item); |
||
102 | } |
||
103 | if (!isset($this->requestData['jsonapi']['version'])) { |
||
104 | $this->throwRequiredException('jsonapi.version'); |
||
105 | } |
||
106 | if (Document::VERSION !== $this->requestData['jsonapi']['version']) { |
||
107 | throw new ApiException( |
||
108 | \sprintf('Unsupported JSON API version: %s', $this->requestData['jsonapi']['version']), |
||
109 | ); |
||
110 | } |
||
111 | if (!\is_array($this->requestData['data'])) { |
||
112 | $this->throwInvalidException('data'); |
||
113 | } |
||
114 | $key = \key($this->requestData['data']); |
||
115 | if (0 === $key) { //multiple data objects |
||
116 | if (!$this->allowMultipleDataObjects) { |
||
117 | throw new ApiException('Multiple data objects not allowed for this endpoint'); |
||
118 | } |
||
119 | foreach ($this->requestData['data'] as $item) { |
||
120 | $this->verifyData($item); |
||
121 | } |
||
122 | } else { // single data object |
||
123 | $this->verifyData($this->requestData['data']); |
||
124 | } |
||
125 | $this->verifyMeta(); |
||
126 | |||
127 | return true; |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * @param array<string,mixed> $data |
||
132 | */ |
||
133 | protected function verifyData(array $data): bool |
||
134 | { |
||
135 | foreach (['type', 'attributes'] as $item) { |
||
136 | if (isset($data[$item])) { |
||
137 | continue; |
||
138 | } |
||
139 | |||
140 | $this->throwRequiredException(\sprintf('data.%s', $item)); |
||
141 | } |
||
142 | if (empty($data['type'])) { |
||
143 | $this->throwRequiredException('data.type'); |
||
144 | } |
||
145 | if (!\is_array($data['attributes'])) { |
||
146 | $this->throwInvalidException('data.attributes'); |
||
147 | } |
||
148 | |||
149 | return true; |
||
150 | } |
||
151 | |||
152 | protected function verifyMeta(): bool |
||
153 | { |
||
154 | if (isset($this->requestData['meta'])) { // meta is optional |
||
155 | if (!\is_array($this->requestData['meta'])) { |
||
156 | $this->throwInvalidException('meta'); |
||
157 | } |
||
158 | } |
||
159 | return true; |
||
160 | } |
||
161 | } |
||
162 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.