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

HttpBuilder::componentIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
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\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