Skip to content

Commit

Permalink
Modifier codebase improved and fixed
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jan 29, 2025
1 parent e6f43d0 commit 2a410e5
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 78 deletions.
3 changes: 2 additions & 1 deletion components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ All Notable changes to `League\Uri\Components` will be documented in this file

### Fixed

- None
- `Modifier::getUriString` returns the result of calling `__tostring` on the underlying URI object being manipulated
- `Modifier` host related method return host in IDN form or ASCII form depending on the URI input format

### Deprecated

Expand Down
126 changes: 82 additions & 44 deletions components/Modifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public function getUri(): Psr7UriInterface|UriInterface

public function getUriString(): string
{
return $this->toString();
return $this->uri->__toString();
}

public function jsonSerialize(): string
Expand All @@ -88,7 +88,7 @@ public function __toString(): string

public function toString(): string
{
return $this->uri->__toString();
return ($this->uri instanceof UriRenderer) ? $this->uri->toString() : Uri::new($this->uri)->toString();
}

public function toDisplayString(): string
Expand Down Expand Up @@ -156,10 +156,9 @@ final public function when(callable|bool $condition, callable $onSuccess, ?calla
*/
public function encodeQuery(KeyValuePairConverter|int $to, KeyValuePairConverter|int|null $from = null): static
{
$to = match (true) {
!$to instanceof KeyValuePairConverter => KeyValuePairConverter::fromEncodingType($to),
default => $to,
};
if (!$to instanceof KeyValuePairConverter) {
$to = KeyValuePairConverter::fromEncodingType($to);
}

$from = match (true) {
null === $from => KeyValuePairConverter::fromRFC3986(),
Expand All @@ -172,14 +171,17 @@ public function encodeQuery(KeyValuePairConverter|int $to, KeyValuePairConverter
}

$originalQuery = $this->uri->getQuery();
if (null === $originalQuery || '' === trim($originalQuery)) {
return $this;
}

/** @var string $query */
$query = QueryString::buildFromPairs(QueryString::parseFromValue($originalQuery, $from), $to);
if ($query === $originalQuery) {
return $this;
}

return match (true) {
null === $query,
'' === $query,
$originalQuery === $query => $this,
default => new static($this->uri->withQuery($query)),
};
return new static($this->uri->withQuery($query));
}

/**
Expand Down Expand Up @@ -410,15 +412,28 @@ public function addRootLabel(): static
*/
public function appendLabel(Stringable|string|null $label): static
{
$host = Host::fromUri($this->uri);
$host = $this->uri->getHost();
$isAsciiDomain = null === $host || IdnaConverter::toAscii($host)->domain() === $host;

$host = Host::new($host);
$label = Host::new($label);

return match (true) {
null === $label->value() => $this,
$host->isDomain() => new static($this->uri->withHost(static::normalizeComponent(Domain::new($host)->append($label)->toUnicode(), $this->uri))),
$host->isIpv4() => new static($this->uri->withHost($host->value().'.'.ltrim($label->value(), '.'))),
default => throw new SyntaxError('The URI host '.$host->toString().' cannot be appended.'),
};
if (null === $label->value()) {
return $this;
}

if ($host->isIpv4()) {
return new static($this->uri->withHost($host->value().'.'.ltrim($label->value(), '.')));
}

if (!$host->isDomain()) {
throw new SyntaxError('The URI host '.$host->toString().' cannot be appended.');
}

$newHost = Domain::new($host)->append($label);
$newHost = !$isAsciiDomain ? $newHost->toUnicode() : $newHost->toAscii();

return new static($this->uri->withHost(static::normalizeComponent($newHost, $this->uri)));
}

/**
Expand Down Expand Up @@ -534,28 +549,45 @@ public function hostToIpv6Expanded(): static
*/
public function prependLabel(Stringable|string|null $label): static
{
$host = Host::fromUri($this->uri);
$host = $this->uri->getHost();
$isAsciiDomain = null === $host || IdnaConverter::toAscii($host)->domain() === $host;

$host = Host::new($host);
$label = Host::new($label);

return match (true) {
null === $label->value() => $this,
$host->isIpv4() => new static($this->uri->withHost(rtrim($label->value(), '.').'.'.$host->value())),
$host->isDomain() => new static($this->uri->withHost(static::normalizeComponent(Domain::new($host)->prepend($label)->toUnicode(), $this->uri))),
default => throw new SyntaxError('The URI host '.$host->toString().' cannot be prepended.'),
};
if (null === $label->value()) {
return $this;
}

if ($host->isIpv4()) {
return new static($this->uri->withHost(rtrim($label->value(), '.').'.'.$host->value()));
}

if (!$host->isDomain()) {
throw new SyntaxError('The URI host '.$host->toString().' cannot be prepended.');
}

$newHost = Domain::new($host)->prepend($label);
$newHost = !$isAsciiDomain ? $newHost->toUnicode() : $newHost->toAscii();

return new static($this->uri->withHost(static::normalizeComponent($newHost, $this->uri)));
}

/**
* Remove host labels according to their offset.
*/
public function removeLabels(int ...$keys): static
{
return new static($this->uri->withHost(
static::normalizeComponent(
Domain::fromUri($this->uri)->withoutLabel(...$keys)->toUnicode(),
$this->uri
)
));
$host = $this->uri->getHost();
if (null === $host || ('' === $host && $this->uri instanceof Psr7UriInterface)) {
return $this;
}

$isAsciiDomain = IdnaConverter::toAscii($host)->domain() === $host;
$newHost = Domain::new($host)->withoutLabel(...$keys);
$newHost = !$isAsciiDomain ? $newHost->toUnicode() : $newHost->toAscii();

return new static($this->uri->withHost(static::normalizeComponent($newHost, $this->uri)));
}

/**
Expand All @@ -579,13 +611,19 @@ public function removeRootLabel(): static
public function sliceLabels(int $offset, ?int $length = null): static
{
$currentHost = $this->uri->getHost();
if (null === $currentHost || ('' === $currentHost && $this->uri instanceof Psr7UriInterface)) {
return $this;
}

$isAsciiDomain = IdnaConverter::toAscii($currentHost)->domain() === $currentHost;
$host = Domain::new($currentHost)->slice($offset, $length);
$host = !$isAsciiDomain ? $host->toUnicode() : $host->toAscii();

return match (true) {
$host->value() === $currentHost,
$host->toUnicode() === $currentHost => $this,
default => new static($this->uri->withHost($host->toUnicode())),
};
if ($currentHost === $host) {
return $this;
}

return new static($this->uri->withHost($host));
}

/**
Expand All @@ -598,7 +636,7 @@ public function removeZoneId(): static
return match (true) {
$host->hasZoneIdentifier() => new static($this->uri->withHost(
static::normalizeComponent(
Host::fromUri($this->uri)->withoutZoneIdentifier()->value(),
$host->withoutZoneIdentifier()->value(),
$this->uri
)
)),
Expand All @@ -611,12 +649,12 @@ public function removeZoneId(): static
*/
public function replaceLabel(int $offset, Stringable|string|null $label): static
{
return new static($this->uri->withHost(
static::normalizeComponent(
Domain::fromUri($this->uri)->withLabel($offset, $label)->toUnicode(),
$this->uri
)
));
$host = $this->uri->getHost();
$isAsciiDomain = null === $host || IdnaConverter::toAscii($host)->domain() === $host;
$newHost = Domain::new($host)->withLabel($offset, $label);
$newHost = !$isAsciiDomain ? $newHost->toUnicode() : $newHost->toAscii();

return new static($this->uri->withHost(static::normalizeComponent($newHost, $this->uri)));
}

public function whatwgHost(): static
Expand Down
8 changes: 4 additions & 4 deletions components/ModifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -436,9 +436,9 @@ public function testModifyingTheHostKeepHostUnicode(): void

$modifier = Modifier::from(Utils::uriFor('http://shop.bebe.be'));

self::assertSame('http://bébé.bebe.be', $modifier->replaceLabel(-1, 'bébé')->getUriString());
self::assertSame('http://bébé.shop.bebe.be', $modifier->prependLabel('bébé')->getUriString());
self::assertSame('http://shop.bebe.be.bébé', $modifier->appendLabel('bébé')->getUriString());
self::assertSame('http://xn--bb-bjab.bebe.be', $modifier->replaceLabel(-1, 'bébé')->getUriString());
self::assertSame('http://xn--bb-bjab.shop.bebe.be', $modifier->prependLabel('bébé')->getUriString());
self::assertSame('http://shop.bebe.be.xn--bb-bjab', $modifier->appendLabel('bébé')->getUriString());
self::assertSame('http://shop.bebe.be', $modifier->hostToAscii()->getUriString());
self::assertSame('http://shop.bebe.be', $modifier->hostToUnicode()->getUriString());
}
Expand Down Expand Up @@ -469,7 +469,7 @@ public function testItCanConvertHostToUnicode(): void

self::assertSame('http://xn--bb-bjab.be', $uri);
self::assertSame('http://xn--bb-bjab.be', (string) $modifier);
self::assertSame($uriString, (string) $modifier->hostToUnicode());
self::assertSame($uriString, $modifier->hostToUnicode()->getUriString());
}

