| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602 |
- <?php
- /**
- * League.Uri (https://uri.thephpleague.com)
- *
- * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
- *
- * 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;
- use BackedEnum;
- use Closure;
- use JsonSerializable;
- use League\Uri\Contracts\Conditionable;
- use League\Uri\Contracts\Transformable;
- use League\Uri\Contracts\UriComponentInterface;
- use League\Uri\Contracts\UriInterface;
- use League\Uri\Exceptions\SyntaxError;
- use League\Uri\UriTemplate\Template;
- use Stringable;
- use Uri\Rfc3986\Uri as Rfc3986Uri;
- use Uri\WhatWg\Url as WhatWgUrl;
- use function is_bool;
- use function preg_match;
- use function str_replace;
- use function strtolower;
- /**
- * @phpstan-type UrnSerialize array{0: array{urn: non-empty-string}, 1: array{}}
- * @phpstan-import-type InputComponentMap from UriString
- * @phpstan-type UrnMap array{
- * scheme: 'urn',
- * nid: string,
- * nss: string,
- * r_component: ?string,
- * q_component: ?string,
- * f_component: ?string,
- * }
- */
- final class Urn implements Conditionable, Stringable, JsonSerializable, Transformable
- {
- /**
- * RFC8141 regular expression URN splitter.
- *
- * The regexp does not perform any look-ahead.
- * Not all invalid URN are caught. Some
- * post-regexp-validation checks
- * are mandatory.
- *
- * @link https://datatracker.ietf.org/doc/html/rfc8141#section-2
- *
- * @var string
- */
- private const REGEXP_URN_PARTS = '/^
- urn:
- (?<nid>[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?): # NID
- (?<nss>.*?) # NSS
- (?<frc>\?\+(?<rcomponent>.*?))? # r-component
- (?<fqc>\?\=(?<qcomponent>.*?))? # q-component
- (?:\#(?<fcomponent>.*))? # f-component
- $/xi';
- /**
- * RFC8141 namespace identifier regular expression.
- *
- * @link https://datatracker.ietf.org/doc/html/rfc8141#section-2
- *
- * @var string
- */
- private const REGEX_NID_SEQUENCE = '/^[a-z0-9]([a-z0-9-]{0,30})[a-z0-9]$/xi';
- /** @var non-empty-string */
- private readonly string $uriString;
- /** @var non-empty-string */
- private readonly string $nid;
- /** @var non-empty-string */
- private readonly string $nss;
- /** @var non-empty-string|null */
- private readonly ?string $rComponent;
- /** @var non-empty-string|null */
- private readonly ?string $qComponent;
- /** @var non-empty-string|null */
- private readonly ?string $fComponent;
- /**
- * @param Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $urn the percent-encoded URN
- */
- public static function parse(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $urn): ?Urn
- {
- try {
- return self::fromString($urn);
- } catch (SyntaxError) {
- return null;
- }
- }
- /**
- * @param Rfc3986Uri|WhatWgUrl|Stringable|string $urn the percent-encoded URN
- * @see self::fromString()
- *
- * @throws SyntaxError if the URN is invalid
- */
- public static function new(Rfc3986Uri|WhatWgUrl|Stringable|string $urn): self
- {
- return self::fromString($urn);
- }
- /**
- * @param Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $urn the percent-encoded URN
- *
- * @throws SyntaxError if the URN is invalid
- */
- public static function fromString(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $urn): self
- {
- $urn = match (true) {
- $urn instanceof Rfc3986Uri => $urn->toRawString(),
- $urn instanceof WhatWgUrl => $urn->toAsciiString(),
- $urn instanceof BackedEnum => (string) $urn->value,
- default => (string) $urn,
- };
- UriString::containsRfc3986Chars($urn) || throw new SyntaxError('The URN is malformed, it contains invalid characters.');
- 1 === preg_match(self::REGEXP_URN_PARTS, $urn, $matches) || throw new SyntaxError('The URN string is invalid.');
- return new self(
- nid: $matches['nid'],
- nss: $matches['nss'],
- rComponent: (isset($matches['frc']) && '' !== $matches['frc']) ? $matches['rcomponent'] : null,
- qComponent: (isset($matches['fqc']) && '' !== $matches['fqc']) ? $matches['qcomponent'] : null,
- fComponent: $matches['fcomponent'] ?? null,
- );
- }
- /**
- * Create a new instance from a hash representation of the URI similar
- * to PHP parse_url function result.
- *
- * @param InputComponentMap $components a hash representation of the URI similar to PHP parse_url function result
- */
- public static function fromComponents(array $components = []): self
- {
- $components += [
- 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
- 'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
- ];
- return self::fromString(UriString::build($components));
- }
- /**
- * @param Stringable|string $nss the percent-encoded NSS
- *
- * @throws SyntaxError if the URN is invalid
- */
- public static function fromRfc2141(BackedEnum|Stringable|string $nid, BackedEnum|Stringable|string $nss): self
- {
- if ($nid instanceof BackedEnum) {
- $nid = $nid->value;
- }
- if ($nss instanceof BackedEnum) {
- $nss = $nss->value;
- }
- return new self((string) $nid, (string) $nss);
- }
- /**
- * @param string $nss the percent-encoded NSS
- * @param ?string $rComponent the percent-encoded r-component
- * @param ?string $qComponent the percent-encoded q-component
- * @param ?string $fComponent the percent-encoded f-component
- *
- * @throws SyntaxError if one of the URN part is invalid
- */
- private function __construct(
- string $nid,
- string $nss,
- ?string $rComponent = null,
- ?string $qComponent = null,
- ?string $fComponent = null,
- ) {
- ('' !== $nid && 1 === preg_match(self::REGEX_NID_SEQUENCE, $nid)) || throw new SyntaxError('The URN is malformed, the NID is invalid.');
- ('' !== $nss && Encoder::isPathEncoded($nss)) || throw new SyntaxError('The URN is malformed, the NSS is invalid.');
- /** @param Closure(string): ?non-empty-string $closure */
- $validateComponent = static fn (?string $value, Closure $closure, string $name): ?string => match (true) {
- null === $value,
- ('' !== $value && 1 !== preg_match('/[#?]/', $value) && $closure($value)) => $value,
- default => throw new SyntaxError('The URN is malformed, the `'.$name.'` component is invalid.'),
- };
- $this->nid = $nid;
- $this->nss = $nss;
- $this->rComponent = $validateComponent($rComponent, Encoder::isPathEncoded(...), 'r-component');
- $this->qComponent = $validateComponent($qComponent, Encoder::isQueryEncoded(...), 'q-component');
- $this->fComponent = $validateComponent($fComponent, Encoder::isFragmentEncoded(...), 'f-component');
- $this->uriString = $this->setUriString();
- }
- /**
- * @return non-empty-string
- */
- private function setUriString(): string
- {
- $str = $this->toRfc2141();
- if (null !== $this->rComponent) {
- $str .= '?+'.$this->rComponent;
- }
- if (null !== $this->qComponent) {
- $str .= '?='.$this->qComponent;
- }
- if (null !== $this->fComponent) {
- $str .= '#'.$this->fComponent;
- }
- return $str;
- }
- /**
- * Returns the NID.
- *
- * @return non-empty-string
- */
- public function getNid(): string
- {
- return $this->nid;
- }
- /**
- * Returns the percent-encoded NSS.
- *
- * @return non-empty-string
- */
- public function getNss(): string
- {
- return $this->nss;
- }
- /**
- * Returns the percent-encoded r-component string or null if it is not set.
- *
- * @return ?non-empty-string
- */
- public function getRComponent(): ?string
- {
- return $this->rComponent;
- }
- /**
- * Returns the percent-encoded q-component string or null if it is not set.
- *
- * @return ?non-empty-string
- */
- public function getQComponent(): ?string
- {
- return $this->qComponent;
- }
- /**
- * Returns the percent-encoded f-component string or null if it is not set.
- *
- * @return ?non-empty-string
- */
- public function getFComponent(): ?string
- {
- return $this->fComponent;
- }
- /**
- * Returns the RFC8141 URN string representation.
- *
- * @return non-empty-string
- */
- public function toString(): string
- {
- return $this->uriString;
- }
- /**
- * Returns the RFC2141 URN string representation.
- *
- * @return non-empty-string
- */
- public function toRfc2141(): string
- {
- return 'urn:'.$this->nid.':'.$this->nss;
- }
- /**
- * Returns the human-readable string representation of the URN as an IRI.
- *
- * @see https://datatracker.ietf.org/doc/html/rfc3987
- */
- public function toDisplayString(): string
- {
- return UriString::toIriString($this->uriString);
- }
- /**
- * Returns the RFC8141 URN string representation.
- *
- * @see self::toString()
- *
- * @return non-empty-string
- */
- public function __toString(): string
- {
- return $this->toString();
- }
- /**
- * Returns the RFC8141 URN string representation.
- * @see self::toString()
- *
- * @return non-empty-string
- */
- public function jsonSerialize(): string
- {
- return $this->toString();
- }
- /**
- * Returns the RFC3986 representation of the current URN.
- *
- * If a template URI is used the following variables as present
- * {nid} for the namespace identifier
- * {nss} for the namespace specific string
- * {r_component} for the r-component without its delimiter
- * {q_component} for the q-component without its delimiter
- * {f_component} for the f-component without its delimiter
- */
- public function resolve(UriTemplate|Template|BackedEnum|string|null $template = null): UriInterface
- {
- return null !== $template ? Uri::fromTemplate($template, $this->toComponents()) : Uri::new($this->uriString);
- }
- public function hasRComponent(): bool
- {
- return null !== $this->rComponent;
- }
- public function hasQComponent(): bool
- {
- return null !== $this->qComponent;
- }
- public function hasFComponent(): bool
- {
- return null !== $this->fComponent;
- }
- public function hasOptionalComponent(): bool
- {
- return null !== $this->rComponent
- || null !== $this->qComponent
- || null !== $this->fComponent;
- }
- /**
- * Return an instance with the specified NID.
- *
- * This method MUST retain the state of the current instance, and return
- * an instance that contains the specified NID.
- *
- * @throws SyntaxError for invalid component or transformations
- * that would result in an object in invalid state.
- */
- public function withNid(BackedEnum|Stringable|string $nid): self
- {
- if ($nid instanceof BackedEnum) {
- $nid = $nid->value;
- }
- $nid = (string) $nid;
- return $this->nid === $nid ? $this : new self(
- nid: $nid,
- nss: $this->nss,
- rComponent: $this->rComponent,
- qComponent: $this->qComponent,
- fComponent: $this->fComponent,
- );
- }
- /**
- * Return an instance with the specified NSS.
- *
- * This method MUST retain the state of the current instance, and return
- * an instance that contains the specified NSS.
- *
- * @throws SyntaxError for invalid component or transformations
- * that would result in an object in invalid state.
- */
- public function withNss(BackedEnum|Stringable|string $nss): self
- {
- $nss = Encoder::encodePath($nss);
- return $this->nss === $nss ? $this : new self(
- nid: $this->nid,
- nss: $nss,
- rComponent: $this->rComponent,
- qComponent: $this->qComponent,
- fComponent: $this->fComponent,
- );
- }
- /**
- * Return an instance with the specified r-component.
- *
- * This method MUST retain the state of the current instance, and return
- * an instance that contains the specified r-component.
- *
- * The component is removed if the value is null.
- *
- * @throws SyntaxError for invalid component or transformations
- * that would result in an object in invalid state.
- */
- public function withRComponent(BackedEnum|Stringable|string|null $component): self
- {
- if ($component instanceof BackedEnum) {
- $component = (string) $component->value;
- }
- if ($component instanceof UriComponentInterface) {
- $component = $component->value();
- }
- if (null !== $component) {
- $component = self::formatComponent(Encoder::encodePath($component));
- }
- return $this->rComponent === $component ? $this : new self(
- nid: $this->nid,
- nss: $this->nss,
- rComponent: $component,
- qComponent: $this->qComponent,
- fComponent: $this->fComponent,
- );
- }
- private static function formatComponent(?string $component): ?string
- {
- return null === $component ? null : str_replace(['?', '#'], ['%3F', '%23'], $component);
- }
- /**
- * Return an instance with the specified q-component.
- *
- * This method MUST retain the state of the current instance, and return
- * an instance that contains the specified q-component.
- *
- * The component is removed if the value is null.
- *
- * @throws SyntaxError for invalid component or transformations
- * that would result in an object in invalid state.
- */
- public function withQComponent(BackedEnum|Stringable|string|null $component): self
- {
- if ($component instanceof UriComponentInterface) {
- $component = $component->value();
- }
- $component = self::formatComponent(Encoder::encodeQueryOrFragment($component));
- return $this->qComponent === $component ? $this : new self(
- nid: $this->nid,
- nss: $this->nss,
- rComponent: $this->rComponent,
- qComponent: $component,
- fComponent: $this->fComponent,
- );
- }
- /**
- * Return an instance with the specified f-component.
- *
- * This method MUST retain the state of the current instance, and return
- * an instance that contains the specified f-component.
- *
- * The component is removed if the value is null.
- *
- * @throws SyntaxError for invalid component or transformations
- * that would result in an object in invalid state.
- */
- public function withFComponent(BackedEnum|Stringable|string|null $component): self
- {
- if ($component instanceof UriComponentInterface) {
- $component = $component->value();
- }
- $component = self::formatComponent(Encoder::encodeQueryOrFragment($component));
- return $this->fComponent === $component ? $this : new self(
- nid: $this->nid,
- nss: $this->nss,
- rComponent: $this->rComponent,
- qComponent: $this->qComponent,
- fComponent: $component,
- );
- }
- public function normalize(): self
- {
- $copy = new self(
- nid: strtolower($this->nid),
- nss: (string) Encoder::normalizePath($this->nss),
- rComponent: null === $this->rComponent ? $this->rComponent : Encoder::normalizePath($this->rComponent),
- qComponent: Encoder::normalizeQuery($this->qComponent),
- fComponent: Encoder::normalizeFragment($this->fComponent),
- );
- return $copy->uriString === $this->uriString ? $this : $copy;
- }
- public function equals(Urn|Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $other, UrnComparisonMode $urnComparisonMode = UrnComparisonMode::ExcludeComponents): bool
- {
- if (!$other instanceof Urn) {
- $other = self::parse($other);
- }
- return (null !== $other) && match ($urnComparisonMode) {
- UrnComparisonMode::ExcludeComponents => $other->normalize()->toRfc2141() === $this->normalize()->toRfc2141(),
- UrnComparisonMode::IncludeComponents => $other->normalize()->toString() === $this->normalize()->toString(),
- };
- }
- 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),
- default => $this,
- } ?? $this;
- }
- public function transform(callable $callback): static
- {
- return $callback($this);
- }
- /**
- * @return UrnSerialize
- */
- public function __serialize(): array
- {
- return [['urn' => $this->toString()], []];
- }
- /**
- * @param UrnSerialize $data
- *
- * @throws SyntaxError
- */
- public function __unserialize(array $data): void
- {
- [$properties] = $data;
- $uri = self::fromString($properties['urn'] ?? throw new SyntaxError('The `urn` property is missing from the serialized object.'));
- $this->nid = $uri->nid;
- $this->nss = $uri->nss;
- $this->rComponent = $uri->rComponent;
- $this->qComponent = $uri->qComponent;
- $this->fComponent = $uri->fComponent;
- $this->uriString = $uri->uriString;
- }
- /**
- * @return UrnMap
- */
- public function toComponents(): array
- {
- return [
- 'scheme' => 'urn',
- 'nid' => $this->nid,
- 'nss' => $this->nss,
- 'r_component' => $this->rComponent,
- 'q_component' => $this->qComponent,
- 'f_component' => $this->fComponent,
- ];
- }
- /**
- * @return UrnMap
- */
- public function __debugInfo(): array
- {
- return $this->toComponents();
- }
- }
|