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
  • Loading branch information
nyamsprod authored Dec 24, 2024
1 parent 10712f1 commit 5d5875c
Show file tree
Hide file tree
Showing 14 changed files with 1,113 additions and 81 deletions.
11 changes: 2 additions & 9 deletions components/Components/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace League\Uri\Components;

use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\UriAccess;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
Expand All @@ -26,7 +27,7 @@
use function preg_match;
use function sprintf;

abstract class Component implements UriComponentInterface
abstract class Component implements UriComponentInterface, Conditionable
{
protected const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/';

Expand Down Expand Up @@ -84,14 +85,6 @@ final protected static function filterComponent(Stringable|int|string|null $comp
};
}

/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable($this): bool)|bool $condition
* @param callable($this): (static|null) $onSuccess
* @param ?callable($this): (static|null) $onFail
*
*/
final public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
{
if (!is_bool($condition)) {
Expand Down
17 changes: 5 additions & 12 deletions components/Components/URLSearchParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ public function delete(?string $name): void
/**
* Sorts all key/value pairs contained in this object in place and returns undefined.
*
* The sort order is according to unicode code points of the keys. This method
* The sort order is according to Unicode code points of the keys. This method
* uses a stable sorting algorithm (i.e. the relative order between
* key/value pairs with equal keys will be preserved).
*/
Expand All @@ -512,24 +512,17 @@ public function sort(): void
$this->updateQuery($this->pairs->sort());
}

/**
* 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);
}

return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
$condition => $onSuccess($this) ?? $this,
null !== $onFail => $onFail($this) ?? $this,
default => $this,
} ?? $this;
};
}

/**
Expand Down
12 changes: 3 additions & 9 deletions components/Modifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;
use League\Uri\Contracts\UriInterface;
Expand Down Expand Up @@ -54,7 +55,7 @@
* @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
class Modifier implements Stringable, JsonSerializable, UriAccess, Conditionable
{
final public function __construct(protected readonly Psr7UriInterface|UriInterface $uri)
{
Expand Down Expand Up @@ -139,14 +140,7 @@ final public function __call(string $name, array $arguments): 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);
Expand Down
174 changes: 168 additions & 6 deletions docs/uri/7.0/rfc3986.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ echo $uri = //returns 'file:///etc/fstab'

<p class="message-notice"><code>fromRfc8089</code> is added since version <code>7.4.0</code></p>

Accessing URI properties
-------
## Accessing URI properties

Let's examine the result of building a URI:

Expand All @@ -122,6 +121,7 @@ echo $uri->getAuthority(); //displays "foo:[email protected]:81"
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);
Expand All @@ -147,8 +147,113 @@ $uri->getComponents();
The returned value for each URI component is kept encoded. If you need the decoded value you should use the
[league/uri-component](/components) to extract and manipulate each individual component.

Modifying URI properties
-------
<p class="message-notice"><code>getOrigin</code> is added in version <code>7.6.0</code></p>

The `getOrigin` method returns the URI origin used for comparison when calling the `isCrossOrigin` and `isSameOrigin` methods.
The algorithm used is defined by the [WHATWG URL Living standard](https://url.spec.whatwg.org/#origin)

~~~php
echo Uri::new('https://uri.thephpleague.com/uri/6.0/info/')->getOrigin(); //display 'https://uri.thephpleague.com';
echo Uri::new('blob:https://mozilla.org:443')->getOrigin(); //display 'https://mozilla.org'
Uri::new('file:///usr/bin/php')->getOrigin(); //returns null
Uri::new('data:text/plain,Bonjour%20le%20monde%21')->getOrigin(); //returns null
~~~

<p class="message-info">For absolute URI with the <code>file</code> scheme the method will return <code>null</code> (as this is left to the implementation decision)</p>
Because the origin property does not exist in the RFC3986 specification this additional steps is implemented:

- For non-absolute URI the method will return `null`

~~~php
Uri::new('/path/to/endpoint')->getOrigin(); //returns null
~~~

## URI information

The class also exposes a list of public methods which returns the URI state.

### Uri::isAbsolute

Tells whether the URI represents an absolute URI.

~~~php
Uri::fromServer($_SERVER)->isAbsoulte(); //returns true
Uri::new("/🍣🍺")->isAbsolute(); //returns false
~~~

### Uri::isAbsolutePath

Tells whether the URI represents an absolute URI path.

~~~php
Uri::fromServer($_SERVER)->isAbsolutePath(); //returns false
Uri::new("/🍣🍺")->isAbsolutePath(); //returns true
~~~

### Uri::isNetworkPath

Tells whether the URI represents a network path URI.

~~~php
Uri::new("//example.com/toto")->isNetworkPath(); //returns true
Uri::new("/🍣🍺")->isNetworkPath(); //returns false
~~~

### Uri::isOpaque

Tells whether the given URI object represents an opaque URI. An URI is said to be
opaque if and only if it is absolute but does not have an authority

~~~php
Uri::new("email:[email protected]?subject=🏳️‍🌈")->isOpaque(); //returns true
Uri::new("/🍣🍺")->isOpaque(); //returns false
~~~

### Uri::isRelativePath

Tells whether the given URI object represents a relative path.

~~~php
Uri::new("🏳️‍🌈")->isRelativePath(); //returns true
Uri::new("/🍣🍺")->isRelativePath(); //returns false
~~~

### Uri::isSameDocument

Tells whether the given URI object represents the same document.

~~~php
Uri::new("example.com?foo=bar#🏳️‍🌈")->isSameDocument("exAMpLE.com?foo=bar#🍣🍺"); //returns true
~~~

### Uri::hasIDN

Tells whether the given URI object contains a IDN host.

~~~php
Uri::new("https://bébé.be")->hasIdn(); //returns true
~~~

### Uri::isCrossOrigin and Uri::isSameOrigin

Tells whether the given URI object represents different origins.
According to [RFC9110](https://www.rfc-editor.org/rfc/rfc9110#section-4.3.1) The "origin"
for a given URI is the triple of scheme, host, and port after normalizing
the scheme and host to lowercase and normalizing the port to remove
any leading zeros.

~~~php
<?php
Uri::new('blob:http://xn--bb-bjab.be./path')
->isCrossOrigin('http://Bébé.BE./path'); // returns false

Uri::new('https://example.com/123')
->isSameOrigin('https://www.example.com/'); // returns false
~~~

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

## Modifying URI properties

Use the modifying methods exposed by all URI instances to replace one of the URI component.
If the modifications do not alter the current object, it is returned as is, otherwise,
Expand Down Expand Up @@ -194,9 +299,28 @@ echo Uri::new('https://uri.thephpleague.com/components/7.0/modifiers/')
// returns 'https://uri.thephpleague.com/default';
```

## URI resolution

<p class="message-notice">Available since version <code>7.6.0</code></p>

The `Uri::resolve` resolves a URI as a browser would for a relative URI while the `Uri::relativize`
does the opposite.

~~~php
$baseUri = Uri::new('http://www.ExaMPle.com');
$uri = 'http://www.example.com/?foo=toto#~typo';

$relativeUri = $baseUri->relativize($uri);
echo $relativeUri; // display "/?foo=toto#~typo
echo $baseUri->resolve($relativeUri);
echo $baseUri; // display 'http://www.example.com'
// display 'http://www.example.com/?foo=toto#~typo'
echo $baseUri->getUri()::class; //display \League\Uri\Uri
~~~

## URI normalization and comparison

URI normalization
-------
### Non destructive normalization

Out of the box the package normalizes any given URI according to the non-destructive rules
of [RFC3986](https://tools.ietf.org/html/rfc3986).
Expand All @@ -217,3 +341,41 @@ echo $uri; //displays http://xn--bb-bjab.be?#
~~~

<p class="message-info">The last example depends on the presence of the <code>idn_to_*</code> functions, otherwise the code will trigger a <code>MissingFeature</code> exception</p>

### Destructive normalization

<p class="message-notice">Available since version <code>7.6.0</code></p>

The `normalize` method applies extra normalization that may modifier the URI definitions, those extra rules are:

- removing dot segments from the path
- sorting the query pairs
- normalizing the IPv6 and IPv4 host
- url decode all non reserved characters in the path and the query

```php
echo Uri::new('eXAMPLE://a/./b/../b/%63/%7bfoo%7d')->normalize()->toString();
echo Uri::new('eXAMPLE://a/./b/../b/%63/%7bfoo%7d')->toNormalizedString();
// both calls display example://a/b/c/%7Bfoo%7D
```

If you are only interested in the normalized string version of the URI you can call the `toNormalizedString`
which is the equivalent to calling `toString` after calling `normalize`.

### URI comparison

Once normalized a URI can be compare using the two new comparison methods, `isSameDocument` and `equals` methods.

The two methods uses the normalized string representation of two URI to tell whether they are referencing the
same resource.

```php

$uri = Uri::new('example://a/b/c/%7Bfoo%7D?foo=bar');
$uri->isSameDocument('eXAMPLE://a/./b/../b/%63/%7bfoo%7d'); // returns true
$uri->equals('eXAMPLE://a/./b/../b/%63/%7bfoo%7d'); // returns true
$uri->equals('eXAMPLE://a/./b/../b/%63/%7bfoo%7d', excludeFragment: false); // returns false
```

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.
30 changes: 30 additions & 0 deletions interfaces/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

All Notable changes to `League\Uri\Interfaces` will be documented in this file

## [Next](https://github.com/thephpleague/uri-interfaces/compare/7.5.0...master) - TBD

### Added

- `Contidionable` interface
- `UriInterface::resolve`
- `UriInterface::relativize`
- `UriInterface::isAbsolute`
- `UriInterface::isNetworkPath`
- `UriInterface::isAbsolutePath`
- `UriInterface::isRelativePath`
- `UriInterface::isSameDocument`
- `UriInterface::equals`
- `UriInterface::toNormalizedString`
- `UriInterface::getOrigin`
- `UriInterface::isSameOrigin`
- `UriInterface::isCrossOrigin`

### Fixed

- None

### Deprecated

- None

### Removed

- None

## [7.5.0](https://github.com/thephpleague/uri-interfaces/compare/7.3.1...7.5.0) - 2024-12-08

### Added
Expand Down
26 changes: 26 additions & 0 deletions interfaces/Contracts/Conditionable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace League\Uri\Contracts;

interface Conditionable
{
/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable(static): bool)|bool $condition
* @param callable(static): (static|null) $onSuccess
* @param ?callable(static): (static|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static;
}
16 changes: 15 additions & 1 deletion interfaces/Contracts/UriInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,22 @@
*
* @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 toNormalizedString() returns the normalized string representation of the URI
* @method array toComponents() returns an associative array containing all the URI components.
* @method self when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null) conditionally return a new instance
* @method self normalize() returns a new URI instance with normalized components
* @method self resolve(UriInterface $uri) resolves a URI against a base URI using RFC3986 rules
* @method self relativize(UriInterface $uri) relativize a URI against a base URI using RFC3986 rules
* @method self|null getOrigin() returns the URI origin as described in the WHATWG URL Living standard specification
* @method bool isOpaque() tells whether the given URI object represents an opaque URI.
* @method bool isAbsolute() tells whether the URI represents an absolute URI.
* @method bool isNetworkPath() tells whether the URI represents a network path URI.
* @method bool isAbsolutePath() tells whether the URI represents an absolute URI path.
* @method bool isRelativePath() tells whether the given URI object represents a relative path.
* @method bool isCrossOrigin(UriInterface $uri) tells whether the URI comes from a different origin than the current instance.
* @method bool isSameOrigin(UriInterface $uri) tells whether the URI comes from the same origin as the current instance.
* @method bool isSameDocument(UriInterface $uri) tells whether the given URI object represents the same document.
* @method bool isLocalFile() tells whether the `file` scheme base URI represents a local file.
* @method bool equals(UriInterface $uri, bool $excludeFragment) tells whether the given URI object represents the same document. It can take the fragment in account if it is explicitly specified
*/
interface UriInterface extends JsonSerializable, Stringable
{
Expand Down
Loading

0 comments on commit 5d5875c

Please sign in to comment.