Skip to content

Commit d208720

Browse files
committed
added UrlImmutable
1 parent 417bab4 commit d208720

File tree

6 files changed

+372
-5
lines changed

6 files changed

+372
-5
lines changed

src/Http/Url.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class Url implements \JsonSerializable
7878

7979

8080
/**
81-
* @param string|self $url
81+
* @param string|self|UrlImmutable $url
8282
* @throws Nette\InvalidArgumentException if URL is malformed
8383
*/
8484
public function __construct($url = null)
@@ -98,10 +98,11 @@ public function __construct($url = null)
9898
$this->setQuery($p['query'] ?? []);
9999
$this->fragment = isset($p['fragment']) ? rawurldecode($p['fragment']) : '';
100100

101-
} elseif ($url instanceof self) {
102-
foreach ($this as $key => $val) {
103-
$this->$key = $url->$key;
104-
}
101+
} elseif ($url instanceof UrlImmutable || $url instanceof self) {
102+
[$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment] = $url->export();
103+
104+
} elseif ($url !== null) {
105+
throw new Nette\InvalidArgumentException;
105106
}
106107
}
107108

@@ -396,6 +397,13 @@ public function jsonSerialize(): string
396397
}
397398

398399

400+
/** @internal */
401+
final public function export(): array
402+
{
403+
return [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment];
404+
}
405+
406+
399407
/**
400408
* Similar to rawurldecode, but preserves reserved chars encoded.
401409
*/

src/Http/UrlImmutable.php

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Nette\Http;
11+
12+
use Nette;
13+
14+
15+
/**
16+
* Immutable representation of a URL.
17+
*
18+
* <pre>
19+
* scheme user password host port path query fragment
20+
* | | | | | | | |
21+
* /--\ /--\ /------\ /-------\ /--\/------------\ /--------\ /------\
22+
* http://john:[email protected]:8042/en/manual.php?name=param#fragment <-- absoluteUrl
23+
* \______\__________________________/
24+
* | |
25+
* hostUrl authority
26+
* </pre>
27+
*
28+
* @property-read string $scheme
29+
* @property-read string $user
30+
* @property-read string $password
31+
* @property-read string $host
32+
* @property-read int $port
33+
* @property-read string $path
34+
* @property-read string $query
35+
* @property-read string $fragment
36+
* @property-read string $absoluteUrl
37+
* @property-read string $authority
38+
* @property-read string $hostUrl
39+
* @property-read array $queryParameters
40+
*/
41+
class UrlImmutable implements \JsonSerializable
42+
{
43+
use Nette\SmartObject;
44+
45+
/** @var string */
46+
private $scheme = '';
47+
48+
/** @var string */
49+
private $user = '';
50+
51+
/** @var string */
52+
private $password = '';
53+
54+
/** @var string */
55+
private $host = '';
56+
57+
/** @var int|null */
58+
private $port;
59+
60+
/** @var string */
61+
private $path = '';
62+
63+
/** @var array */
64+
private $query = [];
65+
66+
/** @var string */
67+
private $fragment = '';
68+
69+
70+
/**
71+
* @param string|self|Url $url
72+
* @throws Nette\InvalidArgumentException if URL is malformed
73+
*/
74+
public function __construct($url)
75+
{
76+
if ($url instanceof Url || $url instanceof self || is_string($url)) {
77+
$url = is_string($url) ? new Url($url) : $url;
78+
[$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment] = $url->export();
79+
} else {
80+
throw new Nette\InvalidArgumentException;
81+
}
82+
83+
if ($this->host && substr($this->path, 0, 1) !== '/') {
84+
$this->path = '/' . $this->path;
85+
}
86+
}
87+
88+
89+
public function getScheme(): string
90+
{
91+
return $this->scheme;
92+
}
93+
94+
95+
public function getUser(): string
96+
{
97+
return $this->user;
98+
}
99+
100+
101+
public function getPassword(): string
102+
{
103+
return $this->password;
104+
}
105+
106+
107+
public function getHost(): string
108+
{
109+
return $this->host;
110+
}
111+
112+
113+
public function getDomain(int $level = 2): string
114+
{
115+
$parts = ip2long($this->host) ? [$this->host] : explode('.', $this->host);
116+
$parts = $level >= 0 ? array_slice($parts, -$level) : array_slice($parts, 0, $level);
117+
return implode('.', $parts);
118+
}
119+
120+
121+
public function getPort(): ?int
122+
{
123+
return $this->port ?: (Url::$defaultPorts[$this->scheme] ?? null);
124+
}
125+
126+
127+
public function getPath(): string
128+
{
129+
return $this->path;
130+
}
131+
132+
133+
public function getQuery(): string
134+
{
135+
return http_build_query($this->query, '', '&', PHP_QUERY_RFC3986);
136+
}
137+
138+
139+
public function getQueryParameters(): array
140+
{
141+
return $this->query;
142+
}
143+
144+
145+
/**
146+
* @return array|string|null
147+
*/
148+
public function getQueryParameter(string $name)
149+
{
150+
return $this->query[$name] ?? null;
151+
}
152+
153+
154+
public function getFragment(): string
155+
{
156+
return $this->fragment;
157+
}
158+
159+
160+
/**
161+
* Returns the entire URI including query string and fragment.
162+
*/
163+
public function getAbsoluteUrl(): string
164+
{
165+
return $this->getHostUrl() . $this->path
166+
. (($tmp = $this->getQuery()) ? '?' . $tmp : '')
167+
. ($this->fragment === '' ? '' : '#' . $this->fragment);
168+
}
169+
170+
171+
/**
172+
* Returns the [user[:pass]@]host[:port] part of URI.
173+
*/
174+
public function getAuthority(): string
175+
{
176+
return $this->host === ''
177+
? ''
178+
: ($this->user !== ''
179+
? rawurlencode($this->user) . ($this->password === '' ? '' : ':' . rawurlencode($this->password)) . '@'
180+
: '')
181+
. $this->host
182+
. ($this->port && (!isset(Url::$defaultPorts[$this->scheme]) || $this->port !== Url::$defaultPorts[$this->scheme])
183+
? ':' . $this->port
184+
: '');
185+
}
186+
187+
188+
/**
189+
* Returns the scheme and authority part of URI.
190+
*/
191+
public function getHostUrl(): string
192+
{
193+
return ($this->scheme ? $this->scheme . ':' : '')
194+
. (($authority = $this->getAuthority()) ? '//' . $authority : '');
195+
}
196+
197+
198+
public function __toString(): string
199+
{
200+
return $this->getAbsoluteUrl();
201+
}
202+
203+
204+
/**
205+
* @param string|Url|self $url
206+
*/
207+
public function isEqual($url): bool
208+
{
209+
return (new Url($this))->isEqual($url);
210+
}
211+
212+
213+
public function jsonSerialize(): string
214+
{
215+
return $this->getAbsoluteUrl();
216+
}
217+
218+
219+
/** @internal */
220+
final public function export(): array
221+
{
222+
return [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment];
223+
}
224+
}

tests/Http/UrlImmutable.Url.phpt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/**
4+
* Test: Nette\Http\UrlImmutable http://
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Nette\Http\Url;
10+
use Nette\Http\UrlImmutable;
11+
use Tester\Assert;
12+
13+
14+
require __DIR__ . '/../bootstrap.php';
15+
16+
$url = new UrlImmutable(new Url('http://username%3A:password%3A@hostn%61me:60/p%61th/script.php?%61rg=value#%61nchor'));
17+
18+
Assert::same('http://username%3A:password%3A@hostname:60/p%61th/script.php?arg=value#anchor', (string) $url);
19+
Assert::same('"http:\/\/username%3A:password%3A@hostname:60\/p%61th\/script.php?arg=value#anchor"', json_encode($url));
20+
Assert::same('http', $url->scheme);
21+
Assert::same('username:', $url->user);
22+
Assert::same('password:', $url->password);
23+
Assert::same('hostname', $url->host);
24+
Assert::same(60, $url->port);
25+
Assert::same('hostname', $url->getDomain());
26+
Assert::same('hostname', $url->getDomain(0));
27+
Assert::same('', $url->getDomain(-1));
28+
Assert::same('/p%61th/script.php', $url->path);
29+
Assert::same('arg=value', $url->query);
30+
Assert::same(['arg' => 'value'], $url->getQueryParameters());
31+
Assert::same('anchor', $url->fragment);
32+
Assert::same('username%3A:password%3A@hostname:60', $url->authority);
33+
Assert::same('http://username%3A:password%3A@hostname:60', $url->hostUrl);
34+
Assert::same('http://username%3A:password%3A@hostname:60/p%61th/script.php?arg=value#anchor', $url->absoluteUrl);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/**
4+
* Test: Nette\Http\UrlImmutable::isEqual()
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Nette\Http\UrlImmutable;
10+
use Tester\Assert;
11+
12+
13+
require __DIR__ . '/../bootstrap.php';
14+
15+
16+
$url = new UrlImmutable('http://exampl%65.COM/p%61th?text=foo%20bar+foo&value');
17+
Assert::true($url->isEqual('http://example.com/path?text=foo+bar%20foo&value'));
18+
Assert::true($url->isEqual('http://example.com/%70ath?value&text=foo+bar%20foo'));
19+
Assert::false($url->isEqual('http://example.com/Path?text=foo+bar%20foo&value'));
20+
Assert::false($url->isEqual('http://example.com/path?value&text=foo+bar%20foo#abc'));
21+
Assert::false($url->isEqual('http://example.com/path?text=foo+bar%20foo'));
22+
Assert::false($url->isEqual('https://example.com/path?text=foo+bar%20foo&value'));
23+
Assert::false($url->isEqual('http://example.org/path?text=foo+bar%20foo&value'));
24+
25+
26+
$url = new UrlImmutable('http://example.com');
27+
Assert::true($url->isEqual('http://example.com/'));
28+
Assert::true($url->isEqual('http://example.com'));
29+
30+
31+
$url = new UrlImmutable('http://example.com/?arr[]=item1&arr[]=item2');
32+
Assert::true($url->isEqual('http://example.com/?arr[0]=item1&arr[1]=item2'));
33+
Assert::false($url->isEqual('http://example.com/?arr[1]=item1&arr[0]=item2'));
34+
35+
36+
$url = new UrlImmutable('http://example.com/?a=9999&b=127.0.0.1&c=1234&d=123456789');
37+
Assert::true($url->isEqual('http://example.com/?d=123456789&a=9999&b=127.0.0.1&c=1234'));
38+
39+
40+
$url = new UrlImmutable('http://example.com/?a=123&b=456');
41+
Assert::false($url->isEqual('http://example.com/?a=456&b=123'));
42+
43+
44+
$url = new UrlImmutable('http://user:[email protected]');
45+
Assert::false($url->isEqual('http://example.com'));
46+
47+
48+
$url = new UrlImmutable('ftp://user:[email protected]');
49+
Assert::false($url->isEqual('ftp://example.com'));
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/**
4+
* Test: Nette\Http\UrlImmutable malformed URI.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Nette\Http\UrlImmutable;
10+
use Tester\Assert;
11+
12+
13+
require __DIR__ . '/../bootstrap.php';
14+
15+
16+
Assert::exception(function () {
17+
new UrlImmutable('http:///');
18+
}, InvalidArgumentException::class, "Malformed or unsupported URI 'http:///'.");

0 commit comments

Comments
 (0)