Passed
Push — main ( 1a8771...d7bb24 )
by Andrey
11:52 queued 10:31
created

HttpBuilder::allowArrayable()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 3
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 3
rs 10
1
<?php
2
3
namespace Helldar\Support\Helpers;
4
5
use ArgumentCountError;
6
use Helldar\Support\Facades\Helpers\Arr as ArrFacade;
7
use Helldar\Support\Facades\Helpers\Http as HttpHelper;
8
use Helldar\Support\Facades\Helpers\Instance as InstanceHelper;
9
use Helldar\Support\Facades\Helpers\Str as StrFacade;
10
use Helldar\Support\Tools\HttpBuilderPrepare;
11
use RuntimeException;
12
13
/**
14
 * Based on code by Maksim (Ellrion) Platonov.
15
 *
16
 * @see https://gist.github.com/Ellrion/f51ba0d40ae1d62eeae44fd1adf7b704
17
 *
18
 * @method HttpBuilder setFragment(array|string $value)
19
 * @method HttpBuilder setHost(string $value)
20
 * @method HttpBuilder setPass(string $value)
21
 * @method HttpBuilder setPath(string $value)
22
 * @method HttpBuilder setPort(string $value)
23
 * @method HttpBuilder setQuery(array|string $value)
24
 * @method HttpBuilder setScheme(string $value)
25
 * @method HttpBuilder setUser(string $value)
26
 * @method HttpBuilder putQuery(string $key, $value)
27
 * @method HttpBuilder removeQuery(string $key)
28
 * @method string|null getFragment()
29
 * @method string|null getHost()
30
 * @method string|null getPass()
31
 * @method string|null getPath()
32
 * @method string|null getPort()
33
 * @method string|null getQuery()
34
 * @method string|null getScheme()
35
 * @method string|null getUser()
36
 */
