• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aplus-framework / http / 21082416990

16 Jan 2026 10:08PM UTC coverage: 98.78% (+0.008%) from 98.772%
21082416990

push

github

natanfelles
Update PHPDocs

1619 of 1639 relevant lines covered (98.78%)

14.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.13
/src/URL.php
1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of Aplus Framework HTTP Library.
4
 *
5
 * (c) Natan Felles <natanfelles@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Framework\HTTP;
11

12
use InvalidArgumentException;
13
use JetBrains\PhpStorm\ArrayShape;
14
use JetBrains\PhpStorm\Pure;
15
use JsonSerializable;
16
use RuntimeException;
17
use Stringable;
18

19
/**
20
 * Class URL.
21
 *
22
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web#urls
23
 * @see https://developer.mozilla.org/en-US/docs/Web/API/URL
24
 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3
25
 *
26
 * @package http
27
 */
28
class URL implements JsonSerializable, Stringable
29
{
30
    /**
31
     * The #fragment (id).
32
     */
33
    protected ?string $fragment = null;
34
    protected ?string $hostname = null;
35
    protected ?string $pass = null;
36
    /**
37
     * The /paths/of/url.
38
     *
39
     * @var array<int,string>
40
     */
41
    protected array $pathSegments = [];
42
    protected ?int $port = null;
43
    /**
44
     *  The ?queries.
45
     *
46
     * @var array<string,mixed>
47
     */
48
    protected array $queryData = [];
49
    protected ?string $scheme = null;
50
    protected ?string $user = null;
51

52
    /**
53
     * URL constructor.
54
     *
55
     * @param string $url An absolute URL
56
     */
57
    public function __construct(string $url)
58
    {
59
        $this->setUrl($url);
182✔
60
    }
61

62
    /**
63
     * @return string
64
     */
65
    public function __toString() : string
66
    {
67
        return $this->toString();
19✔
68
    }
69

70
    /**
71
     * @param string $query
72
     * @param int|string|null $value
73
     *
74
     * @return static
75
     */
76
    public function addQuery(string $query, int | string | null $value = null) : static
77
    {
78
        $this->queryData[$query] = $value;
1✔
79
        return $this;
1✔
80
    }
81

82
    /**
83
     * @param array<string,int|string|null> $queries
84
     *
85
     * @return static
86
     */
87
    public function addQueries(array $queries) : static
88
    {
89
        foreach ($queries as $name => $value) {
1✔
90
            $this->addQuery($name, $value);
1✔
91
        }
92
        return $this;
1✔
93
    }
94

95
    /**
96
     * @param array<int,string> $allowed
97
     *
98
     * @return array<string,mixed>
99
     */
100
    #[Pure]
101
    protected function filterQuery(array $allowed) : array
102
    {
103
        return $this->queryData ?
1✔
104
            \array_intersect_key($this->queryData, \array_flip($allowed))
1✔
105
            : [];
1✔
106
    }
107

108
    #[Pure]
109
    public function getBaseUrl(string $path = '/') : string
110
    {
111
        if ($path && $path !== '/') {
1✔
112
            $path = '/' . \ltrim($path, '/');
1✔
113
        }
114
        return $this->getOrigin() . $path;
1✔
115
    }
116

117
    /**
118
     * @return string|null
119
     */
120
    public function getFragment() : ?string
121
    {
122
        return $this->fragment;
22✔
123
    }
124

125
    /**
126
     * @return string|null
127
     */
128
    #[Pure]
129
    public function getHost() : ?string
130
    {
131
        return $this->hostname === null ? null : $this->hostname . $this->getPortPart();
169✔
132
    }
133

134
    #[Pure]
135
    public function getHostname() : ?string
136
    {
137
        return $this->hostname;
2✔
138
    }
139

140
    #[Pure]
141
    public function getOrigin() : string
142
    {
143
        return $this->getScheme() . '://' . $this->getHost();
2✔
144
    }
145

146
    /**
147
     * @return array<string,mixed>
148
     */
149
    #[ArrayShape([
150
        'scheme' => 'string',
151
        'user' => 'null|string',
152
        'pass' => 'null|string',
153
        'hostname' => 'string',
154
        'port' => 'int|null',
155
        'path' => 'string[]',
156
        'query' => 'mixed[]',
157
        'fragment' => 'null|string',
158
    ])]
159
    #[Pure]
160
    public function getParsedUrl() : array
161
    {
162
        return [
1✔
163
            'scheme' => $this->getScheme(),
1✔
164
            'user' => $this->getUser(),
1✔
165
            'pass' => $this->getPass(),
1✔
166
            'hostname' => $this->getHostname(),
1✔
167
            'port' => $this->getPort(),
1✔
168
            'path' => $this->getPathSegments(),
1✔
169
            'query' => $this->getQueryData(),
1✔
170
            'fragment' => $this->getFragment(),
1✔
171
        ];
1✔
172
    }
