Skip to content

Commit

Permalink
Adding more URI string representation (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod authored Dec 24, 2024
1 parent 5d5875c commit 19874d1
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 117 deletions.
1 change: 0 additions & 1 deletion components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ All Notable changes to `League\Uri\Components` will be documented in this file
- `Modifier::prependQueryParameters` returns a modifier with prepend query paramters
- `Modifier::when` conditional method to ease component building logic.
- `Modifier::with*` method from the underlying `Uri` object are proxy to improve DX.
- `Modifier::displayUriString` shows the URI in a human-readable format which may be an invalid URI.
- `Query::decoded` the string representation of the component decoded.
- `URLSearchParams::decoded` the string representation of the component decoded.
- `tryNew` named constructor added to all components class to returns a new instance on success or `null` on failure.
Expand Down
27 changes: 0 additions & 27 deletions components/Modifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
use League\Uri\Components\Host;
use League\Uri\Components\Path;
use League\Uri\Components\Query;
use League\Uri\Components\UserInfo;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\UriAccess;
Expand Down Expand Up @@ -94,32 +93,6 @@ public function __toString(): string
return $this->uri->__toString();
}

public function displayUriString(): string
{
$userInfo = UserInfo::fromUri($this->uri);
$host = $this->uri->getHost();
if (null !== $host) {
$hostIp = self::ipv4Converter()->toDecimal($host);
$host = IdnConverter::toUnicode((string) IPv6Converter::compress(match (true) {
'' === $host,
null === $hostIp,
$host === $hostIp => $host,
default => $hostIp,
}))->domain();
}

return UriString::build([
'scheme' => $this->uri->getScheme(),
'user' => $userInfo->getUser(),
'pass' => $userInfo->getPass(),
'host' => $host,
'port' => $this->uri->getPort(),
'path' => Path::fromUri($this->uri)->withoutDotSegments()->decoded(),
'query' => Query::fromUri($this->uri)->decoded(),
'fragment' => Fragment::fromUri($this->uri)->decoded(),
]);
}

final public function __call(string $name, array $arguments): static
{
static $allowedMethods = [
Expand Down
74 changes: 0 additions & 74 deletions components/ModifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -832,35 +832,6 @@ public function testItCanSlicePathSegments(): void
self::assertSame('http://www.localhost.com/the/sky/', Modifier::from($uri)->sliceSegments(2, 2)->getUriString());
}

#[DataProvider('idnUriProvider')]
public function testItReturnsTheCorrectUriString(string $expected, string $input): void
{
self::assertSame($expected, Modifier::from($input)->displayUriString());
}

public static function idnUriProvider(): iterable
{
yield 'basic uri stays the same' => [
'expected' => 'http://example.com/foo/bar',
'input' => 'http://example.com/foo/bar',
];

yield 'idn host are changed' => [
'expected' => 'http://bébé.be',
'input' => 'http://xn--bb-bjab.be',
];

yield 'idn host are the same' => [
'expected' => 'http://bébé.be',
'input' => 'http://bébé.be',
];

yield 'the rest of the URI is not affected and uses RFC3986 rules' => [
'expected' => 'http://bébé.be?q=toto le héros',
'input' => 'http://bébé.be:80?q=toto%20le%20h%C3%A9ros',
];
}

#[DataProvider('ipv6NormalizationUriProvider')]
public function testItCanExpandOrCompressTheHost(
string $inputUri,
Expand Down Expand Up @@ -912,49 +883,4 @@ public function it_will_remove_empty_pairs_fix_issue_133(): void
self::assertNull($removeEmptyPairs('https://a.b/c?=d'));
self::assertNull($removeEmptyPairs('https://a.b/c?='));
}

#[Test]
#[DataProvider('providesUriToDisplay')]
public function it_will_generate_the_display_uri_string(string $input, string $output): void
{
self::assertSame($output, Modifier::from($input)->displayUriString());
}

public static function providesUriToDisplay(): iterable
{
yield 'empty string' => [
'input' => '',
'output' => '',
];

yield 'host IPv6' => [
'input' => 'https://[fe80:0000:0000:0000:0000:0000:0000:000a%25en1]/foo/bar',
'output' => 'https://[fe80::a%en1]/foo/bar',
];

yield 'IPv6 gets expanded if needed' => [
'input' => 'http://bébé.be?q=toto%20le%20h%C3%A9ros',
'output' => 'http://bébé.be?q=toto le héros',
];

yield 'complex URI' => [
'input' => 'https://xn--google.com/secret/../search?q=%F0%9F%8D%94',
'output' => 'https://䕮䕵䕶䕱.com/search?q=🍔',
];

yield 'basic uri stays the same' => [
'input' => 'http://example.com/foo/bar',
'output' => 'http://example.com/foo/bar',
];

yield 'idn host are changed' => [
'input' => 'http://xn--bb-bjab.be',
'output' => 'http://bébé.be',
];

yield 'idn host are the same' => [
'input' => 'http://bébé.be',
'output' => 'http://bébé.be',
];
}
}
31 changes: 27 additions & 4 deletions docs/uri/7.0/rfc3986.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,6 @@ echo $uri->getPath(); //displays "/how/are/you"
echo $uri->getQuery(); //displays "foo=baz"
echo $uri->getFragment(); //displays "title"
echo $uri->getOrigin(); //returns ''
echo $uri->toString();
//displays "http://foo:[email protected]:81/how/are/you?foo=baz#title"
echo json_encode($uri);
//displays "http:\/\/foo:[email protected]:81\/how\/are\/you?foo=baz#title"
$uri->getComponents();
// returns array {
// "scheme" => "http",
Expand Down Expand Up @@ -253,6 +249,32 @@ Uri::new('https://example.com/123')

The method takes into account i18n while comparing both URI if the PHP's `idn_*` functions can be used.

