Passed
Pull Request — main (#133)
by Andrey
12:06
created

HttpBuilder::__call()   B

Complexity

Conditions 11
Paths 7

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 11.0245

Importance

Changes 0
Metric Value
cc 11
eloc 18
c 0
b 0
f 0
nc 7
nop 2
dl 0
loc 33
ccs 16
cts 17
cp 0.9412
crap 11.0245
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Http\Uri;
11
use Helldar\Support\Tools\HttpBuilderPrepare;
12
use Psr\Http\Message\UriInterface;
13
use RuntimeException;
14
15
/**
16
 * Based on code by Maksim (Ellrion) Platonov.
17
 *
18
 * @see https://gist.github.com/Ellrion/f51ba0d40ae1d62eeae44fd1adf7b704
19
 *
20
 * @method HttpBuilder setFragment(array|string $value)
21
 * @method HttpBuilder setHost(string $value)
22
 * @method HttpBuilder setPass(string $value)
23
 * @method HttpBuilder setPath(string $value)
24
 * @method HttpBuilder setPort(string $value)
25
 * @method HttpBuilder setQuery(array|string $value)
26
 * @method HttpBuilder setScheme(string $value)
27
 * @method HttpBuilder setUser(string $value)
28
 * @method HttpBuilder putQuery(string $key, $value)
29
 * @method HttpBuilder removeQuery(string $key)
30
 * @method string|null getFragment()
31
 * @method string|null getHost()
32
 * @method string|null getPass()
33
 * @method string|null getPath()
34
 * @method string|null getPort()
35
 * @method string|null getQuery()
36
 * @method string|null getScheme()
37
 * @method string|null getUser()
38
 */
39
class HttpBuilder
40
{
41
    protected $parsed = [];
42
43
    protected $components = [
44
        PHP_URL_SCHEME   => 'scheme',
45
        PHP_URL_HOST     => 'host',
46
        PHP_URL_PORT     => 'port',
47
        PHP_URL_USER     => 'user',
48
        PHP_URL_PASS     => 'pass',
49
        PHP_URL_QUERY    => 'query',
50
        PHP_URL_PATH     => 'path',
51
        PHP_URL_FRAGMENT => 'fragment',
52
    ];
53
54
    protected $allow_put_remove = ['query'];
55
56
    protected $casts = [
57
        'query' => 'array',
58
    ];
59
60
    /**
61
     * Calling magic methods.
62
     *
63
     * @param  string  $method
64
     * @param  mixed  $args
65
     *
66 88
     * @return $this|string|null
67
     */
68 88
    public function __call(string $method, $args)
69 88
    {
70
        if ($this->isGetter($method) || $this->isSetter($method) || $this->isPutter($method) || $this->isRemover($method)) {
71 88
            $key = $this->parseKey($method);
72 32
73
            if (! $this->allowKey($key) || ! $this->allowArrayable($method, $key)) {
74
                throw new RuntimeException($method . ' method not defined.');
75
            }
76 56
77 52
            switch (true) {
78
                case $this->isGetter($method):
79 52
                    $this->validateArgumentsCount($method, $args, 0);
80
81 28
                    return $this->get($key);
0 ignored issues
show
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...
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

81
                    return $this->get(/** @scrutinizer ignore-type */ $key);
Loading history...
82 24
83
                case $this->isSetter($method):
84 20
                    $this->validateArgumentsCount($method, $args);
85
86 4
                    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

86
                    return $this->set(/** @scrutinizer ignore-type */ $key, $args[0]);
Loading history...
87 2
88
                case $this->isPutter($method):
89 2
                    $this->validateArgumentsCount($method, $args, 2);
90
91 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

91
                    return $this->put(/** @scrutinizer ignore-type */ $key, $args[0], $args[1]);
Loading history...
92 2
93
                case $this->isRemover($method):
94 2
                    $this->validateArgumentsCount($method, $args);
95
96
                    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

96
                    return $this->remove(/** @scrutinizer ignore-type */ $key, $args[0]);
Loading history...
97
            }
98
        }
99
100
        throw new RuntimeException("Using an unknown method: \"{$method}\"");
101
    }
102
103
    /**
104
     * Gets the current instance of the object.
105
     *
106 8
     * @return $this
107
     */
108 8
    public function same(): self
109
    {
110
        return $this;
111
    }
112
113
    /**
114
     * Parse a URL.
115
     *
116
     * @param  string|null  $url
117
     * @param  int  $component
118
     *
119 36
     * @return $this
120
     */
121 36
    public function parse(?string $url, int $component = -1): self
122 32
    {
123
        if ($component === -1) {
124
            HttpHelper::validateUrl($url);
125 30
        }
126 30
127
        $component = $this->componentIndex($component);
128 30
        $key       = $this->componentKey($component);
129 26
130 4
        $component === -1 || empty($key)
131
            ? $this->parsed       = parse_url($url)
132 30
            : $this->parsed[$key] = parse_url($url, $component);
133
134 30
        $this->cast();
135
136
        return $this;
137
    }
138
139
    /**
140
     * Filling the builder with parsed data.
141
     *
142
     * @param  array  $parsed
143
     *
144 6
     * @return $this
145
     */
146 6
    public function raw(array $parsed): self
147 6
    {
148 2
        foreach ($parsed as $key => $value) {
149
            if (! $this->allowKey($key)) {
150
                throw new RuntimeException('Filling in the "' . $key . '" key is prohibited.');
151 4
            }
152
153
            $this->set($key, $value);
154 4
        }
155
156 4
        $this->cast();
157
158
        return $this;
159
    }
160
161
    /**
162
     * Compiles parameters to URL.
163
     *
164 16
     * @return string
165
     */
166 16
    public function compile(): string
167 16
    {
168 16
        return implode('', array_filter(array_map(function ($value) {
169
            return InstanceHelper::of($value, HttpBuilderPrepare::class) ? $value->get() : $value;
170
        }, $this->prepare())));
171
    }
172
173
    /**
174
     * Returns parsed data.
175
     *
176 4
     * @return null[]|string[]
177
     */
178
    public function toArray(): array
179 4
    {
180 4
        return [
181 4
            'scheme'   => $this->getScheme(),
182 4
            'host'     => $this->getHost(),
183 4
            'port'     => $this->getPort(),
184 4
            'user'     => $this->getUser(),
185 4
            'pass'     => $this->getPass(),
186 4
            'query'    => $this->getQuery(),
187
            'path'     => $this->getPath(),
188
            'fragment' => $this->getFragment(),
189
        ];
190
    }
191
192
    /**
193
     * Converts HttpBuilder from `\Psr\Http\Message\UriInterface` interface.
194
     *
195 16
     * @param  \Psr\Http\Message\UriInterface  $uri
196
     *
197
     * @return $this
198 16
     */
199 16
    public function fromUriInterface(UriInterface $uri): self
200 16
    {
201
        $this->parse((string) $uri);
202 16
203
        return $this;
204 16
    }
205 16
206 16
    /**
207 16
     * Converts `\Psr\Http\Message\UriInterface` from `HttpBuilder` instance.
208 16
     *
209
     * @return \Psr\Http\Message\UriInterface
210
     */
211
    public function toUriInterface(): UriInterface
212
    {
213
        return Uri::make($this);
214
    }
215
216
    /**
217
     * Prepares data for compilation.
218
     *
219 30
     * @return array
220
     */
221 30
    protected function prepare(): array
222
    {
223
        return [
224
            HttpBuilderPrepare::make()->of($this->getScheme())->suffix('://'),
225
            HttpBuilderPrepare::make()->of($this->getUser()),
226
            HttpBuilderPrepare::make()->of($this->getPass())->prefix(':'),
227
228
            $this->getUser() || $this->getPass() ? '@' : '',
229
230
            HttpBuilderPrepare::make()->of($this->getHost()),
231 30
            HttpBuilderPrepare::make()->of($this->getPort())->prefix(':'),
232
            HttpBuilderPrepare::make()->of($this->getPath())->prefix('/'),
233 30
            HttpBuilderPrepare::make()->of($this->getQuery())->prefix('?'),
234
            HttpBuilderPrepare::make()->of($this->getFragment())->prefix('#'),
235
        ];
236
    }
237
238
    /**
239
     * Gets the index of the component.
240
     *
241
     * @param  int  $component
242
     *
243 90
     * @return int
244
     */
245 90
    protected function componentIndex(int $component = -1): int
246
    {
247
        return ArrFacade::getKey($this->components, $component, -1);
248 84
    }
249
250 84
    /**
251 32
     * Gets the key for the component.
252
     *
253
     * @param  int  $component
254 56
     *
255
     * @return string|null
256
     */
257
    protected function componentKey(int $component = -1): ?string
258
    {
259
        return ArrFacade::get($this->components, $component);
260
    }
261
262
    /**
263
     * Checks if calling the requested key is allowed.
264 88
     *
265
     * @param  string|null  $key
266 88
     *
267
     * @return bool
268
     */
269
    protected function allowKey(?string $key): bool
270
    {
271
        return in_array($key, $this->components);
272
    }
273
274
    protected function allowArrayable(?string $method, ?string $key): bool
275
    {
276 58
        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::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

276
        if ($this->isPutter(/** @scrutinizer ignore-type */ $method) || $this->isRemover($method)) {
Loading history...
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

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