Skip to content

Commit

Permalink
#148 Adding more convenient methods to UriInterface (#149)
Browse files Browse the repository at this point in the history
#148 Adding convenient methods to UriInterface
nyamsprod authored Dec 24, 2024
1 parent 721d5ca commit 03fb333
Showing 6 changed files with 862 additions and 42 deletions.
12 changes: 3 additions & 9 deletions BaseUri.php
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
namespace League\Uri;

use JsonSerializable;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\UriAccess;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\MissingFeature;
@@ -43,7 +44,7 @@
/**
* @phpstan-import-type ComponentMap from UriInterface
*/
class BaseUri implements Stringable, JsonSerializable, UriAccess
class BaseUri implements Stringable, JsonSerializable, UriAccess, Conditionable
{
/** @var array<string,int> */
final protected const WHATWG_SPECIAL_SCHEMES = ['ftp' => 1, 'http' => 1, 'https' => 1, 'ws' => 1, 'wss' => 1];
@@ -362,14 +363,7 @@ public function relativize(Stringable|string $uri): static
);
}

/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable($this): bool)|bool $condition
* @param callable($this): (self|null) $onSuccess
* @param ?callable($this): (self|null) $onFail
*/
final public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
final public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
{
if (!is_bool($condition)) {
$condition = $condition($this);
24 changes: 18 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -6,15 +6,27 @@ All Notable changes to `League\Uri` will be documented in this file

### Added

- `BaseUri::when` conditional method to ease component building logic.
- `Http::when` conditional method to ease component building logic.
- `Http::tryNew` returns a new `Uri` instance on success or null on failure.
- `Uri::when` conditional method to ease component building logic.
- `BaseUri::when` conditional method to ease component building logic.
- `Uri::tryNew` returns a new `Uri` instance on success or null on failure.
- `Http::tryNew` returns a new `Uri` instance on success or null on failure.

### Fixed

- None
- `Uri::resolve`
- `Uri::relativize`
- `Uri::isAbsolute`
- `Uri::isNetworkPath`
- `Uri::isAbsolutePath`
- `Uri::isRelativePath`
- `Uri::isSameDocument`
- `Uri::equals`
- `Uri::toNormalizedString`
- `Uri::getOrigin`
- `Uri::isSameOrigin`
- `Uri::isCrossOrigin`

### Fixed

- `Uri` and `Http` normalization normalized IP against RFC3986 rules and not WHATWG rules.

### Deprecated

12 changes: 3 additions & 9 deletions Http.php
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@

use Deprecated;
use JsonSerializable;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
@@ -27,7 +28,7 @@
/**
* @phpstan-import-type InputComponentMap from UriString
*/
final class Http implements Stringable, Psr7UriInterface, JsonSerializable
final class Http implements Stringable, Psr7UriInterface, JsonSerializable, Conditionable
{
private readonly UriInterface $uri;

@@ -224,14 +225,7 @@ private function newInstance(UriInterface $uri): self
};
}

/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable($this): bool)|bool $condition
* @param callable($this): (self|null) $onSuccess
* @param ?callable($this): (self|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
{
if (!is_bool($condition)) {
$condition = $condition($this);
2 changes: 1 addition & 1 deletion HttpTest.php
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ public function testCreateFromComponents(): void
public function testCreateFromBaseUri(): void
{
self::assertEquals(
Http::new('http://0:0@0.0.0.0/0?0#0'),
Http::new('http://0:0@0/0?0#0'),
Http::fromBaseUri('0?0#0', 'http://0:0@0/')
);
}
525 changes: 508 additions & 17 deletions Uri.php

Large diffs are not rendered by default.

329 changes: 329 additions & 0 deletions UriTest.php
Original file line number Diff line number Diff line change
@@ -18,12 +18,15 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use TypeError;

#[CoversClass(Uri::class)]
#[Group('uri')]
class UriTest extends TestCase
{
private const BASE_URI = 'http://a/b/c/d;p?q';

private Uri $uri;

protected function setUp(): void
@@ -463,4 +466,330 @@ public function testItThrowsWhenTheUriComponentValueIsNull(): void

Uri::new('https://www.example.com/')->withPath(Port::new());
}

#[DataProvider('resolveProvider')]
public function testCreateResolve(string $baseUri, string $uri, string $expected): void
{
self::assertSame($expected, Uri::new($baseUri)->resolve($uri)->toString());
}

public static function resolveProvider(): array
{
return [
'base uri' => [self::BASE_URI, '', self::BASE_URI],
'scheme' => [self::BASE_URI, 'http://d/e/f', 'http://d/e/f'],
'path 1' => [self::BASE_URI, 'g', 'http://a/b/c/g'],
'path 2' => [self::BASE_URI, './g', 'http://a/b/c/g'],
'path 3' => [self::BASE_URI, 'g/', 'http://a/b/c/g/'],
'path 4' => [self::BASE_URI, '/g', 'http://a/g'],
'authority' => [self::BASE_URI, '//g', 'http://g'],
'query' => [self::BASE_URI, '?y', 'http://a/b/c/d;p?y'],
'path + query' => [self::BASE_URI, 'g?y', 'http://a/b/c/g?y'],
'fragment' => [self::BASE_URI, '#s', 'http://a/b/c/d;p?q#s'],
'path + fragment' => [self::BASE_URI, 'g#s', 'http://a/b/c/g#s'],
'path + query + fragment' => [self::BASE_URI, 'g?y#s', 'http://a/b/c/g?y#s'],
'single dot 1' => [self::BASE_URI, '.', 'http://a/b/c/'],
'single dot 2' => [self::BASE_URI, './', 'http://a/b/c/'],
'single dot 3' => [self::BASE_URI, './g/.', 'http://a/b/c/g/'],
'single dot 4' => [self::BASE_URI, 'g/./h', 'http://a/b/c/g/h'],
'double dot 1' => [self::BASE_URI, '..', 'http://a/b/'],
'double dot 2' => [self::BASE_URI, '../', 'http://a/b/'],
'double dot 3' => [self::BASE_URI, '../g', 'http://a/b/g'],
'double dot 4' => [self::BASE_URI, '../..', 'http://a/'],
'double dot 5' => [self::BASE_URI, '../../', 'http://a/'],
'double dot 6' => [self::BASE_URI, '../../g', 'http://a/g'],
'double dot 7' => [self::BASE_URI, '../../../g', 'http://a/g'],
'double dot 8' => [self::BASE_URI, '../../../../g', 'http://a/g'],
'double dot 9' => [self::BASE_URI, 'g/../h' , 'http://a/b/c/h'],
'mulitple slashes' => [self::BASE_URI, 'foo////g', 'http://a/b/c/foo////g'],
'complex path 1' => [self::BASE_URI, ';x', 'http://a/b/c/;x'],
'complex path 2' => [self::BASE_URI, 'g;x', 'http://a/b/c/g;x'],
'complex path 3' => [self::BASE_URI, 'g;x?y#s', 'http://a/b/c/g;x?y#s'],
'complex path 4' => [self::BASE_URI, 'g;x=1/./y', 'http://a/b/c/g;x=1/y'],
'complex path 5' => [self::BASE_URI, 'g;x=1/../y', 'http://a/b/c/y'],
'dot segments presence 1' => [self::BASE_URI, '/./g', 'http://a/g'],
'dot segments presence 2' => [self::BASE_URI, '/../g', 'http://a/g'],
'dot segments presence 3' => [self::BASE_URI, 'g.', 'http://a/b/c/g.'],
'dot segments presence 4' => [self::BASE_URI, '.g', 'http://a/b/c/.g'],
'dot segments presence 5' => [self::BASE_URI, 'g..', 'http://a/b/c/g..'],
'dot segments presence 6' => [self::BASE_URI, '..g', 'http://a/b/c/..g'],
'origin uri without path' => ['http://h:b@a', 'b/../y', 'http://h:b@a/y'],
'not same origin' => [self::BASE_URI, 'ftp://a/b/c/d', 'ftp://a/b/c/d'],
];
}



public function testRelativizeIsNotMade(): void
{
$uri = '//path#fragment';

self::assertEquals($uri, Uri::new('https://example.com/path')->relativize($uri)->toString());
}

#[DataProvider('relativizeProvider')]
public function testRelativize(string $uri, string $resolved, string $expected): void
{
self::assertSame(
$expected,
Uri::new(Http::new($uri))->relativize($resolved)->toString()
);
}

public static function relativizeProvider(): array
{
return [
'different scheme' => [self::BASE_URI, 'https://a/b/c/d;p?q', 'https://a/b/c/d;p?q'],
'different authority' => [self::BASE_URI, 'https://g/b/c/d;p?q', 'https://g/b/c/d;p?q'],
'empty uri' => [self::BASE_URI, '', ''],
'same uri' => [self::BASE_URI, self::BASE_URI, ''],
'same path' => [self::BASE_URI, 'http://a/b/c/d;p', 'd;p'],
'parent path 1' => [self::BASE_URI, 'http://a/b/c/', './'],
'parent path 2' => [self::BASE_URI, 'http://a/b/', '../'],
'parent path 3' => [self::BASE_URI, 'http://a/', '../../'],
'parent path 4' => [self::BASE_URI, 'http://a', '../../'],
'sibling path 1' => [self::BASE_URI, 'http://a/b/c/g', 'g'],
'sibling path 2' => [self::BASE_URI, 'http://a/b/c/g/h', 'g/h'],
'sibling path 3' => [self::BASE_URI, 'http://a/b/g', '../g'],
'sibling path 4' => [self::BASE_URI, 'http://a/g', '../../g'],
'query' => [self::BASE_URI, 'http://a/b/c/d;p?y', '?y'],
'fragment' => [self::BASE_URI, 'http://a/b/c/d;p?q#s', '#s'],
'path + query' => [self::BASE_URI, 'http://a/b/c/g?y', 'g?y'],
'path + fragment' => [self::BASE_URI, 'http://a/b/c/g#s', 'g#s'],
'path + query + fragment' => [self::BASE_URI, 'http://a/b/c/g?y#s', 'g?y#s'],
'empty segments' => [self::BASE_URI, 'http://a/b/c/foo////g', 'foo////g'],
'empty segments 1' => [self::BASE_URI, 'http://a/b////c/foo/g', '..////c/foo/g'],
'relative single dot 1' => [self::BASE_URI, '.', '.'],
'relative single dot 2' => [self::BASE_URI, './', './'],
'relative double dot 1' => [self::BASE_URI, '..', '..'],
'relative double dot 2' => [self::BASE_URI, '../', '../'],
'path with colon 1' => ['http://a/', 'http://a/d:p', './d:p'],
'path with colon 2' => [self::BASE_URI, 'http://a/b/c/g/d:p', 'g/d:p'],
'scheme + auth 1' => ['http://a', 'http://a?q#s', '?q#s'],
'scheme + auth 2' => ['http://a/', 'http://a?q#s', '/?q#s'],
'2 relative paths 1' => ['a/b', '../..', '../..'],
'2 relative paths 2' => ['a/b', './.', './.'],
'2 relative paths 3' => ['a/b', '../c', '../c'],
'2 relative paths 4' => ['a/b', 'c/..', 'c/..'],
'2 relative paths 5' => ['a/b', 'c/.', 'c/.'],
'baseUri with query' => ['/a/b/?q', '/a/b/#h', './#h'],
'targetUri with fragment' => ['/', '/#h', '#h'],
'same document' => ['/', '/', ''],
'same URI normalized' => ['http://a', 'http://a/', ''],
];
}

/**
* @param array<bool> $infos
*/
#[DataProvider('uriProvider')]
public function testInfo(
Psr7UriInterface|Uri $uri,
Psr7UriInterface|Uri|null $base_uri,
array $infos
): void {
if (null !== $base_uri) {
self::assertSame($infos['same_document'], Uri::new($base_uri)->isSameDocument($uri));
}
self::assertSame($infos['relative_path'], Uri::new($uri)->isRelativePath());
self::assertSame($infos['absolute_path'], Uri::new($uri)->isAbsolutePath());
self::assertSame($infos['absolute_uri'], Uri::new($uri)->isAbsolute());
self::assertSame($infos['network_path'], Uri::new($uri)->isNetworkPath());
}

public static function uriProvider(): array
{
return [
'absolute uri' => [
'uri' => Http::new('http://a/p?q#f'),
'base_uri' => null,
'infos' => [
'absolute_uri' => true,
'network_path' => false,
'absolute_path' => false,
'relative_path' => false,
'same_document' => false,
],
],
'network relative uri' => [
'uri' => Http::new('//스타벅스코리아.com/p?q#f'),
'base_uri' => Http::new('//xn--oy2b35ckwhba574atvuzkc.com/p?q#z'),
'infos' => [
'absolute_uri' => false,
'network_path' => true,
'absolute_path' => false,
'relative_path' => false,
'same_document' => true,
],
],
'path relative uri with non empty path' => [
'uri' => Http::new('p?q#f'),
'base_uri' => null,
'infos' => [
'absolute_uri' => false,
'network_path' => false,
'absolute_path' => false,
'relative_path' => true,
'same_document' => false,
],
],
'path relative uri with empty' => [
'uri' => Http::new('?q#f'),
'base_uri' => null,
'infos' => [
'absolute_uri' => false,
'network_path' => false,
'absolute_path' => false,
'relative_path' => true,
'same_document' => false,
],
],
];
}

public function testIsFunctionsThrowsTypeError(): void
{
self::assertTrue(Uri::new('http://example.com')->isAbsolute());
self::assertFalse(Uri::new('http://example.com')->isNetworkPath());
self::assertTrue(Uri::new('/example.com')->isAbsolutePath());
self::assertTrue(Uri::new('example.com#foobar')->isRelativePath());
}

#[DataProvider('sameValueAsProvider')]
public function testSameValueAs(Psr7UriInterface|Uri $uri1, Psr7UriInterface|Uri $uri2, bool $expected): void
{
self::assertSame($expected, Uri::new($uri2)->isSameDocument($uri1));
}

public static function sameValueAsProvider(): array
{
return [
'2 disctincts URIs' => [
Http::new('http://example.com'),
Uri::new('ftp://example.com'),
false,
],
'2 identical URIs' => [
Http::new('http://example.com'),
Http::new('http://example.com'),
true,
],
'2 identical URIs after removing dot segment' => [
Http::new('http://example.org/~foo/'),
Http::new('http://example.ORG/bar/./../~foo/'),
true,
],
'2 distincts relative URIs' => [
Http::new('~foo/'),
Http::new('../~foo/'),
false,
],
'2 identical relative URIs' => [
Http::new('../%7efoo/'),
Http::new('../~foo/'),
true,
],
'2 identical URIs after normalization (1)' => [
Http::new('HtTp://مثال.إختبار:80/%7efoo/%7efoo/'),
Http::new('http://xn--mgbh0fb.xn--kgbechtv/%7Efoo/~foo/'),
true,
],
'2 identical URIs after normalization (2)' => [
Http::new('http://www.example.com'),
Http::new('http://www.example.com/'),
true,
],
'2 identical URIs after normalization (3)' => [
Http::new('http://www.example.com'),
Http::new('http://www.example.com:/'),
true,
],
'2 identical URIs after normalization (4)' => [
Http::new('http://www.example.com'),
Http::new('http://www.example.com:80/'),
true,
],
];
}

#[DataProvider('getOriginProvider')]
public function testGetOrigin(Psr7UriInterface|Uri|string $uri, ?string $expectedOrigin): void
{
self::assertSame($expectedOrigin, Uri::new($uri)->getOrigin()?->toString());
}

public static function getOriginProvider(): array
{
return [
'http uri' => [
'uri' => Uri::new('https://example.com/path?query#fragment'),
'expectedOrigin' => 'https://example.com',
],
'http uri with non standard port' => [
'uri' => Uri::new('https://example.com:81/path?query#fragment'),
'expectedOrigin' => 'https://example.com:81',
],
'relative uri' => [
'uri' => Uri::new('//example.com:81/path?query#fragment'),
'expectedOrigin' => null,
],
'absolute uri with user info' => [
'uri' => Uri::new('https://user:pass@example.com:81/path?query#fragment'),
'expectedOrigin' => 'https://example.com:81',
],
'opaque URI' => [
'uri' => Uri::new('mailto:info@thephpleague.com'),
'expectedOrigin' => null,
],
'file URI' => [
'uri' => Uri::new('file:///usr/bin/test'),
'expectedOrigin' => null,
],
'blob' => [
'uri' => Uri::new('blob:https://mozilla.org:443/'),
'expectedOrigin' => 'https://mozilla.org',
],
'normalized ipv4' => [
'uri' => 'https://0:443/',
'expectedOrigin' => 'https://0.0.0.0',
],
'normalized ipv4 with object' => [
'uri' => Uri::new('https://0:443/'),
'expectedOrigin' => 'https://0.0.0.0',
],
'compressed ipv6' => [
'uri' => 'https://[1050:0000:0000:0000:0005:0000:300c:326b]:443/',
'expectedOrigin' => 'https://[1050::5:0:300c:326b]',
],
];
}

#[DataProvider('getCrossOriginExamples')]
public function testIsCrossOrigin(string $original, string $modified, bool $expected): void
{
self::assertSame($expected, !Uri::new($original)->isSameOrigin($modified));
}

/**
* @return array<string, array{0:string, 1:string, 2:bool}>
*/
public static function getCrossOriginExamples(): array
{
return [
'different path' => ['http://example.com/123', 'http://example.com/', false],
'same port with default value (1)' => ['https://example.com/123', 'https://example.com:443/', false],
'same port with default value (2)' => ['ws://example.com:80/123', 'ws://example.com/', false],
'same explicit port' => ['wss://example.com:443/123', 'wss://example.com:443/', false],
'same origin with i18n host' => ['https://xn--bb-bjab.be./path', 'https://Bébé.BE./path', false],
'same origin using a blob' => ['blob:https://mozilla.org:443/', 'https://mozilla.org/123', false],
'different scheme' => ['https://example.com/123', 'ftp://example.com/', true],
'different host' => ['ftp://example.com/123', 'ftp://www.example.com/123', true],
'different port implicit' => ['https://example.com/123', 'https://example.com:81/', true],
'different port explicit' => ['https://example.com:80/123', 'https://example.com:81/', true],
'same scheme different port' => ['https://example.com:443/123', 'https://example.com:444/', true],
'comparing two opaque URI' => ['ldap://ldap.example.net', 'ldap://ldap.example.net', true],
'comparing a URI with an origin and one with an opaque origin' => ['https://example.com:443/123', 'ldap://ldap.example.net', true],
'cross origin using a blob' => ['blob:http://mozilla.org:443/', 'https://mozilla.org/123', true],
];
}
}

0 comments on commit 03fb333

Please sign in to comment.