Uri::withPath()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.512

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 16
ccs 8
cts 13
cp 0.6153
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 3.512
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 *
11
 * Implementation of this file has been influenced by Zend Diactoros
12
 *
13
 * @link    https://github.com/zendframework/zend-diactoros
14
 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md
15
 */
16
17
declare(strict_types=1);
18
19
namespace Phalcon\Http\Message;
20
21
use Phalcon\Helper\Arr;
22
use Phalcon\Helper\Str;
23
use Phalcon\Http\Message\Exception\InvalidArgumentException;
24
use Phalcon\Http\Message\Traits\CommonTrait;
25
use Phalcon\Http\Message\Traits\UriTrait;
26
use Psr\Http\Message\UriInterface;
27
28
use function parse_url;
29
use function rawurlencode;
30
use function strpos;
31
use function strtolower;
32
33
/**
34
 * PSR-7 Uri
35
 *
36
 * @property string   $fragment
37
 * @property string   $host
38
 * @property string   $pass
39
 * @property string   $path
40
 * @property null|int $port
41
 * @property string   $query
42
 * @property string   $scheme
43
 * @property string   $user
44
 */
45
final class Uri implements UriInterface
46
{
47
    use CommonTrait;
48
    use UriTrait;
49
50
    /**
51
     * Retrieve the fragment component of the URI.
52
     *
53
     * @var string
54
     */
55
    private $fragment = '';
56
57
    /**
58
     * @var string
59
     */
60
    private $host = '';
61
62
    /**
63
     * @var string
64
     */
65
    private $pass = '';
66
67
    /**
68
     * Retrieve the path component of the URI.
69
     *
70
     * @var string
71
     */
72
    private $path = '';
73
74
    /**
75
     * @var null | int
76
     */
77
    private $port = null;
78
79
    /**
80
     * Retrieve the query string of the URI.
81
     *
82
     * @var string
83
     */
84
    private $query = '';
85
86
    /**
87
     * @var string
88
     */
89
    private $scheme = 'https';
90
91
    /**
92
     * @var string
93
     */
94
    private $user = '';
95
96
    /**
97
     * Uri constructor.
98
     *
99
     * @param string $uri
100
     */
101 139
    public function __construct(string $uri = '')
102
    {
103 139
        if ('' !== $uri) {
104 45
            $urlParts = parse_url($uri);
105
106 45
            if (false === $urlParts) {
107 1
                $urlParts = [];
108
            }
109
110 45
            $this->fragment = $this->filterFragment(
111 45
                (string) Arr::get($urlParts, 'fragment', '')
112
            );
113 45
            $this->host     = strtolower(
114 45
                (string) Arr::get($urlParts, 'host', '')
115
            );
116 45
            $this->pass     = rawurlencode(
117 45
                (string) Arr::get($urlParts, 'pass', '')
118
            );
119 45
            $this->path     = $this->filterPath(
120 45
                (string) Arr::get($urlParts, 'path', '')
121
            );
122 45
            $this->port     = $this->filterPort(
123 45
                Arr::get($urlParts, 'port', null)
124
            );
125 45
            $this->query    = $this->filterQuery(
126 45
                (string) Arr::get($urlParts, 'query', '')
127
            );
128 45
            $this->scheme   = $this->filterScheme(
129 45
                (string) Arr::get($urlParts, 'scheme', '')
130
            );
131 45
            $this->user     = rawurlencode(
132 45
                (string) Arr::get($urlParts, 'user', '')
133
            );
134
        }
135 139
    }
136
137
    /**
138
     * Return the string representation as a URI reference.
139
     *
140
     * Depending on which components of the URI are present, the resulting
141
     * string is either a full URI or relative reference according to RFC 3986,
142
     * Section 4.1. The method concatenates the various components of the URI,
143
     * using the appropriate delimiters
144
     *
145
     * @return string
146
     */
147 10
    public function __toString(): string
148
    {
149 10
        $authority = $this->getAuthority();
150 10
        $path      = $this->path;
151
152
        /**
153
         * The path can be concatenated without delimiters. But there are two
154
         * cases where the path has to be adjusted to make the URI reference
155
         * valid as PHP does not allow to throw an exception in __toString():
156
         *   - If the path is rootless and an authority is present, the path
157
         *     MUST be prefixed by "/".
158
         *   - If the path is starting with more than one "/" and no authority
159
         *     is present, the starting slashes MUST be reduced to one.
160
         */
161 10
        if ('' !== $path && true !== Str::startsWith($path, '/') && '' !== $authority) {
162 2
            $path = '/' . $path;
163
        }
164
165 10
        $uri = $this->checkValue($this->scheme, '', ':')
166 10
            . $this->checkValue($authority, '//')
167 10
            . $path
168 10
            . $this->checkValue($this->query, '?')
169 10
            . $this->checkValue($this->fragment, '#');
170
171 10
        return $uri;
172
    }
173
174
    /**
175
     * Retrieve the authority component of the URI.
176
     *
177
     * @return string
178
     */
179 11
    public function getAuthority(): string
180
    {
181
        /**
182
         * If no authority information is present, this method MUST return an
183
         * empty string.
184
         */
185 11
        if ('' === $this->host) {
186 1
            return '';
187
        }
188
189 11
        $authority = $this->host;
190 11
        $userInfo  = $this->getUserInfo();
191
192
        /**
193
         * The authority syntax of the URI is:
194
         *
195
         * [user-info@]host[:port]
196
         */
197 11
        if ('' !== $userInfo) {
198 8
            $authority = $userInfo . '@' . $authority;
199
        }
200
201
        /**
202
         * If the port component is not set or is the standard port for the
203
         * current scheme, it SHOULD NOT be included.
204
         */
205 11
        if (null !== $this->port) {
206 8
            $authority .= ':' . $this->port;
207
        }
208
209 11
        return $authority;
210
    }
211
212
    /**
213
     * Returns the fragment of the URL
214
     *
215
     * @return string
216
     */
217 5
    public function getFragment(): string
218
    {
219 5
        return $this->fragment;
220
    }
221
222
    /**
223
     * Retrieve the host component of the URI.
224
     *
225
     * If no host is present, this method MUST return an empty string.
226
     *
227
     * The value returned MUST be normalized to lowercase, per RFC 3986
228
     * Section 3.2.2.
229
     *
230
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
231
     *
232
     * @return string
233
     */
234 7
    public function getHost(): string
235
    {
236 7
        return $this->host;
237
    }
238
239
    /**
240
     * Returns the path of the URL
241
     *
242
     * @return string
243
     */
244 11
    public function getPath(): string
245
    {
246 11
        return $this->path;
247
    }
248
249
    /**
250
     * Retrieve the port component of the URI.
251
     *
252
     * If a port is present, and it is non-standard for the current scheme,
253
     * this method MUST return it as an integer. If the port is the standard
254
     * port used with the current scheme, this method SHOULD return null.
255
     *
256
     * If no port is present, and no scheme is present, this method MUST return
257
     * a null value.
258
     *
259
     * If no port is present, but a scheme is present, this method MAY return
260
     * the standard port for that scheme, but SHOULD return null.
261
     *
262
     * @return int|null
263
     */
264 6
    public function getPort(): ?int
265
    {
266 6
        return $this->port;
267
    }
268
269
    /**
270
     * Returns the query of the URL
271
     *
272
     * @return string
273
     */
274 9
    public function getQuery(): string
275
    {
276 9
        return $this->query;
277
    }
278
279
    /**
280
     * Retrieve the scheme component of the URI.
281
     *
282
     * If no scheme is present, this method MUST return an empty string.
283
     *
284
     * The value returned MUST be normalized to lowercase, per RFC 3986
285
     * Section 3.1.
286
     *
287
     * The trailing ':' character is not part of the scheme and MUST NOT be
288
     * added.
289
     *
290
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
291
     *
292
     * @return string
293
     */
294 4
    public function getScheme(): string
295
    {
296 4
        return $this->scheme;
297
    }
298
299
    /**
300
     * Retrieve the user information component of the URI.
301
     *
302
     * If no user information is present, this method MUST return an empty
303
     * string.
304
     *
305
     * If a user is present in the URI, this will return that value;
306
     * additionally, if the password is also present, it will be appended to the
307
     * user value, with a colon (":") separating the values.
308
     *
309
     * The trailing "@" character is not part of the user information and MUST
310
     * NOT be added.
311
     *
312
     * @return string The URI user information, in "username[:password]" format.
313
     */
314 15
    public function getUserInfo(): string
315
    {
316 15
        if (true !== empty($this->pass)) {
317 10
            return $this->user . ':' . $this->pass;
318
        }
319
320 7
        return $this->user;
321
    }
322
323
    /**
324
     * Return an instance with the specified URI fragment.
325
     *
326
     * This method MUST retain the state of the current instance, and return
327
     * an instance that contains the specified URI fragment.
328
     *
329
     * Users can provide both encoded and decoded fragment characters.
330
     * Implementations ensure the correct encoding as outlined in getFragment().
331
     *
332
     * An empty fragment value is equivalent to removing the fragment.
333
     *
334
     * @param string $fragment
335
     *
336
     * @return Uri
337
     */
338 3
    public function withFragment($fragment): Uri
339
    {
340 3
        $this->checkStringParameter($fragment);
341
342 2
        $fragment = $this->filterFragment($fragment);
343
344 2
        return $this->cloneInstance($fragment, 'fragment');
345
    }
346
347
    /**
348
     * Return an instance with the specified path.
349
     *
350
     * This method MUST retain the state of the current instance, and return
351
     * an instance that contains the specified path.
352
     *
353
     * The path can either be empty or absolute (starting with a slash) or
354
     * rootless (not starting with a slash). Implementations MUST support all
355
     * three syntaxes.
356
     *
357
     * If an HTTP path is intended to be host-relative rather than path-relative
358
     * then it must begin with a slash ("/"). HTTP paths not starting with a
359
     * slash are assumed to be relative to some base path known to the
360
     * application or consumer.
361
     *
362
     * Users can provide both encoded and decoded path characters.
363
     * Implementations ensure the correct encoding as outlined in getPath().
364
     *
365
     * @param string $path
366
     *
367
     * @return Uri
368
     * @throws InvalidArgumentException for invalid paths.
369
     */
370 17
    public function withPath($path): Uri
371
    {
372 17
        $this->checkStringParameter($path);
373
374
        if (
375 16
            false !== strpos($path, '?') ||
376 16
            false !== strpos($path, '#')
377
        ) {
378 2
            throw new InvalidArgumentException(
379 2
                'Path cannot contain a query string or fragment'
380
            );
381
        }
382
383 14
        $path = $this->filterPath($path);
384
385 14
        return $this->cloneInstance($path, 'path');
386
    }
387
388
    /**
389
     * Return an instance with the specified port.
390
     *
391
     * This method MUST retain the state of the current instance, and return
392
     * an instance that contains the specified port.
393
     *
394
     * Implementations MUST raise an exception for ports outside the
395
     * established TCP and UDP port ranges.
396
     *
397
     * A null value provided for the port is equivalent to removing the port
398
     * information.
399
     *
400
     * @param int|null $port
401
     *
402
     * @return Uri
403
     * @throws InvalidArgumentException for invalid ports.
404
     */
405 4
    public function withPort($port): Uri
406
    {
407 4
        if (null !== $port) {
408 4
            $port = $this->filterPort($port);
409
410 4
            if (null !== $port && ($port < 1 || $port > 65535)) {
411 1
                throw new InvalidArgumentException(
412 1
                    'Method expects valid port (1-65535)'
413
                );
414
            }
415
        }
416
417 3
        return $this->cloneInstance($port, 'port');
418
    }
419
420
    /**
421
     * Return an instance with the specified query string.
422
     *
423
     * This method MUST retain the state of the current instance, and return
424
     * an instance that contains the specified query string.
425
     *
426
     * Users can provide both encoded and decoded query characters.
427
     * Implementations ensure the correct encoding as outlined in getQuery().
428
     *
429
     * An empty query string value is equivalent to removing the query string.
430
     *
431
     * @param string $query
432
     *
433
     * @return Uri
434
     * @throws InvalidArgumentException for invalid query strings.
435
     */
436 14
    public function withQuery($query): Uri
437
    {
438 14
        $this->checkStringParameter($query);
439
440 13
        if (false !== strpos($query, '#')) {
441 1
            throw new InvalidArgumentException(
442 1
                'Query cannot contain a query fragment'
443
            );
444
        }
445
446 12
        $query = $this->filterQuery($query);
447
448 12
        return $this->cloneInstance($query, 'query');
449
    }
450
451
    /**
452
     * Return an instance with the specified scheme.
453
     *
454
     * This method MUST retain the state of the current instance, and return
455
     * an instance that contains the specified scheme.
456
     *
457
     * Implementations MUST support the schemes "http" and "https" case
458
     * insensitively, and MAY accommodate other schemes if required.
459
     *
460
     * An empty scheme is equivalent to removing the scheme.
461
     *
462
     * @param string $scheme
463
     *
464
     * @return Uri
465
     * @throws InvalidArgumentException for invalid schemes.
466
     * @throws InvalidArgumentException for unsupported schemes.
467
     */
468 14
    public function withScheme($scheme): Uri
469
    {
470 14
        $this->checkStringParameter($scheme);
471
472 13
        $scheme = $this->filterScheme($scheme);
473
474 12
        return $this->processWith($scheme, 'scheme');
475
    }
476
477
    /**
478
     * Return an instance with the specified user information.
479
     *
480
     * @param string      $user
481
     * @param string|null $password
482
     *
483
     * @return Uri
484
     */
485 2
    public function withUserInfo($user, $password = null): self
486
    {
487 2
        $this->checkStringParameter($user);
488
489 1
        if (null !== $password) {
490 1
            $this->checkStringParameter($user);
491
        }
492
493 1
        $user = rawurlencode($user);
494
495 1
        if (null !== $password) {
496 1
            $password = rawurlencode($password);
497
        }
498
499
        /**
500
         * Immutable - need to send a new object back
501
         */
502 1
        $newInstance       = $this->cloneInstance($user, 'user');
503 1
        $newInstance->pass = $password;
504
505 1
        return $newInstance;
506
    }
507
508
    /**
509
     * Return an instance with the specified host.
510
     *
511
     * This method MUST retain the state of the current instance, and return
512
     * an instance that contains the specified host.
513
     *
514
     * An empty host value is equivalent to removing the host.
515
     *
516
     * @param string $host
517
     *
518
     * @return Uri
519
     * @throws InvalidArgumentException for invalid hostnames.
520
     *
521
     */
522 6
    public function withHost($host): Uri
523
    {
524 6
        return $this->processWith($host, 'host');
525
    }
526
}
527