173

174
    /**
175
     * @return string|null
176
     */
177
    #[Pure]
178
    public function getPass() : ?string
179
    {
180
        return $this->pass;
4✔
181
    }
182

183
    #[Pure]
184
    public function getPath() : string
185
    {
186
        return '/' . \implode('/', $this->pathSegments);
25✔
187
    }
188

189
    /**
190
     * @return array<int,string>
191
     */
192
    #[Pure]
193
    public function getPathSegments() : array
194
    {
195
        return $this->pathSegments;
2✔
196
    }
197

198
    #[Pure]
199
    public function getPathSegment(int $index) : ?string
200
    {
201
        return $this->pathSegments[$index] ?? null;
1✔
202
    }
203

204
    /**
205
     * @return int|null
206
     */
207
    #[Pure]
208
    public function getPort() : ?int
209
    {
210
        return $this->port;
171✔
211
    }
212

213
    #[Pure]
214
    protected function getPortPart() : string
215
    {
216
        $part = $this->getPort();
169✔
217
        if (!\in_array($part, [
169✔
218
            null,
169✔
219
            80,
169✔
220
            443,
169✔
221
        ], true)) {
169✔
222
            return ':' . $part;
15✔
223
        }
224
        return '';
157✔
225
    }
226

227
    /**
228
     * Get the "Query" part of the URL.
229
     *
230
     * @param array<int,string> $allowedKeys Allowed query keys
231
     *
232
     * @return string|null
233
     */
234
    #[Pure]
235
    public function getQuery(array $allowedKeys = []) : ?string
236
    {
237
        $query = $this->getQueryData($allowedKeys);
26✔
238
        return $query ? \http_build_query($query) : null;
26✔
239
    }
240

241
    /**
242
     * @param array<int,string> $allowedKeys
243
     *
244
     * @return array<string,mixed>
245
     */
246
    #[Pure]
247
    public function getQueryData(array $allowedKeys = []) : array
248
    {
249
        return $allowedKeys ? $this->filterQuery($allowedKeys) : $this->queryData;
27✔
250
    }
251

252
    /**
253
     * @return string|null
254
     */
255
    #[Pure]
256
    public function getScheme() : ?string
257
    {
258
        return $this->scheme;
26✔
259
    }
260

261
    /**
262
     * @since 5.3
263
     *
264
     * @return string
265
     */
266
    public function toString() : string
267
    {
268
        return $this->getUrl();
20✔
269
    }
270

271
    /**
272
     * @since 6.1
273
     *
274
     * @return string
275
     */
276
    public function getUrl() : string
277
    {
278
        $url = $this->getScheme() . '://';
20✔
279
        $part = $this->getUser();
20✔
280
        if ($part !== null) {
20✔
281
            $url .= $part;
3✔
282
            $part = $this->getPass();
3✔
283
            if ($part !== null) {
3✔
284
                $url .= ':' . $part;
3✔
285
            }
286
            $url .= '@';
3✔
287
        }
288
        $url .= $this->getHost();
20✔
289
        $url .= $this->getRelative();
20✔
290
        return $url;
20✔
291
    }
292

293
    /**
294
     * Get the relative URL.
295
     *
296
     * @since 6.1
297
     *
298
     * @return string
299
     */
300
    public function getRelative() : string
301
    {
302
        $relative = $this->getPath();
21✔
303
        $part = $this->getQuery();
21✔
304
        if ($part !== null) {
21✔
305
            $relative .= '?' . $part;
7✔
306
        }
307
        $part = $this->getFragment();
21✔
308
        if ($part !== null) {
21✔
309
            $relative .= '#' . $part;
4✔
310
        }
311
        return $relative;
21✔
312
    }
313

314
    /**
315
     * @return string|null
316
     */
317
    #[Pure]
318
    public function getUser() : ?string
319
    {
320
        return $this->user;
21✔
321
    }
322

323
    /**
324
     * @param string $key
325
     *
326
     * @return static
327
     */
328
    public function removeQueryData(string $key) : static
329
    {
330
        unset($this->queryData[$key]);
1✔
331
        return $this;
1✔
332
    }
333

334
    /**
335
     * @param string $fragment
336
     *
337
     * @return static
338
     */
339
    public function setFragment(string $fragment) : static
340
    {
341
        $this->fragment = \ltrim($fragment, '#');
20✔
342
        return $this;
20✔
343
    }
344

