From e6f43d0718f254d22fb7f75f9809417c750c4ef0 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 29 Jan 2025 07:16:37 +0100 Subject: [PATCH] Improve Modifier usage and implementation --- components/Modifier.php | 83 +++++++++++++++++++------------- components/ModifierTest.php | 36 +++++++++++++- docs/components/7.0/modifiers.md | 22 ++++----- docs/uri/7.0/psr-compliance.md | 32 ++++++++++-- phpstan.neon | 4 -- 5 files changed, 124 insertions(+), 53 deletions(-) diff --git a/components/Modifier.php b/components/Modifier.php index a2fb8669..092aa6ab 100644 --- a/components/Modifier.php +++ b/components/Modifier.php @@ -13,7 +13,6 @@ namespace League\Uri; -use BadMethodCallException; use Deprecated; use JsonSerializable; use League\Uri\Components\DataPath; @@ -37,28 +36,17 @@ use Psr\Http\Message\UriInterface as Psr7UriInterface; use Stringable; -use function array_map; use function filter_var; use function get_object_vars; use function is_bool; use function ltrim; use function rtrim; -use function sprintf; use function str_ends_with; use function str_starts_with; use const FILTER_FLAG_IPV4; use const FILTER_VALIDATE_IP; -/** - * @method static withScheme(Stringable|string|null $scheme) returns a new instance with the specified scheme. - * @method static withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null) returns a new instance with the specified user info. - * @method static withHost(Stringable|string|null $host) returns a new instance with the specified host. - * @method static withPort(?int $port) returns a new instance with the specified port. - * @method static withPath(Stringable|string $path) returns a new instance with the specified path. - * @method static withQuery(Stringable|string|null $query) returns a new instance with the specified query. - * @method static withFragment(Stringable|string|null $fragment) returns a new instance with the specified fragment. - */ class Modifier implements Stringable, JsonSerializable, UriAccess, Conditionable { final public function __construct(protected readonly Psr7UriInterface|UriInterface $uri) @@ -85,37 +73,65 @@ public function getUri(): Psr7UriInterface|UriInterface public function getUriString(): string { - return $this->uri->__toString(); + return $this->toString(); } public function jsonSerialize(): string { - return $this->uri->__toString(); + return $this->toString(); } public function __toString(): string + { + return $this->toString(); + } + + public function toString(): string { return $this->uri->__toString(); } - final public function __call(string $name, array $arguments): static + public function toDisplayString(): string { - static $allowedMethods = [ - 'withScheme', - 'withUserInfo', - 'withHost', - 'withPort', - 'withPath', - 'withQuery', - 'withFragment', - ]; + return ($this->uri instanceof UriRenderer) ? $this->uri->toDisplayString() : Uri::new($this->uri)->toDisplayString(); + } - return match (true) { - !in_array($name, $allowedMethods, true) => throw new BadMethodCallException(sprintf('Call to undefined method %s::%s()', self::class, $name)), - $this->uri instanceof UriInterface, - 'withPort' === $name => new static($this->uri->$name(...$arguments)), - default => new static($this->uri->$name(...array_map(fn (Stringable|string|null $value): string => (string) $value, $arguments))), - }; + public function withScheme(Stringable|string|null $scheme): static + { + return new static($this->uri->withScheme(self::normalizeComponent($scheme, $this->uri))); + } + + public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password): static + { + return new static($this->uri->withUserInfo( + self::normalizeComponent($user, $this->uri), + $password instanceof Stringable ? $password->__toString() : $password + )); + } + + public function withQuery(Stringable|string|null $query): static + { + return new static($this->uri->withQuery(self::normalizeComponent($query, $this->uri))); + } + + public function withHost(Stringable|string|null $host): static + { + return new static($this->uri->withHost(self::normalizeComponent($host, $this->uri))); + } + + public function withFragment(Stringable|string|null $fragment): static + { + return new static($this->uri->withFragment(self::normalizeComponent($fragment, $this->uri))); + } + + public function withPort(?int $port): static + { + return new static($this->uri->withPort($port)); + } + + public function withPath(Stringable|string $path): static + { + return new static($this->uri->withPath((string) $path)); } final public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static @@ -603,7 +619,7 @@ public function replaceLabel(int $offset, Stringable|string|null $label): static )); } - public function whatWgHost(): static + public function whatwgHost(): static { $host = $this->uri->getHost(); try { @@ -837,10 +853,11 @@ final protected static function normalizePath(Psr7UriInterface|UriInterface $uri * * null value MUST be converted to the empty string if a Psr7 UriInterface is being manipulated. */ - final protected static function normalizeComponent(?string $component, Psr7UriInterface|UriInterface $uri): ?string + final protected static function normalizeComponent(Stringable|string|null $component, Psr7UriInterface|UriInterface $uri): ?string { return match (true) { - $uri instanceof Psr7UriInterface => (string) $component, + $uri instanceof Psr7UriInterface, + $component instanceof Stringable => (string) $component, default => $component, }; } diff --git a/components/ModifierTest.php b/components/ModifierTest.php index fb8e8f03..a776a838 100644 --- a/components/ModifierTest.php +++ b/components/ModifierTest.php @@ -738,7 +738,7 @@ public function testRemoveBasePathWithRelativePath(): void } #[\PHPUnit\Framework\Attributes\DataProvider('validwithoutSegmentProvider')] - public function testwithoutSegment(array $keys, string $expected): void + public function testWithoutSegment(array $keys, string $expected): void { self::assertSame($expected, $this->modifier->removeSegments(...$keys)->getUri()->getPath()); } @@ -890,9 +890,41 @@ public function it_will_convert_uri_host_following_whatwg_rules(): void self::assertSame( '192.168.2.13', Modifier::from(Http::new('https://0:0@0xc0a8020d/0?0#0')) - ->whatWgHost() + ->whatwgHost() ->getUri() ->getHost() ); } + + #[Test] + #[DataProvider('providesUriToDisplay')] + public function it_will_allow_direct_string_conversion( + string $uri, + string $expectedString, + string $expectedDisplayString + ): void { + self::assertSame($expectedString, Modifier::from($uri)->toString()); + self::assertSame($expectedDisplayString, Modifier::from($uri)->toDisplayString()); + } + + public static function providesUriToDisplay(): iterable + { + yield 'uri is unchanged' => [ + 'uri' => 'https://127.0.0.1/foo/bar', + 'expectedString' => 'https://127.0.0.1/foo/bar', + 'expectedDisplayString' => 'https://127.0.0.1/foo/bar', + ]; + + yield 'idn host are changed' => [ + 'uri' => 'http://bébé.be', + 'expectedString' => 'http://xn--bb-bjab.be', + 'expectedDisplayString' => 'http://bébé.be', + ]; + + yield 'other components are changed' => [ + 'uri' => 'http://bébé.be:80?q=toto%20le%20h%C3%A9ros', + 'expectedString' => 'http://xn--bb-bjab.be?q=toto%20le%20h%C3%A9ros', + 'expectedDisplayString' => 'http://bébé.be?q=toto le héros', + ]; + } } diff --git a/docs/components/7.0/modifiers.md b/docs/components/7.0/modifiers.md index a0daa673..184508f5 100644 --- a/docs/components/7.0/modifiers.md +++ b/docs/components/7.0/modifiers.md @@ -87,10 +87,10 @@ implements the `Stringable` and the `JsonSerializable` interface to improve deve Under the hood the `Modifier` class intensively uses the [URI components objects](/components/7.0/) to apply changes to the submitted URI object. -

The when method is available since version 7.6.0

+

The when, toString and toDisplayString methods are available since version 7.6.0

-To ease modifying URI since version 7.6.0, the `when` method is added and the modifier methods from -the underlying URI object are now accessible via proxy. +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 ```php use League\Uri\Modifier; @@ -99,19 +99,19 @@ $foo = ''; echo Modifier::from('http://bébé.be') ->when( '' !== $foo, - fn (Modifier $uri) => $uri->withQuery('firstname=jane&lastname=Doe'), //on true - fn (Modifier $uri) => $uri->mergeQueryParameters(['firstname' => 'john', 'lastname' => 'Doe']), //on false + 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') - ->getUriIdnString(); -// returns 'http://shop.bébé.be./toto?firstname=john&lastname=Doe&foo=toto&foo=tata#chapter1'; + ->toDisplayString(); +// returns 'http://shop.bébé.be./toto?fname=john&lname=Doe&foo=toto&foo=tata#chapter1'; ``` -### Available methods +### Available modifiers
@@ -145,7 +145,7 @@ echo Modifier::from('http://bébé.be')
  • replaceLabel
  • removeLabels
  • sliceLabels
  • -
  • whatWgHost
  • +
  • whatwgHost
  • @@ -605,7 +605,7 @@ echo Modifier::from($uri)->sliceLabels(1, 1)->getUriString();

    This modifier supports negative offset

    -### Modifier::whatWgHost +### Modifier::whatwgHost Returns the host as formatted following WHATWG host formatting @@ -613,7 +613,7 @@ Returns the host as formatted following WHATWG host formatting ~~~php $uri = "https://0:0@0:0"; -echo Modifier::from($uri)->whatWgHost()->getUriString(); +echo Modifier::from($uri)->whatwgHost()->getUriString(); //display "https://0:0@0.0.0.0:0" ~~~ diff --git a/docs/uri/7.0/psr-compliance.md b/docs/uri/7.0/psr-compliance.md index f600cd5c..e1be437f 100644 --- a/docs/uri/7.0/psr-compliance.md +++ b/docs/uri/7.0/psr-compliance.md @@ -7,9 +7,8 @@ PSR interoperability ======= As we are dealing with URI, the package provides classes compliant -with [PSR-7](https://www.php-fig.org/psr/psr-7/) and -[PSR-17](https://www.php-fig.org/psr/psr-17/). This is done to allow -more interoperability amongs PHP packages. +with [PSR-7](https://www.php-fig.org/psr/psr-7/) and [PSR-17](https://www.php-fig.org/psr/psr-17/) and [PSR-13](https://www.php-fig.org/psr/psr-13/). This +is done to allow more interoperability amongs PHP packages. ## PSR-7 compatibility @@ -132,3 +131,30 @@ $uriFactory = new HttpFactory(); $uri = $uriFactory->createUri('http://example.com/path/to?q=foo%20bar#section-42'); echo $uri::class; // display League\Uri\Http ~~~ + +## PSR-13 compatibility + +

    Available since version 7.6

    + +To allow easier integration with other PHP packages and especially [PSR-13](https://www.php-fig.org/psr/psr-13/) +the `UriTemplate` class implements the `Stringable` interface. + +~~~php +use League\Uri\UriTemplate; +use Symfony\Component\WebLink\Link; + +$uriTemplate = new UriTemplate('https://google.com/search{?q*}'); + +$link = (new Link()) + ->withHref($uriTemplate) + ->withRel('next') + ->withAttribute('me', 'you'); + +// Once serialized will return +// '; rel="next"; me="you"' +~~~ + +The `Symfony\Component\WebLink\Link` package implements `PSR-13` interfaces. + +

    You could already use a Uri instance if the link must use a +concrete URI instead as the class also implements the Stringable interface.

    diff --git a/phpstan.neon b/phpstan.neon index 837423f5..2b714996 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,10 +16,6 @@ parameters: - message: '#function gmp_(.*)? expects (GMP|int)#' path: interfaces/IPv4/GMPCalculator.php - identifier: missingType.iterableValue - - message: '#Variable method call on League\\Uri\\Contracts\\UriInterface.#' - path: components/Modifier.php - - message: '#Variable method call on Psr\\Http\\Message\\UriInterface.#' - path: components/Modifier.php - '#Attribute class Deprecated does not exist.#' - '#deprecated class League\\Uri\\BaseUri#' - '#\Dom\\HTMLDocument#'