public function testICanNormalizeIPv4HostToDecimal(): void
Expand Down
81 changes: 52 additions & 29 deletions docs/components/7.0/modifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,46 +73,43 @@ echo $uri::class; // returns GuzzleHttp\Psr7\Uri
echo $uri, PHP_EOL; // returns http://shop.bébé.be./toto?foo=toto&foo=tata
~~~

### Returned URI object and string representations

<p class="message-warning">While the class does manipulate URI it does not implement any URI related interface.</p>
<p class="message-notice">If a PSR-7 or a League <code>UriInterface</code> implementing instance is given
then the return value will also be a PSR-7 <code>UriInterface</code> implementing instance.</p>
<p class="message-notice">The <code>getIdnUriString</code> method is available since version <code>7.5.0</code>.</p>
<p class="message-notice">If an <code>UriInterface</code> implementing instance is given, then the returned URI object will also be of the same <code>UriInterface</code> type.</p>

The `Modifier::getUri` method returns either a `PSR-7` or a League URI `UriInterface`, conversely,
the `Modifier::getUriString` method returns the RFC3986 string representation for the URI and
the `Modifier::getIdnUriString` method returns the RFC3986 string representation for the URI
with a Internationalized Domain Name (IDNA) if applicable. Last but not least, the class
implements the `Stringable` and the `JsonSerializable` interface to improve developer experience.
The `Modifier` can return different URI results depending on the context and your usage.