345
    /**
346
     * @param string $hostname
347
     *
348
     * @throws InvalidArgumentException for invalid URL Hostname
349
     *
350
     * @return static
351
     */
352
    public function setHostname(string $hostname) : static
353
    {
354
        $filtered = \filter_var($hostname, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME);
182✔
355
        if (!$filtered) {
182✔
356
            throw new InvalidArgumentException("Invalid URL Hostname: {$hostname}");
2✔
357
        }
358
        $this->hostname = $filtered;
182✔
359
        return $this;
182✔
360
    }
361

362
    /**
363
     * @param string $pass
364
     *
365
     * @return static
366
     */
367
    public function setPass(string $pass) : static
368
    {
369
        $this->pass = $pass;
20✔
370
        return $this;
20✔
371
    }
372

373
    /**
374
     * @param string $segments
375
     *
376
     * @return static
377
     */
378
    public function setPath(string $segments) : static
379
    {
380
        return $this->setPathSegments(\explode('/', \ltrim($segments, '/')));
182✔
381
    }
382

383
    /**
384
     * @param array<int,string> $segments
385
     *
386
     * @return static
387
     */
388
    public function setPathSegments(array $segments) : static
389
    {
390
        $this->pathSegments = $segments;
182✔
391
        return $this;
182✔
392
    }
393

394
    /**
395
     * @param int $port
396
     *
397
     * @throws InvalidArgumentException for invalid URL Port
398
     *
399
     * @return static
400
     */
401
    public function setPort(int $port) : static
402
    {
403
        if ($port < 1 || $port > 65535) {
29✔
404
            throw new InvalidArgumentException("Invalid URL Port: {$port}");
1✔
405
        }
406
        $this->port = $port;
29✔
407
        return $this;
29✔
408
    }
409

410
    /**
411
     * @param string $data
412
     * @param array<string> $only
413
     *
414
     * @return static
415
     */
416
    public function setQuery(string $data, array $only = []) : static
417
    {
418
        \parse_str(\ltrim($data, '?'), $result);
169✔
419
        return $this->setQueryData($result, $only);
169✔
420
    }
421

422
    /**
423
     * @param array<mixed> $data
424
     * @param array<string> $only
425
     *
426
     * @return static
427
     */
428
    public function setQueryData(array $data, array $only = []) : static
429
    {
430
        if ($only) {
169✔
431
            $data = \array_intersect_key($data, \array_flip($only));
1✔
432
        }
433
        $this->queryData = $data;
169✔
434
        return $this;
169✔
435
    }
436

437
    /**
438
     * @param string $scheme
439
     *
440
     * @return static
441
     */
442
    public function setScheme(string $scheme) : static
443
    {
444
        $this->scheme = $scheme;
182✔
445
        return $this;
182✔
446
    }
447

448
    /**
449
     * @param string $url
450
     *
451
     * @throws InvalidArgumentException for invalid URL
452
     *
453
     * @return static
454
     */
455
    protected function setUrl(string $url) : static
456
    {
457
        $filteredUrl = \filter_var($url, \FILTER_VALIDATE_URL);
182✔
458
        if (!$filteredUrl) {
182✔
459
            throw new InvalidArgumentException("Invalid URL: {$url}");
6✔
460
        }
461
        $url = \parse_url($filteredUrl);
182✔
462
        if ($url === false) {
182✔
463
            throw new RuntimeException("URL could not be parsed: {$filteredUrl}");
×
464
        }
465
        $this->setScheme($url['scheme']); // @phpstan-ignore-line
182✔
466
        if (isset($url['user'])) {
182✔
467
            $this->setUser($url['user']);
20✔
468
        }
469
        if (isset($url['pass'])) {
182✔
470
            $this->setPass($url['pass']);
20✔
471
        }
472
        $this->setHostname($url['host']); // @phpstan-ignore-line
182✔
473
        if (isset($url['port'])) {
182✔
474
            $this->setPort($url['port']);
29✔
475
        }
476
        if (isset($url['path'])) {
182✔
477
            $this->setPath($url['path']);
182✔
478
        }
479
        if (isset($url['query'])) {
182✔
480
            $this->setQuery($url['query']);
169✔
481
        }
482
        if (isset($url['fragment'])) {
182✔
483
            $this->setFragment($url['fragment']);
20✔
484
        }
485
        return $this;
182✔
486
    }
487

488
    /**
489
     * @param string $user
490
     *
491
     * @return static
492
     */
493
    public function setUser(string $user) : static
494
    {
495
        $this->user = $user;
20✔
496
        return $this;
20✔
497
    }
498

499
    #[Pure]
500
    public function jsonSerialize() : string
501
    {
502
        return $this->toString();
1✔
503
    }
504
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc