Skip to content

Commit 9382af6

Browse files
committed
Improve Directive support in URI
1 parent 457f548 commit 9382af6

File tree

5 files changed

+222
-20
lines changed

5 files changed

+222
-20
lines changed

components/Modifier.php

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,60 @@ public function toHtmlAnchor(?string $linkTextTemplate = null, iterable $attribu
164164
return $html;
165165
}
166166

167+
public function resolve(Rfc3986Uri|WhatWgUrl|Psr7UriInterface|UriInterface|Stringable|string $uri): static
168+
{
169+
$uriString = match (true) {
170+
$uri instanceof Rfc3986Uri,
171+
$uri instanceof UriInterface => $uri->toString(),
172+
$uri instanceof WhatWgUrl => $uri->toAsciiString(),
173+
default => (string) $uri,
174+
};
175+
176+
if (!$this->uri instanceof Psr7UriInterface) {
177+
return new static($this->uri->resolve($uriString));
178+
}
179+
180+
$components = UriString::parse(UriString::resolve($uriString, $this->toString()));
181+
182+
return new static(
183+
$this->uri
184+
->withFragment($components['fragment'] ?? '')
185+
->withQuery($components['query'] ?? '')
186+
->withPath($components['path'] ?? '')
187+
->withHost($components['host'] ?? '')
188+
->withPort($components['port'] ?? null)
189+
->withUserInfo($components['user'] ?? '', $components['pass'] ?? null)
190+
->withScheme($components['scheme'] ?? '')
191+
);
192+
}
193+
194+
public function normalize(): static
195+
{
196+
if ($this->uri instanceof WhatWgUrl) {
197+
return $this;
198+
}
199+
200+
if ($this->uri instanceof Rfc3986Uri) {
201+
return new static(new Rfc3986Uri($this->uri->toString()));
202+
}
203+
204+
if ($this->uri instanceof UriInterface) {
205+
return new static($this->uri->normalize());
206+
}
207+
208+
$uri = Uri::new($this->uri->__toString())->normalize();
209+
if ($uri->toString() === $this->uri->__toString()) {
210+
return $this;
211+
}
212+
213+
return new static(
214+
$this->uri
215+
->withPath($uri->getPath())
216+
->withHost($uri->getHost() ?? '')
217+
->withUserInfo($uri->getUsername() ?? '', $uri->getPassword())
218+
);
219+
}
220+
167221
public function withScheme(Stringable|string|null $scheme): static
168222
{
169223
return new static($this->uri->withScheme(self::normalizeComponent($scheme, $this->uri)));
@@ -205,7 +259,6 @@ public function withUserInfo(Stringable|string|null $user, Stringable|string|nul
205259
public function withQuery(Stringable|string|null $query): static
206260
{
207261
$query = self::normalizeComponent($query, $this->uri);
208-
209262
$query = match (true) {
210263
$this->uri instanceof Rfc3986Uri => match (true) {
211264
Encoder::isQueryEncoded($query) => $query,

components/ModifierTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#[Group('resolution')]
3636
final class ModifierTest extends TestCase
3737
{
38+
private const BASE_URI = 'http://a/b/c/d;p?q';
3839
private readonly string $uri;
3940
private readonly Modifier $modifier;
4041

@@ -1063,4 +1064,55 @@ public static function providesDirectivesToPrepend(): iterable
10631064
'expectedFragment' => ":~:text=start,en'd&unknownDirective",
10641065
];
10651066
}
1067+
1068+
#[DataProvider('resolveProvider')]
1069+
public function testCreateResolve(string $baseUri, string $uri, string $expected): void
1070+
{
1071+
self::assertSame($expected, Modifier::from(Utils::uriFor($baseUri))->resolve($uri)->toString());
1072+
}
1073+
1074+
public static function resolveProvider(): array
1075+
{
1076+
return [
1077+
'base uri' => [self::BASE_URI, '', self::BASE_URI],
1078+
'scheme' => [self::BASE_URI, 'http://d/e/f', 'http://d/e/f'],
1079+
'path 1' => [self::BASE_URI, 'g', 'http://a/b/c/g'],
1080+
'path 2' => [self::BASE_URI, './g', 'http://a/b/c/g'],
1081+
'path 3' => [self::BASE_URI, 'g/', 'http://a/b/c/g/'],
1082+
'path 4' => [self::BASE_URI, '/g', 'http://a/g'],
1083+
'authority' => [self::BASE_URI, '//g', 'http://g'],
1084+
'query' => [self::BASE_URI, '?y', 'http://a/b/c/d;p?y'],
1085+
'path + query' => [self::BASE_URI, 'g?y', 'http://a/b/c/g?y'],
1086+
'fragment' => [self::BASE_URI, '#s', 'http://a/b/c/d;p?q#s'],
1087+
'path + fragment' => [self::BASE_URI, 'g#s', 'http://a/b/c/g#s'],
1088+
'path + query + fragment' => [self::BASE_URI, 'g?y#s', 'http://a/b/c/g?y#s'],
1089+
'single dot 1' => [self::BASE_URI, '.', 'http://a/b/c/'],
1090+
'single dot 2' => [self::BASE_URI, './', 'http://a/b/c/'],
1091+
'single dot 3' => [self::BASE_URI, './g/.', 'http://a/b/c/g/'],
1092+
'single dot 4' => [self::BASE_URI, 'g/./h', 'http://a/b/c/g/h'],
1093+
'double dot 1' => [self::BASE_URI, '..', 'http://a/b/'],
1094+
'double dot 2' => [self::BASE_URI, '../', 'http://a/b/'],
1095+
'double dot 3' => [self::BASE_URI, '../g', 'http://a/b/g'],
1096+
'double dot 4' => [self::BASE_URI, '../..', 'http://a/'],
1097+
'double dot 5' => [self::BASE_URI, '../../', 'http://a/'],
1098+
'double dot 6' => [self::BASE_URI, '../../g', 'http://a/g'],
1099+
'double dot 7' => [self::BASE_URI, '../../../g', 'http://a/g'],
1100+
'double dot 8' => [self::BASE_URI, '../../../../g', 'http://a/g'],
1101+
'double dot 9' => [self::BASE_URI, 'g/../h' , 'http://a/b/c/h'],
1102+
'mulitple slashes' => [self::BASE_URI, 'foo////g', 'http://a/b/c/foo////g'],
1103+
'complex path 1' => [self::BASE_URI, ';x', 'http://a/b/c/;x'],
1104+
'complex path 2' => [self::BASE_URI, 'g;x', 'http://a/b/c/g;x'],
1105+
'complex path 3' => [self::BASE_URI, 'g;x?y#s', 'http://a/b/c/g;x?y#s'],
1106+
'complex path 4' => [self::BASE_URI, 'g;x=1/./y', 'http://a/b/c/g;x=1/y'],
1107+
'complex path 5' => [self::BASE_URI, 'g;x=1/../y', 'http://a/b/c/y'],
1108+
'dot segments presence 1' => [self::BASE_URI, '/./g', 'http://a/g'],
1109+
'dot segments presence 2' => [self::BASE_URI, '/../g', 'http://a/g'],
1110+
'dot segments presence 3' => [self::BASE_URI, 'g.', 'http://a/b/c/g.'],
1111+
'dot segments presence 4' => [self::BASE_URI, '.g', 'http://a/b/c/.g'],
1112+
'dot segments presence 5' => [self::BASE_URI, 'g..', 'http://a/b/c/g..'],
1113+
'dot segments presence 6' => [self::BASE_URI, '..g', 'http://a/b/c/..g'],
1114+
'origin uri without path' => ['http://h:b@a', 'b/../y', 'http://h:b@a/y'],
1115+
'not same origin' => [self::BASE_URI, 'ftp://a/b/c/d', 'ftp://a/b/c/d'],
1116+
];
1117+
}
10661118
}

docs/components/7.0/modifiers.md

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ to apply the following changes to the submitted URI.
123123
<div>
124124
<h4>Query modifiers</h4>
125125
<ul>
126+
<li><a href="#modifierwithquery">withQuery</a></li>
126127
<li><a href="#modifierencodequery">encodeQuery</a></li>
127128
<li><a href="#modifiersortquery">sortQuery</a></li>
128129
<li><a href="#modifiermergequery">mergeQuery</a></li>
@@ -137,19 +138,9 @@ to apply the following changes to the submitted URI.
137138
</ul>
138139
</div>
139140
<div>
140-
<h4>Fragment modifiers <span class="text-red-800 text-sm">since <code class="text-sm">7.6.0</code></span></h4>
141-
<ul>
142-
<li><a href="#modifierappendfragmentdirectives">appendFragmentDirectives</a></li>
143-
<li><a href="#modifierprependfragmentdirectives">prependFragmentDirectives</a></li>
144-
<li><a href="#modifierremovefragmentdirectives">removeFragmentDirectives</a></li>
145-
<li><a href="#modifierreplacefragmentdirective">replaceFragmentDirective</a></li>
146-
<li><a href="#modifierfilterfragmentdirectives">filterFragmentDirectives</a></li>
147-
<li><a href="#modifierslicefragmentdirectives">sliceFragmentDirectives</a></li>
148-
</ul>
149-
</div>
150-
<div>
151141
<h4>Host modifiers</h4>
152142
<ul>
143+
<li><a href="#modifierwithhost">withHost</a></li>
153144
<li><a href="#modifiernormalizehost">normalizeHost</a></li>
154145
<li><a href="#modifierhosttoascii">hostToAscii</a></li>
155146
<li><a href="#modifierhosttounicode">hostToUnicode</a></li>
@@ -171,6 +162,7 @@ to apply the following changes to the submitted URI.
171162
<div>
172163
<h4>Path modifiers</h4>
173164
<ul>
165+
<li><a href="#modifierwithpath">withPath</a></li>
174166
<li><a href="#modifierremovedotsegments">removeDotSegments</a></li>
175167
<li><a href="#modifierremoveemptysegments">removeEmptySegments</a></li>
176168
<li><a href="#modifierremovetrailingslash">removeTrailingSlash</a></li>
@@ -192,16 +184,54 @@ to apply the following changes to the submitted URI.
192184
<li><a href="#modifierdatapathtoascii">dataPathToAscii</a></li>
193185
</ul>
194186
</div>
187+
<div>
188+
<h4>Fragment modifiers <span class="text-red-800 text-sm">since <code class="text-sm">7.6.0</code></span></h4>
189+
<ul>
190+
<li><a href="#modifierwithfragment">withFragment</a></li>
191+
<li><a href="#modifierappendfragmentdirectives">appendFragmentDirectives</a></li>
192+
<li><a href="#modifierprependfragmentdirectives">prependFragmentDirectives</a></li>
193+
<li><a href="#modifierremovefragmentdirectives">removeFragmentDirectives</a></li>
194+
<li><a href="#modifierreplacefragmentdirective">replaceFragmentDirective</a></li>
195+
<li><a href="#modifierfilterfragmentdirectives">filterFragmentDirectives</a></li>
196+
<li><a href="#modifierslicefragmentdirectives">sliceFragmentDirectives</a></li>
197+
</ul>
198+
</div>
199+
<div>
200+
<h4>Component modifiers <span class="text-red-800 text-sm">since <code class="text-sm">7.6.0</code></span></h4>
201+
<ul>
202+
<li><a href="#modifierwithscheme">withScheme</a></li>
203+
<li><a href="#modifierwithuserinfo">withUserInfo</a></li>
204+
<li><a href="#modifierwithport">withPort</a></li>
205+
<li><a href="#modifierresovlve">URI resolve</a></li>
206+
</ul>
207+
</div>
195208
</div>
196209

197-
## Query modifiers
198210

199-
The following modifiers update and normalize the URI query component.
211+
## Query Modifiers
212+
213+
Following modifiers update and normalize the URI query component.
200214

201215
<p class="message-notice">Because each modification is done after parsing and building, the
202216
resulting query string may update the component character encoding. These changes are expected because of
203217
the rules governing parsing and building query string.</p>
204218

219+
### Modifier::withQuery
220+
221+
<p class="message-notice">since version <code>7.6.0</code></p>
222+
223+
Change the full query component.
224+
225+
~~~php
226+
use League\Uri\Modifier;
227+
228+
echo Modifier::from("https://example.com/?kingkong=toto&foo=bar%20baz&kingkong=ape")
229+
->withQuery('foo=bar')
230+
->uri()
231+
->getQuery();
232+
//display "foo=bar"
233+
~~~
234+
205235
### Modifier::encodeQuery
206236

207237
<p class="message-notice">since version <code>7.1.0</code></p>
@@ -384,7 +414,23 @@ echo $newUri->uri()->getQuery(); //display "kingkong=toto&fo.o=champion&fo_o=bar
384414

385415
## Host modifiers
386416

387-
The following modifiers update and normalize the URI host component according to RFC3986 or RFC3987.
417+
The following methods update and normalize the URI host component according to the underlying URI object.
418+
419+
### Modifier::withHost
420+
421+
<p class="message-notice">since version <code>7.6.0</code></p>
422+
423+
Change the full query component.
424+
425+
~~~php
426+
use League\Uri\Modifier;
427+
428+
echo Modifier::from("https://example.com/?kingkong=toto&foo=bar%20baz&kingkong=ape")
429+
->withHost('hello.be')
430+
->uri()
431+
->getHost();
432+
//display "hello.be"
433+
~~~
388434

389435
### Modifier::hostToAscii
390436

@@ -664,6 +710,22 @@ echo Modifier::from($uri)->sliceLabels(1, 1)->toString();
664710
the resulting path may update the component character encoding. These changes are
665711
expected because of the rules governing parsing and building path string.</p>
666712

713+
### Modifier::withPath
714+
715+
<p class="message-notice">since version <code>7.6.0</code></p>
716+
717+
Change the full path component.
718+
719+
~~~php
720+
use League\Uri\Modifier;
721+
722+
echo Modifier::from("https://example.com/?kingkong=toto&foo=bar%20baz&kingkong=ape")
723+
->withPath('/path/to')
724+
->uri()
725+
->getPath();
726+
//display "/path/to"
727+
~~~
728+
667729
### Modifier::removeDotSegments
668730

669731
Removes dot segments according to RFC3986:
@@ -906,6 +968,22 @@ echo Modifier::from($uri)
906968

907969
## Fragment Modifiers
908970

971+
### Modifier::withFragment
972+
973+
<p class="message-notice">since version <code>7.6.0</code></p>
974+
975+
Change the full fragment component.
976+
977+
~~~php
978+
use League\Uri\Modifier;
979+
980+
echo Modifier::from("https://example.com/?kingkong=toto&foo=bar%20baz&kingkong=ape")
981+
->withFragment('/path/to')
982+
->uri()
983+
->getFragment();
984+
//display "/path/to"
985+
~~~
986+
909987
### Modifier::appendFragmentDirectives
910988

911989
<p class="message-notice">available since version <code>7.6.0</code></p>
@@ -1008,15 +1086,22 @@ echo Modifier::from($uri)
10081086
// display ":~:text=foo,bar&text=yes"
10091087
~~~
10101088

1011-
### General modification
1089+
## General modifications
10121090

10131091
<p class="message-notice">available since version <code>7.6.0</code></p>
10141092

1015-
To ease modifying URI since version 7.6.0 you can directly access the modifier methods from the underlying
1016-
URI object.
1093+
To ease modifying URI since version `7.6.0` you can directly access:
1094+
1095+
- the modifier methods from the underlying URI object or
1096+
- resolve an URI base on the underlying URI object rules or
1097+
- normalize an URI base on the underlying URI object rules.
1098+
1099+
The difference being that the `Modifier` class will perform the correct conversion
1100+
to handle the differences between URI object signature.
10171101

10181102
```php
10191103
use League\Uri\Modifier;
1104+
use Uri\WhatWg\Url;
10201105

10211106
$foo = '';
10221107
echo Modifier::from('http://bébé.be')
@@ -1032,4 +1117,11 @@ echo Modifier::from('http://bébé.be')
10321117
->withFragment('chapter1')
10331118
->toDisplayString();
10341119
// returns 'http://shop.bébé.be./toto?fname=john&lname=Doe&foo=toto&foo=tata#chapter1';
1120+
1121+
echo Modifier::from(new Url('http://bébé.be/../do/it'))
1122+
->appendSegment('toto')
1123+
->resolve('./foo/../bar')
1124+
->uri()
1125+
->toAsciiString(), PHP_EOL;
1126+
// returns http://xn--bb-bjab.be/do/it/bar
10351127
```

polyfill/lib/WhatWg/Url.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ public static function parse(string $uri, ?self $baseUrl = null, array &$errors
7979
public function __construct(string $uri, ?self $baseUrl = null, array &$softErrors = [])
8080
{
8181
$collector = new UrlValidationErrorCollector();
82-
8382
try {
8483
$this->url = new WhatWgURL($uri, $baseUrl?->url->href, ['logger' => $collector]);
8584
} catch (Exception $exception) {

uri/Uri.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use DOMDocument;
2020
use DOMException;
2121
use finfo;
22+
use League\Uri\Components\FragmentDirectives;
23+
use League\Uri\Components\FragmentDirectives\Directive;
2224
use League\Uri\Contracts\Conditionable;
2325
use League\Uri\Contracts\UriComponentInterface;
2426
use League\Uri\Contracts\UriException;
@@ -1596,6 +1598,10 @@ public function withQuery(Stringable|string|null $query): static
15961598

15971599
public function withFragment(Stringable|string|null $fragment): static
15981600
{
1601+
if ($fragment instanceof Directive) {
1602+
$fragment = new FragmentDirectives($fragment);
1603+
}
1604+
15991605
$fragment = Encoder::encodeQueryOrFragment($this->filterString($fragment));
16001606

16011607
return match ($fragment) {
@@ -1755,9 +1761,9 @@ public function resolve(Rfc3986Uri|WhatWgUrl|Stringable|string $uri): static
17551761
{
17561762
return self::new(UriString::resolve(
17571763
match (true) {
1764+
$uri instanceof UriInterface,
17581765
$uri instanceof Rfc3986Uri => $uri->toString(),
17591766
$uri instanceof WhatWgUrl => $uri->toAsciiString(),
1760-
$uri instanceof Stringable => (string) $uri,
17611767
default => $uri,
17621768
},
17631769
$this->toString()

0 commit comments

Comments
 (0)