37
final class HttpBuilder
38
{
39
    protected $parsed = [];
40
41
    protected $components = [
42
        PHP_URL_SCHEME   => 'scheme',
43
        PHP_URL_HOST     => 'host',
44
        PHP_URL_PORT     => 'port',
45
        PHP_URL_USER     => 'user',
46
        PHP_URL_PASS     => 'pass',
47
        PHP_URL_QUERY    => 'query',
48
        PHP_URL_PATH     => 'path',
49
        PHP_URL_FRAGMENT => 'fragment',
50
    ];
51
52
    protected $allow_put_remove = ['query'];
53
54
    protected $casts = [
55
        'query' => 'array',
56
    ];
57
58
    /**
59
     * Calling magic methods.
60
     *
61
     * @param  string  $method
62
     * @param  mixed  $args
63
     *
64
     * @return $this|string|null
65
     */
66 88
    public function __call(string $method, $args)
67
    {
68 88
        if ($this->isGetter($method) || $this->isSetter($method) || $this->isPutter($method) || $this->isRemover($method)) {
69 88
            $key = $this->parseKey($method);
70
71 88
            if (! $this->allowKey($key) || ! $this->allowArrayable($method, $key)) {
72 32
                throw new RuntimeException($method . ' method not defined.');
73
            }
74
75
            switch (true) {
76 56
                case $this->isGetter($method):
77 52
                    $this->validateArgumentsCount($method, $args, 0);
78
79 52
                    return $this->get($key);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type null; however, parameter $key of Helldar\Support\Helpers\HttpBuilder::get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

79
                    return $this->get(/** @scrutinizer ignore-type */ $key);
Loading history...
Bug Best Practice introduced by
The expression return $this->get($key) also could return the type array which is incompatible with the documented return type Helldar\Support\Helpers\HttpBuilder|null|string.
Loading history...
80
81 28
                case $this->isSetter($method):
82 24
                    $this->validateArgumentsCount($method, $args);
83
84 20
                    return $this->set($key, $args[0]);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type null; however, parameter $key of Helldar\Support\Helpers\HttpBuilder::set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

84
                    return $this->set(/** @scrutinizer ignore-type */ $key, $args[0]);
Loading history...
85
86 4
                case $this->isPutter($method):
87 2
                    $this->validateArgumentsCount($method, $args, 2);
88
89 2
                    return $this->put($key, $args[0], $args[1]);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type null; however, parameter $key of Helldar\Support\Helpers\HttpBuilder::put() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
                    return $this->put(/** @scrutinizer ignore-type */ $key, $args[0], $args[1]);
Loading history...
90
91 2
                case $this->isRemover($method):
92 2
                    $this->validateArgumentsCount($method, $args);
93
94 2
                    return $this->remove($key, $args[0]);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type null; however, parameter $key of Helldar\Support\Helpers\HttpBuilder::remove() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

94
                    return $this->remove(/** @scrutinizer ignore-type */ $key, $args[0]);
Loading history...
95
            }
96
        }
97
98
        throw new RuntimeException("Using an unknown method: \"{$method}\"");
99
    }
100
101
    /**
102
     * Gets the current instance of the object.
103
     *
104
     * @return $this
105
     */
106 8
    public function same(): self
107
    {
108 8
        return $this;
109
    }
110
111
    /**
112
     * Parse a URL.
113
     *
114
     * @param  string|null  $url
115
     * @param  int  $component
116
     *
117
     * @return $this
118
     */
119 36
    public function parse(?string $url, int $component = -1): self
120
    {
121 36
        if ($component === -1) {
122 32
            HttpHelper::validateUrl($url);
123
        }
124
125 30
        $component = $this->componentIndex($component);
126 30
        $key       = $this->componentKey($component);
127
128 30
        $component === -1 || empty($key)
129 26
            ? $this->parsed       = parse_url($url)
130 4
            : $this->parsed[$key] = parse_url($url, $component);
131
132 30
        $this->cast();
133
134 30
        return $this;
135
    }
136
137
    /**
138
     * Filling the builder with parsed data.
139
     *
140
     * @param  array  $parsed
141
     *
142
     * @return $this
143
     */
144 6
    public function raw(array $parsed): self
145
    {
146 6
        foreach ($parsed as $key => $value) {
147 6
            if (! $this->allowKey($key)) {
148 2
                throw new RuntimeException('Filling in the "' . $key . '" key is prohibited.');
149
            }
150
151 4
            $this->set($key, $value);
152
        }
153
154 4
        $this->cast();
155
156 4
        return $this;
157
    }
158
159
    /**
160
     * Compiles parameters to URL.
161
     *
162
     * @return string
163
     */
164 16
    public function compile(): string
165
    {
166 16
        return implode('', array_filter(array_map(function ($value) {
167 16
            return InstanceHelper::of($value, HttpBuilderPrepare::class) ? $value->get() : $value;
168 16
        }, $this->prepare())));
169
    }
170
171
    /**
172
     * Returns parsed data.
173
     *
174
     * @return null[]|string[]
175
     */
176 4
    public function toArray(): array
177
    {
178
        return [
179 4
            'scheme'   => $this->getScheme(),
180 4
            'host'     => $this->getHost(),
181 4
            'port'     => $this->getPort(),
182 4
            'user'     => $this->getUser(),
183 4
            'pass'     => $this->getPass(),
184 4
            'query'    => $this->getQuery(),
185 4
            'path'     => $this->getPath(),
186 4
            'fragment' => $this->getFragment(),
187
        ];
188
    }
189
190
    /**
191
     * Prepares data for compilation.
192
     *
193
     * @return array
194
     */
195 16
    protected function prepare(): array
196
    {
197
        return [
198 16
            HttpBuilderPrepare::make()->of($this->getScheme())->suffix('://'),
199 16
            HttpBuilderPrepare::make()->of($this->getUser()),
200 16
            HttpBuilderPrepare::make()->of($this->getPass())->prefix(':'),
201
202 16
            $this->getUser() || $this->getPass() ? '@' : '',
203
204 16
            HttpBuilderPrepare::make()->of($this->getHost()),
205 16
            HttpBuilderPrepare::make()->of($this->getPort())->prefix(':'),
206 16
            HttpBuilderPrepare::make()->of($this->getPath())->prefix('/'),
207 16
            HttpBuilderPrepare::make()->of($this->getQuery())->prefix('?'),
208 16
            HttpBuilderPrepare::make()->of($this->getFragment())->prefix('#'),
209
        ];
210
    }
211
212
    /**
213
     * Gets the index of the component.
214
     *
215
     * @param  int  $component
216
     *
217
     * @return int
218
     */
219 30
    protected function componentIndex(int $component = -1): int
220
    {
221 30
        return ArrFacade::getKey($this->components, $component, -1);
222
    }
223
224
    /**
225
     * Gets the key for the component.
226
     *
227
     * @param  int  $component
228
     *
229
     * @return string|null
230
     */
231 30
    protected function componentKey(int $component = -1): ?string
232
    {
233 30
        return ArrFacade::get($this->components, $component);
234
    }
235
236
    /**
237
     * Checks if calling the requested key is allowed.
238
     *
239
     * @param  string|null  $key
240
     *
241
     * @return bool
242
     */
243 90
    protected function allowKey(?string $key): bool
244
    {
245 90
        return in_array($key, $this->components);
246
    }
247
248 84
    protected function allowArrayable(?string $method, ?string $key): bool
249
    {
250 84
        if ($this->isPutter($method) || $this->isRemover($method)) {
0 ignored issues
show
Bug introduced by
It seems like $method can also be of type null; however, parameter $method of Helldar\Support\Helpers\HttpBuilder::isRemover() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
        if ($this->isPutter($method) || $this->isRemover(/** @scrutinizer ignore-type */ $method)) {
Loading history...
Bug introduced by
It seems like $method can also be of type null; however, parameter $method of Helldar\Support\Helpers\HttpBuilder::isPutter() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
        if ($this->isPutter(/** @scrutinizer ignore-type */ $method) || $this->isRemover($method)) {
Loading history...
251 32
            return in_array($key, $this->allow_put_remove);
252
        }
253
254 56
        return true;
255
    }
256
257
    /**
258
     * Checks if the method is a request for information.
259
     *
260
     * @param  string  $method
261
     *
262
     * @return bool
263
     */
264 88
    protected function isGetter(string $method): bool
265
    {
266 88
        return StrFacade::startsWith($method, 'get');
267
    }
268
269
    /**
270
     * Checks if the method is a request to fill information.
271
     *
272
     * @param  string  $method
273
     *
274
     * @return bool
275
     */
276 58
    protected function isSetter(string $method): bool
277
    {
278 58
        return StrFacade::startsWith($method, 'set');
279
    }
280
281
    /**
282
     * Checks if the method is a request to put value.
283
     *
284
     * @param  string  $method
285
     *
286
     * @return bool
287
     */
288 84
    protected function isPutter(string $method): bool
289
    {
290 84
        return StrFacade::startsWith($method, 'put');
291
    }
292
293
    /**
294
     * Checks if the method is a request to remove value.
295
     *
296
     * @param  string  $method
297
     *
298
     * @return bool
299
     */
300 70
    protected function isRemover(string $method): bool
301
    {
302 70
        return StrFacade::startsWith($method, 'remove');
303
    }
304
305
    /**
306
     * Gets the key of the component from the name of the magic method.
307
     *
308
     * @param  string  $method
309
     *
310
     * @return string|null
311
     */
312 88
    protected function parseKey(string $method): ?string
313
    {
314 88
        $search = 'unknown';
315
316
        switch (true) {
317 88
            case $this->isGetter($method):
318 54
                $search = 'get';
319 54
                break;
320
321 58
            case $this->isSetter($method):
322 26
                $search = 'set';
323 26
                break;
324
325 32
            case $this->isPutter($method):
326 16
                $search = 'put';
327 16
                break;
328
329 16
            case $this->isRemover($method):
330 16
                $search = 'remove';
331 16
                break;
332
        }
333
334 88
        return StrFacade::lower(StrFacade::after($method, $search));
335
    }
336
337
    /**
338
     * Set the component key with a value.
339
     *
340
     * @param  string  $key
341
     * @param  mixed  $value
342
     *
343
     * @return $this
344
     */
345 24
    protected function set(string $key, $value): self
346
    {
347
        switch (true) {
348 24
            case $this->hasCastArray($key):
349 8
                $value = $this->castToArray($value);
350 8
                break;
351
        }
352
353 24
        $this->parsed[$key] = $value;
354
355 24
        return $this;
356
    }
357
358
    /**
359
     * Gets the value of the component.
360
     *
361
     * @param  string  $key
362
     *
363
     * @return array|string|null
364
     */
365 52
    protected function get(string $key)
366
    {
367 52
        if ($value = $this->parsed[$key] ?? null) {
368 52
            return $this->hasCastArray($key) ? http_build_query($value) : $value;
369
        }
370
371 42
        return null;
372
    }
373
374
    /**
375
     * Adds a key-value to an array.
376
     *
377
     * @param  string  $key
378
     * @param  string  $parameter
379
     * @param  mixed  $value
380
     *
381
     * @return $this
382
     */
383 2
    protected function put(string $key, string $parameter, $value): self
384
    {
385 2
        $this->parsed[$key][$parameter] = $value;
386
387 2
        return $this;
388
    }
389
390
    /**
391
     * Removes a key from a variable.
392
     *
393
     * @param  string  $key
394
     * @param  string  $parameter
395
     *
396
     * @return $this
397
     */
398 2
    protected function remove(string $key, string $parameter): self
399
    {
400 2
        unset($this->parsed[$key][$parameter]);
401
402 2
        return $this;
403
    }
404
405 52
    protected function hasCastArray(string $key): bool
406
    {
407 52
        return ($this->casts[$key] ?? null) === 'array';
408
    }
409
410 34
    protected function cast(): void
411
    {
412 34
        foreach ($this->casts as $key => $cast) {
413 34
            $value = $this->parsed[$key] ?? null;
414
415
            switch ($cast) {
416 34
                case 'array':
417 34
                    $value = $this->castToArray($value);
418 34
                    break;
419
            }
420
421 34
            $this->parsed[$key] = $value;
422
        }
423 34
    }
424
425 40
    protected function castToArray($value): array
426
    {
427 40
        if (empty($value)) {
428 20
            return [];
429
        }
430
431 24
        if (is_array($value)) {
432 4
            return $value;
433
        }
434
435 22
        $items = [];
436
437 22
        foreach (explode('&', $value) as $item) {
438 22
            [$key, $value] = StrFacade::contains($item, '=') ? explode('=', $item) : [0, $item];
439
440 22
            $items[$key] = $value;
441
        }
442
443 22
        return $items;
444
    }
445
446
    /**
447
     * Checks the number of arguments passed.
448
     *
449
     * @param  string  $method
450
     * @param  array  $args
451
     * @param  int  $need
452
     */
453 56
    protected function validateArgumentsCount(string $method, array $args, int $need = 1): void
454
    {
455 56
        if (count($args) > $need) {
456 4
            throw new ArgumentCountError($method . ' expects at most ' . $need . ' parameter, ' . count($args) . ' given.');
457
        }
458 52
    }
459
}
460