## URI string representation

The `Uri` class handles URI according to RFC3986 as such you can retrieve its string representation using the
`toString` method. But `URI` can have multiple string representation depending on its scheme or context. As
such the package provides several other string representations:

```php
use League\Uri\Uri;

$uri = Uri::new("http://foo:[email protected]:81/how/are/you?foo=baz#title");

echo $uri->toString(); //displays RFC3986 string representation
echo $uri; //displays RFC3986 string representation
echo json_encode($uri); //display JSON encoded string representation

/**
* NEW in version 7.6+
*/

echo $uri->toNormalizedString(); //displays the normalized URI string representation
echo $uri->toDisplayString(); //displays the URI display representation
echo $uri->toRfc8089String(); //display the string file representation according to RFC8089 or null if the scheme is not file
echo $uri->toUnixPath(); //display the string path as a Unix Path or null if the scheme is not file
echo $uri->toWindowsPath(); //display the string path as a Windows Path or null if the scheme is not file
```

## Modifying URI properties

Use the modifying methods exposed by all URI instances to replace one of the URI component.
Expand Down Expand Up @@ -379,3 +401,4 @@ $uri->equals('eXAMPLE://a/./b/../b/%63/%7bfoo%7d', excludeFragment: false); // r

In the last example the `equals` method took into account the URI `fragment` component. The `isSameDocument`
follow closely RFC3986 and never takes into account the URI `fragment` component.

3 changes: 3 additions & 0 deletions interfaces/Contracts/UriInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
*
* @method string|null getUsername() returns the user component of the URI.
* @method string|null getPassword() returns the scheme-specific information about how to gain authorization to access the resource.
* @method string|null toUnixPath() returns the Unix filesystem path. The method returns null for any other scheme
* @method string|null toWindowsPath() returns the Windows filesystem path. The method returns null for any other scheme
* @method string|null toRfc8089() returns a string representation of a File URI according to RFC8089. The method returns null for any other scheme
* @method string toNormalizedString() returns the normalized string representation of the URI
* @method array toComponents() returns an associative array containing all the URI components.
* @method self normalize() returns a new URI instance with normalized components
Expand Down
4 changes: 4 additions & 0 deletions uri/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ All Notable changes to `League\Uri` will be documented in this file
- `Uri::getOrigin`
- `Uri::isSameOrigin`
- `Uri::isCrossOrigin`
- `Uri::todisplayString` shows the URI in a human-readable format which may be an invalid URI.
- `Uri::toUnixPath` returns the URI path as a Unix Path or `null`
- `Uri::toWindowsPath` returns the URI path as a Windows Path or `null`
- `Uri::toRfc8089` return the URI in a RFC8089 formator `null`

### Fixed

Expand Down
104 changes: 93 additions & 11 deletions uri/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -1000,25 +1000,112 @@ private function isNonEmptyHostUriWithoutFragmentAndQuery(): bool
return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query;
}

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

public function jsonSerialize(): string
{
return $this->toString();
}

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

public function toNormalizedString(): string
{
return $this->normalize()->toString();
}

public function toDisplayString(): string
{
/** @var ComponentMap $components */
$components = array_map(
fn (?string $value): ?string => (null === $value || '' === $value) ? $value : rawurldecode($value),
$this->normalize()->toComponents()
);

if (null !== $components['host']) {
$components['host'] = IdnaConverter::toUnicode($components['host'])->domain();
}

if ('/' === $components['path'] && null !== $this->authority) {
$components['path'] = '';
}

return UriString::build($components);
}

/**
* {@inheritDoc}
* Returns the Unix filesystem path.
*
* The method will return null if a scheme is present and is not the `file` scheme
*/
public function __toString(): string
public function toUnixPath(): ?string
{
return $this->toString();
return match ($this->scheme) {
'file', null => rawurldecode($this->path),
default => null,
};
}

/**
* {@inheritDoc}
* Returns the Windows filesystem path.
*
* The method will return null if a scheme is present and is not the `file` scheme
*/
public function jsonSerialize(): string
public function toWindowsPath(): ?string
{
return $this->toString();
static $regexpWindowsPath = ',^(?<root>[a-zA-Z]:),';

if (!in_array($this->scheme, ['file', null], true)) {
return null;
}

$originalPath = $this->path;
$path = $originalPath;
if ('/' === ($path[0] ?? '')) {
$path = substr($path, 1);
}

if (1 === preg_match($regexpWindowsPath, $path, $matches)) {
$root = $matches['root'];
$path = substr($path, strlen($root));

return $root.str_replace('/', '\\', rawurldecode($path));
}

$host = $this->host;

return match (null) {
$host => str_replace('/', '\\', rawurldecode($originalPath)),
default => '\\\\'.$host.'\\'.str_replace('/', '\\', rawurldecode($path)),
};
}

/**
* Returns a string representation of a File URI according to RFC8089.
*
* The method will return null if the URI scheme is not the `file` scheme
*
* @see https://datatracker.ietf.org/doc/html/rfc8089
*/
public function toRfc8089(): ?string
{
$path = $this->path;

return match (true) {
'file' !== $this->scheme => null,
in_array($this->authority, ['', null, 'localhost'], true) => 'file:'.match (true) {
'' === $path,
'/' === $path[0] => $path,
default => '/'.$path,
},
default => $this->toString(),
};
}

/**
Expand Down Expand Up @@ -1408,11 +1495,6 @@ public function equals(UriInterface|Stringable|string $uri, bool $excludeFragmen
};
}

public function toNormalizedString(): string
{
return $this->normalize()->toString();
}

/**
* Tells whether the URI contains an Internationalized Domain Name (IDN).
*/
Expand Down
Loading

0 comments on commit 19874d1

Please sign in to comment.