1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace BEAR\Resource; |
||
6 | |||
7 | use ArrayAccess; |
||
8 | use ArrayIterator; |
||
9 | use BEAR\Resource\Exception\MethodException; |
||
10 | use BEAR\Resource\Exception\OutOfBoundsException; |
||
11 | use IteratorAggregate; |
||
12 | use JsonSerializable; |
||
13 | use LogicException; |
||
14 | use Override; |
||
15 | use ReturnTypeWillChange; |
||
16 | use Serializable; |
||
17 | use Stringable; |
||
18 | use Throwable; |
||
19 | |||
20 | use function array_key_exists; |
||
21 | use function array_merge; |
||
22 | use function assert; |
||
23 | use function in_array; |
||
24 | use function is_array; |
||
25 | use function md5; |
||
26 | use function serialize; |
||
27 | use function strtolower; |
||
28 | use function trigger_error; |
||
29 | |||
30 | use const E_USER_ERROR; |
||
31 | use const PHP_EOL; |
||
32 | |||
33 | /** |
||
34 | * @property int $code |
||
35 | * @property array $headers |
||
36 | * @property mixed $body |
||
37 | * @property string $view |
||
38 | * @phpstan-implements IteratorAggregate<string, mixed> |
||
39 | * @phpstan-implements ArrayAccess<string, mixed> |
||
40 | * @psalm-suppress PropertyNotSetInConstructor |
||
41 | * @psalm-import-type Query from Types |
||
42 | */ |
||
43 | abstract class AbstractRequest implements RequestInterface, ArrayAccess, IteratorAggregate, Serializable, JsonSerializable, Stringable |
||
44 | { |
||
45 | /** |
||
46 | * URI |
||
47 | * |
||
48 | * @var string |
||
49 | */ |
||
50 | public $uri; |
||
51 | |||
52 | /** |
||
53 | * Method |
||
54 | * |
||
55 | * @var string |
||
56 | */ |
||
57 | public $method = ''; |
||
58 | |||
59 | /** |
||
60 | * Options |
||
61 | * |
||
62 | * @var array<mixed> |
||
63 | */ |
||
64 | public $options = []; |
||
65 | |||
66 | /** |
||
67 | * Request option (eager or lazy) |
||
68 | * |
||
69 | * @var 'eager'|'lazy' |
||
70 | */ |
||
71 | public $in = 'lazy'; |
||
72 | |||
73 | /** |
||
74 | * Request Result |
||
75 | * |
||
76 | * @var ?ResourceObject |
||
77 | */ |
||
78 | protected $result; |
||
79 | |||
80 | /** |
||
81 | * @param Query $query |
||
82 | * @param list<LinkType> $links |
||
83 | * |
||
84 | * @throws MethodException |
||
85 | */ |
||
86 | public function __construct( |
||
87 | protected InvokerInterface $invoker, |
||
88 | public ResourceObject $resourceObject, |
||
89 | string $method = Request::GET, |
||
90 | public array $query = [], |
||
91 | // phpcs:ignore SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingAnyTypeHint |
||
92 | public array $links = [], |
||
93 | private readonly LinkerInterface|null $linker = null, |
||
94 | ) { |
||
95 | if (! in_array(strtolower($method), ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'], true)) { |
||
96 | throw new MethodException($method, 400); |
||
97 | } |
||
98 | |||
99 | $this->method = $method; |
||
100 | } |
||
101 | |||
102 | /** @psalm-suppress UnevaluatedCode */ |
||
103 | #[Override] |
||
104 | public function __toString(): string |
||
105 | { |
||
106 | try { |
||
107 | $this->invoke(); |
||
108 | |||
109 | return (string) $this->result; |
||
110 | } catch (Throwable $e) { |
||
111 | trigger_error($e->getMessage() . PHP_EOL . $e->getTraceAsString(), E_USER_ERROR); |
||
112 | |||
113 | /** @noinspection PhpUnreachableStatementInspection */ |
||
114 | return ''; // @phpstan-ignore-line |
||
115 | } |
||
116 | } |
||
117 | |||
118 | /** |
||
119 | * {@inheritDoc} |
||
120 | * |
||
121 | * @param Query $query |
||
122 | */ |
||
123 | #[Override] |
||
124 | public function __invoke(array|null $query = null): ResourceObject |
||
125 | { |
||
126 | if (is_array($query)) { |
||
127 | $this->query = array_merge($this->query, $query); |
||
128 | } |
||
129 | |||
130 | $this->resourceObject->uri->query = $this->query; |
||
131 | if ($this->links && $this->linker instanceof LinkerInterface) { |
||
0 ignored issues
–
show
|
|||
132 | return $this->linker->invoke($this); |
||
133 | } |
||
134 | |||
135 | return clone $this->invoker->invoke($this); |
||
136 | } |
||
137 | |||
138 | /** |
||
139 | * {@inheritDoc} |
||
140 | * |
||
141 | * @return mixed |
||
142 | * |
||
143 | * @noinspection MagicMethodsValidityInspection |
||
144 | */ |
||
145 | public function __get(string $name) |
||
146 | { |
||
147 | $this->result = $this->invoke(); |
||
148 | |||
149 | return $this->result->{$name}; |
||
150 | } |
||
151 | |||
152 | /** |
||
153 | * {@inheritDoc} |
||
154 | * |
||
155 | * @throws OutOfBoundsException |
||
156 | */ |
||
157 | #[Override] |
||
158 | #[ReturnTypeWillChange] |
||
159 | public function offsetSet($offset, $value) |
||
160 | { |
||
161 | throw new OutOfBoundsException(__METHOD__ . ' is unavailable.', 400); |
||
162 | } |
||
163 | |||
164 | /** |
||
165 | * {@inheritDoc} |
||
166 | * |
||
167 | * @param string $offset |
||
168 | * |
||
169 | * @return never |
||
170 | * |
||
171 | * @throws OutOfBoundsException |
||
172 | */ |
||
173 | #[Override] |
||
174 | #[ReturnTypeWillChange] |
||
175 | public function offsetUnset($offset) |
||
176 | { |
||
177 | unset($offset); |
||
178 | |||
179 | throw new OutOfBoundsException(__METHOD__ . ' is unavailable.', 400); |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * {@inheritDoc} |
||
184 | */ |
||
185 | #[Override] |
||
186 | public function request() |
||
187 | { |
||
188 | if ($this->in === 'eager') { |
||
189 | $this->result = $this->invoke(); |
||
190 | |||
191 | return $this->result; |
||
192 | } |
||
193 | |||
194 | return $this; |
||
195 | } |
||
196 | |||
197 | /** |
||
198 | * {@inheritDoc} |
||
199 | * |
||
200 | * @param string $offset |
||
201 | * |
||
202 | * @return mixed |
||
203 | * |
||
204 | * @throws OutOfBoundsException |
||
205 | */ |
||
206 | #[Override] |
||
207 | #[ReturnTypeWillChange] |
||
208 | public function offsetGet($offset) |
||
209 | { |
||
210 | $this->invoke(); |
||
211 | assert($this->result instanceof ResourceObject); |
||
212 | if (! is_array($this->result->body) || ! array_key_exists($offset, $this->result->body)) { |
||
213 | throw new OutOfBoundsException("[{$offset}] for object[" . $this->result::class . ']', 400); |
||
214 | } |
||
215 | |||
216 | return $this->result->body[$offset]; |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * {@inheritDoc} |
||
221 | */ |
||
222 | #[Override] |
||
223 | #[ReturnTypeWillChange] |
||
224 | public function offsetExists($offset): bool |
||
225 | { |
||
226 | $this->invoke(); |
||
227 | assert($this->result instanceof ResourceObject); |
||
228 | |||
229 | return is_array($this->result->body) && array_key_exists($offset, $this->result->body); |
||
230 | } |
||
231 | |||
232 | /** |
||
233 | * Invoke resource request then return resource body iterator |
||
234 | * |
||
235 | * @psalm-return ArrayIterator |
||
236 | * @phpstan-return ArrayIterator<string, mixed> |
||
237 | */ |
||
238 | #[Override] |
||
239 | public function getIterator(): ArrayIterator |
||
240 | { |
||
241 | $this->invoke(); |
||
242 | assert($this->result instanceof ResourceObject); |
||
243 | |||
244 | return is_array($this->result->body) ? new ArrayIterator($this->result->body) : new ArrayIterator([]); |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * {@inheritDoc} |
||
249 | */ |
||
250 | #[Override] |
||
251 | public function hash(): string |
||
252 | { |
||
253 | return md5($this->resourceObject::class . $this->method . serialize($this->query) . serialize($this->links)); |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * {@inheritDoc} |
||
258 | * |
||
259 | * @return never |
||
260 | * |
||
261 | * @noinspection MagicMethodsValidityInspection |
||
262 | */ |
||
263 | public function __serialize() |
||
264 | { |
||
265 | throw new LogicException(__METHOD__ . ' not supported'); |
||
266 | } |
||
267 | |||
268 | /** |
||
269 | * @param array<mixed> $data |
||
270 | * |
||
271 | * @codeCoverageIgnore |
||
272 | */ |
||
273 | public function __unserialize(array $data): void |
||
274 | { |
||
275 | unset($data); |
||
276 | } |
||
277 | |||
278 | private function invoke(): ResourceObject |
||
279 | { |
||
280 | if ($this->result === null) { |
||
281 | /* @noinspection ImplicitMagicMethodCallInspection */ |
||
282 | $this->result = ($this)(); |
||
283 | } |
||
284 | |||
285 | return $this->result; |
||
286 | } |
||
287 | |||
288 | #[Override] |
||
289 | public function jsonSerialize(): ResourceObject |
||
290 | { |
||
291 | return $this->invoke(); |
||
292 | } |
||
293 | |||
294 | /** |
||
295 | * @return never |
||
296 | * |
||
297 | * @codeCoverageIgnore |
||
298 | * @psalm-suppress MethodSignatureMustProvideReturnType - method not supported |
||
299 | */ |
||
300 | #[Override] |
||
301 | public function serialize() |
||
302 | { |
||
303 | $this->__serialize(); |
||
304 | } |
||
305 | |||
306 | /** |
||
307 | * @param string $data |
||
308 | * |
||
309 | * @codeCoverageIgnore |
||
310 | * @psalm-suppress MethodSignatureMustProvideReturnType - method not supported |
||
311 | */ |
||
312 | #[Override] |
||
313 | public function unserialize($data) |
||
314 | { |
||
315 | unset($data); |
||
316 | } |
||
317 | } |
||
318 |
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.