Passed
Push — main ( a01d42...f042b8 )
by Andrey
13:24 queued 11:56
created

HttpBuilder::toUriInterface()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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
     * @return $this|string|null
67
     */
68 92
    public function __call(string $method, $args)
69
    {
70 92
        if ($this->isGetter($method) || $this->isSetter($method) || $this->isPutter($method) || $this->isRemover($method)) {
71 92
            $key = $this->parseKey($method);
72
73 92
            if (! $this->allowKey($key) || ! $this->allowArrayable($method, $key)) {
74 32
                throw new RuntimeException($method . ' method not defined.');
75
            }
76
77
            switch (true) {
78 60
                case $this->isGetter($method):
79 56
                    $this->validateArgumentsCount($method, $args, 0);
80
81 56
                    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
83 28
                case $this->isSetter($method):
84 24
                    $this->validateArgumentsCount($method, $args);
85
86 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

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

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
     * @return $this
107
     */
108 8
    public function same(): self
109
    {
110 8
        return $this;
111
    }
112
113
    /**
114
     * Parse a URL.
115
     *
116
     * @param  string|null  $url
117
     * @param  int  $component
118
     *
119
     * @return $this
120
     */
121 40
    public function parse(?string $url, int $component = -1): self
122
    {
123 40
        if ($component === -1) {
124 36
            HttpHelper::validateUrl($url);
125
        }
126
127 34
        $component = $this->componentIndex($component);
128 34
        $key       = $this->componentKey($component);
129
130 34
        $component === -1 || empty($key)
131 30
            ? $this->parsed       = parse_url($url)
132 4
            : $this->parsed[$key] = parse_url($url, $component);
133
134 34
        $this->cast();
135
136 34
        return $this;
137
    }
138
139
    /**
140
     * Filling the builder with parsed data.
141
     *
142
     * @param  array  $parsed
143
     *
144
     * @return $this
145
     */
146 6
    public function raw(array $parsed): self
147
    {
148 6
        foreach ($parsed as $key => $value) {
149 6
            if (! $this->allowKey($key)) {
150 2
                throw new RuntimeException('Filling in the "' . $key . '" key is prohibited.');
151
            }
152
153 4
            $this->set($key, $value);
154
        }
155
156 4
        $this->cast();
157
158 4
        return $this;
159
    }
160
161
    /**
162
     * Compiles parameters to URL.
163
     *
164
     * @return string
165
     */
166 20
    public function compile(): string
167
    {
168 20
        return implode('', array_filter(array_map(function ($value) {
169 20
            return InstanceHelper::of($value, HttpBuilderPrepare::class) ? $value->get() : $value;
170 20
        }, $this->prepare())));
171
    }
172
173
    /**
174
     * Returns parsed data.
175
     *
176
     * @return null[]|string[]
177
     */
178 4
    public function toArray(): array
179
    {
180
        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 4
            'path'     => $this->getPath(),
188 4
            'fragment' => $this->getFragment(),
189
        ];
190
    }
191
192
    /**
193
     * Converts HttpBuilder from `\Psr\Http\Message\UriInterface` interface.
194
     *
195
     * @param  \Psr\Http\Message\UriInterface  $uri
196
     *
197
     * @return $this
198
     */
199 2
    public function fromUriInterface(UriInterface $uri): self
200
    {
201 2
        $this->parse((string) $uri);
202
203 2
        return $this;
204
    }
205
206
    /**
207
     * Converts `\Psr\Http\Message\UriInterface` from `HttpBuilder` instance.
208
     *
209
     * @return \Psr\Http\Message\UriInterface
210
     */
211 2
    public function toUriInterface(): UriInterface
212
    {
213 2
        return Uri::make($this);
214
    }
215
216
    /**
217
     * Prepares data for compilation.
218
     *
219
     * @return array
220
     */
221 20
    protected function prepare(): array
222
    {
223
        return [
224 20
            HttpBuilderPrepare::make()->of($this->getScheme())->suffix('://'),
225 20
            HttpBuilderPrepare::make()->of($this->getUser()),
226 20
            HttpBuilderPrepare::make()->of($this->getPass())->prefix(':'),
227
228 20
            $this->getUser() || $this->getPass() ? '@' : '',
229
230 20
            HttpBuilderPrepare::make()->of($this->getHost()),
231 20
            HttpBuilderPrepare::make()->of($this->getPort())->prefix(':'),
232 20
            HttpBuilderPrepare::make()->of($this->getPath())->prefix('/'),
233 20
            HttpBuilderPrepare::make()->of($this->getQuery())->prefix('?'),
234 20
            HttpBuilderPrepare::make()->of($this->getFragment())->prefix('#'),
235
        ];
236
    }
237
238
    /**
239
     * Gets the index of the component.
240
     *
241
     * @param  int  $component
242
     *
243
     * @return int
244
     */
245 34
    protected function componentIndex(int $component = -1): int
246
    {
247 34
        return ArrFacade::getKey($this->components, $component, -1);
248
    }
249
250
    /**
251
     * Gets the key for the component.
252
     *
253
     * @param  int  $component
254
     *
255
     * @return string|null
256
     */
257 34
    protected function componentKey(int $component = -1): ?string
258
    {
259 34
        return ArrFacade::get($this->components, $component);
260
    }
261
262
    /**
263
     * Checks if calling the requested key is allowed.
264
     *
265
     * @param  string|null  $key
266
     *
267
     * @return bool
268
     */
269 94
    protected function allowKey(?string $key): bool
270
    {
271 94
        return in_array($key, $this->components);
272
    }
273
274 88
    protected function allowArrayable(?string $method, ?string $key): bool
275
    {
276 88
        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 32
            return in_array($key, $this->allow_put_remove);
278
        }
279
280 60
        return true;
281
    }
282
283
    /**
284
     * Checks if the method is a request for information.
285
     *
286
     * @param  string  $method
287
     *
288
     * @return bool
289
     */
290 92
    protected function isGetter(string $method): bool
291
    {
292 92
        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
     * @return bool
301
     */
302 58
    protected function isSetter(string $method): bool
303
    {
304 58
        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
     * @return bool
313
     */
314 88
    protected function isPutter(string $method): bool
315
    {
316 88
        return StrFacade::startsWith($method, 'put');
317
    }
318
319
    /**
320
     * Checks if the method is a request to remove value.
321
     *
322
     * @param  string  $method
323
     *
324
     * @return bool
325
     */
326 74
    protected function isRemover(string $method): bool
327
    {
328 74
        return StrFacade::startsWith($method, 'remove');
329
    }
330
331
    /**
332
     * Gets the key of the component from the name of the magic method.
333
     *
334
     * @param  string  $method
335
     *
336
     * @return string|null
337
     */
338 92
    protected function parseKey(string $method): ?string
339
    {
340 92
        $search = 'unknown';
341
342
        switch (true) {
343 92
            case $this->isGetter($method):
344 58
                $search = 'get';
345 58
                break;
346
347 58
            case $this->isSetter($method):
348 26
                $search = 'set';
349 26
                break;
350
351 32
            case $this->isPutter($method):
352 16
                $search = 'put';
353 16
                break;
354
355 16
            case $this->isRemover($method):
356 16
                $search = 'remove';
357 16
                break;
358
        }
359
360 92
        return StrFacade::lower(StrFacade::after($method, $search));
361
    }
362
363
    /**
364
     * Set the component key with a value.
365
     *
366
     * @param  string  $key
367
     * @param  mixed  $value
368
     *
369
     * @return $this
370
     */
371 24
    protected function set(string $key, $value): self
372
    {
373
        switch (true) {
374 24
            case $this->hasCastArray($key):
375 8
                $value = $this->castToArray($value);
376 8
                break;
377
        }
378
379 24
        $this->parsed[$key] = $value;
380
381 24
        return $this;
382
    }
383
384
    /**
385
     * Gets the value of the component.
386
     *
387
     * @param  string  $key
388
     *
389
     * @return array|string|null
390
     */
391 56
    protected function get(string $key)
392
    {
393 56
        if ($value = $this->parsed[$key] ?? null) {
394 56
            return $this->hasCastArray($key) ? http_build_query($value) : $value;
395
        }
396
397 42
        return null;
398
    }
399
400
    /**
401
     * Adds a key-value to an array.
402
     *
403
     * @param  string  $key
404
     * @param  string  $parameter
405
     * @param  mixed  $value
406
     *
407
     * @return $this
408
     */
409 2
    protected function put(string $key, string $parameter, $value): self
410
    {
411 2
        $this->parsed[$key][$parameter] = $value;
412
413 2
        return $this;
414
    }
415
416
    /**
417
     * Removes a key from a variable.
418
     *
419
     * @param  string  $key
420
     * @param  string  $parameter
421
     *
422
     * @return $this
423
     */
424 2
    protected function remove(string $key, string $parameter): self
425
    {
426 2
        unset($this->parsed[$key][$parameter]);
427
428 2
        return $this;
429
    }
430
431 56
    protected function hasCastArray(string $key): bool
432
    {
433 56
        return ($this->casts[$key] ?? null) === 'array';
434
    }
435
436 38
    protected function cast(): void
437
    {
438 38
        foreach ($this->casts as $key => $cast) {
439 38
            $value = $this->parsed[$key] ?? null;
440
441
            switch ($cast) {
442 38
                case 'array':
443 38
                    $value = $this->castToArray($value);
444 38
                    break;
445
            }
446
447 38
            $this->parsed[$key] = $value;
448
        }
449 38
    }
450
451 44
    protected function castToArray($value): array
452
    {
453 44
        if (empty($value)) {
454 20
            return [];
455
        }
456
457 28
        if (is_array($value)) {
458 4
            return $value;
459
        }
460
461 26
        $items = [];
462
463 26
        foreach (explode('&', $value) as $item) {
464 26
            [$key, $value] = StrFacade::contains($item, '=') ? explode('=', $item) : [0, $item];
465
466 26
            $items[$key] = $value;
467
        }
468
469 26
        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 60
    protected function validateArgumentsCount(string $method, array $args, int $need = 1): void
480
    {
481 60
        if (count($args) > $need) {
482 4
            throw new ArgumentCountError($method . ' expects at most ' . $need . ' parameter, ' . count($args) . ' given.');
483
        }
484 56
    }
485
}
486