Under the hood the `Modifier` class intensively uses the [URI components objects](/components/7.0/)
to apply changes to the submitted URI object.
The `Modifier::getUri` method returns a League URI `UriInterface` unless you instantiated the modifier
with a `PSR-7` Uri object in which case a `PSR-7` Uri object of the same type is returned. If you
are not interested in the returned URI but only on its underlying string representation, you can instead use
the `Modifier::getUriString` which is a shortcut to `Modifier::getUri->__toString()`.

<p class="message-notice">The <code>when</code>, <code>toString</code> and <code>toDisplayString</code> methods are available since version <code>7.6.0</code></p>
<p class="message-notice">Available since version <code>7.6.0</code>

To ease modifying URI since version 7.6.0 you can directly access the modifier methods from the underlying
URI object. The methods behave as their owner so T
the `Modifier::toString` method returns the **strict** RFC3986 string representation of the URI regardless of the underlying URI object string representation.
This is the representation used by the `Stringable` and the `JsonSerializable` interface to improve interoperability.

```php
use League\Uri\Modifier;
The `Modifier::toDisplayString` method returns a RFC3987 like string representation which is more suited for
displaying the URI and should not be used to interact with an API as the produced URI may not be RFC3986 compliant
at all.

$foo = '';
echo Modifier::from('http://bébé.be')
->when(
'' !== $foo,
fn (Modifier $uri) => $uri->withQuery('fname=jane&lname=Doe'), //on true
fn (Modifier $uri) => $uri->mergeQueryParameters(['fname' => 'john', 'lname' => 'Doe']), //on false
)
->appendSegment('toto')
->addRootLabel()
->prependLabel('shop')
->appendQuery('foo=toto&foo=tata')
->withFragment('chapter1')
->toDisplayString();
// returns 'http://shop.bébé.be./toto?fname=john&lname=Doe&foo=toto&foo=tata#chapter1';
```php
use GuzzleHttp\Psr7\Utils;

$uri = Modifier::from(Utils::uriFor('https://bébé.be?foo[]=bar'))->prepend('shop');
$uri->getUri()::class; // returns 'GuzzleHttp\Psr7\Uri'
$uri->getUri()->__toString(); // returns 'https://shop.bébé.be?foo%5B%5D=bar'
$uri->getUriString(); // returns 'https://shop.bébé.be?foo%5B%5D=bar'
$uri->toString(); // returns 'https://shop.xn--bb-bjab.be?foo%5B%5D=bar'
$uri->toDisplayString(); // returns 'https://shop.bébé.be?foo[]=bar'
```

### Available modifiers

Under the hood the `Modifier` class intensively uses the [URI components objects](/components/7.0/)
to apply the following changes to the submitted URI.

<div class="flex flex-row flex-wrap">
<div>
<ul>
Expand Down Expand Up @@ -864,3 +861,29 @@ echo Modifier::from($uri)
->getPath();
//display "text/plain;charset=US-ASCII,Hello%20World!"
~~~

### General modification

<p class="message-notice">The <code>when</code> methods is available since version <code>7.6.0</code></p>

To ease modifying URI since version 7.6.0 you can directly access the modifier methods from the underlying
URI object.

```php
use League\Uri\Modifier;

$foo = '';
echo Modifier::from('http://bébé.be')
->when(
'' !== $foo,
fn (Modifier $uri) => $uri->withQuery('fname=jane&lname=Doe'), //on true
fn (Modifier $uri) => $uri->mergeQueryParameters(['fname' => 'john', 'lname' => 'Doe']), //on false
)
->appendSegment('toto')
->addRootLabel()
->prependLabel('shop')
->appendQuery('foo=toto&foo=tata')
->withFragment('chapter1')
->toDisplayString();
// returns 'http://shop.bébé.be./toto?fname=john&lname=Doe&foo=toto&foo=tata#chapter1';
```

0 comments on commit 2a410e5

Please sign in to comment.