🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,319 @@
<?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\Components;
use Deprecated;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\HostInterface;
use League\Uri\Contracts\PortInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Contracts\UserInfoInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use SensitiveParameter;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_string;
final class Authority extends Component implements AuthorityInterface
{
private readonly HostInterface $host;
private readonly PortInterface $port;
private readonly UserInfoInterface $userInfo;
public function __construct(
Stringable|string|null $host,
Stringable|string|int|null $port = null,
#[SensitiveParameter] Stringable|string|null $userInfo = null
) {
$this->host = !$host instanceof HostInterface ? Host::new($host) : $host;
$this->port = !$port instanceof PortInterface ? Port::new($port) : $port;
$this->userInfo = !$userInfo instanceof UserInfoInterface ? UserInfo::new($userInfo) : $userInfo;
if (null === $this->host->value() && null !== $this->value()) {
throw new SyntaxError('A non-empty authority must contains a non null host.');
}
}
/**
* @throws SyntaxError If the component contains invalid HostInterface part.
*/
public static function new(Stringable|string|null $value = null): self
{
$components = UriString::parseAuthority(self::filterComponent($value));
return new self(
Host::new($components['host']),
Port::new($components['port']),
new UserInfo(
$components['user'],
$components['pass']
)
);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatwgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
if ($uri instanceof Rfc3986Uri) {
return new self($uri->getHost(), $uri->getPort(), $uri->getUserInfo());
}
if ($uri instanceof WhatWgUrl) {
$userInfo = $uri->getUsername();
if (null !== ($password = $uri->getPassword())) {
$userInfo .= ':'.$password;
}
return new self($uri->getUnicodeHost(), $uri->getPort(), $userInfo);
}
if ($uri instanceof Psr7UriInterface) {
$components = UriString::parse($uri);
$userInfo = $components['user'];
if (null !== ($password = $components['pass'])) {
$userInfo .= ':'.$password;
}
return new self($components['host'], $components['port'], $userInfo);
}
return self::new($uri->getAuthority());
}
/**
* Create a new instance from a hash of parse_url parts.
*
* Create a new instance from a hash representation of the URI similar
* to PHP parse_url function result
*
* @param array{
* user? : ?string,
* pass? : ?string,
* host? : ?string,
* port? : ?int
* } $components
*/
public static function fromComponents(array $components): self
{
$components += ['user' => null, 'pass' => null, 'host' => null, 'port' => null];
return match (true) {
null === $components['user'] => new self($components['host'], $components['port']),
null === $components['pass'] => new self($components['host'], $components['port'], $components['user']),
default => new self($components['host'], $components['port'], $components['user'].':'.$components['pass']),
};
}
public function value(): ?string
{
return self::getAuthorityValue($this->userInfo, $this->host, $this->port);
}
private static function getAuthorityValue(
UserInfoInterface $userInfo,
HostInterface $host,
PortInterface $port
): ?string {
$auth = $host->value();
$port = $port->value();
if (null !== $port) {
$auth .= ':'.$port;
}
$userInfo = $userInfo->value();
return match (null) {
$userInfo => $auth,
default => $userInfo.'@'.$auth,
};
}
public function getUriComponent(): string
{
return match (null) {
$this->host->value() => $this->toString(),
default => '//'.$this->toString(),
};
}
public function getHost(): ?string
{
return $this->host->value();
}
public function getPort(): ?int
{
return $this->port->toInt();
}
public function getUserInfo(): ?string
{
return $this->userInfo->value();
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
/**
* @return array{user: ?string, pass: ?string, host: ?string, port: ?int}
*/
public function components(): array
{
return $this->userInfo->components() + [
'host' => $this->host->value(),
'port' => $this->port->toInt(),
];
}
public function withHost(Stringable|string|null $host): AuthorityInterface
{
if (!$host instanceof HostInterface) {
$host = Host::new($host);
}
return match ($this->host->value()) {
$host->value() => $this,
default => new self($host, $this->port, $this->userInfo),
};
}
public function withPort(Stringable|string|int|null $port): AuthorityInterface
{
if (!$port instanceof PortInterface) {
$port = Port::new($port);
}
return match ($this->port->value()) {
$port->value() => $this,
default => new self($this->host, $port, $this->userInfo),
};
}
public function withUserInfo(Stringable|string|null $user, #[SensitiveParameter] Stringable|string|null $password = null): AuthorityInterface
{
$userInfo = new UserInfo($user, $password);
return match ($this->userInfo->value()) {
$userInfo->value() => $this,
default => new self($this->host, $this->port, $userInfo),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Authority::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Authority::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(UriInterface|Psr7UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Authority::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\Authority::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $authority): self
{
return self::new($authority);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Authority::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from null.
*/
#[Deprecated(message:'use League\Uri\Components\Authority::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromNull(): self
{
return self::new();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Authority::fromComponents()
*
* @codeCoverageIgnore
*
* Create a new instance from a hash of parse_url parts.
*
* Create a new instance from a hash representation of the URI similar
* to PHP parse_url function result
*
* @param array{
* user? : ?string,
* pass? : ?string,
* host? : ?string,
* port? : ?int
* } $components
*/
#[Deprecated(message:'use League\Uri\Components\Authority::fromComponents() instead', since:'league/uri-components:7.0.0')]
public static function createFromComponents(array $components): self
{
return self::fromComponents($components);
}
}
@@ -0,0 +1,110 @@
<?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\Components;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Modifier;
use League\Uri\Uri;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_bool;
use function preg_match;
use function sprintf;
abstract class Component implements UriComponentInterface, Conditionable
{
protected const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/';
abstract public function value(): ?string;
public function jsonSerialize(): ?string
{
return $this->value();
}
public function toString(): string
{
return $this->value() ?? '';
}
public function __toString(): string
{
return $this->toString();
}
public function getUriComponent(): string
{
return $this->toString();
}
final protected static function filterUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): WhatWgUrl|Rfc3986Uri|UriInterface|Psr7UriInterface
{
if ($uri instanceof Modifier) {
return $uri->unwrap();
}
if ($uri instanceof Rfc3986Uri
|| $uri instanceof WhatWgUrl
|| $uri instanceof PSR7UriInterface
|| $uri instanceof UriInterface
) {
return $uri;
}
return Uri::new($uri);
}
/**
* Validate the component content.
*/
protected function validateComponent(Stringable|int|string|null $component): ?string
{
return Encoder::decodeNecessary($component);
}
/**
* Filter the input component.
*
* @throws SyntaxError If the component cannot be converted to a string or null
*/
final protected static function filterComponent(Stringable|int|string|null $component): ?string
{
return match (true) {
$component instanceof UriComponentInterface => $component->value(),
null === $component => null,
1 === preg_match(self::REGEXP_INVALID_URI_CHARS, (string) $component) => throw new SyntaxError(sprintf('Invalid component string: %s.', $component)),
default => (string) $component,
};
}
final 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;
}
}
@@ -0,0 +1,453 @@
<?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\Components;
use Deprecated;
use finfo;
use League\Uri\Contracts\DataPathInterface;
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\FeatureDetection;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use SplFileObject;
use Stringable;
use Throwable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function base64_decode;
use function base64_encode;
use function count;
use function explode;
use function file_get_contents;
use function implode;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function sprintf;
use function str_replace;
use function strlen;
use function strtolower;
use const FILEINFO_MIME;
final class DataPath extends Component implements DataPathInterface
{
/**
* All ASCII letters sorted by typical frequency of occurrence.
*/
private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";
private const BINARY_PARAMETER = 'base64';
private const DEFAULT_MIMETYPE = 'text/plain';
private const DEFAULT_PARAMETER = 'charset=us-ascii';
private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
private const REGEXP_DATAPATH = '/^\w+\/[-.\w]+(?:\+[-.\w]+)?;,$/';
private const REGEXP_DATAPATH_ENCODING = '/[^A-Za-z0-9_\-.~!$&\'()*+,;=%:\/@]+|%(?![A-Fa-f0-9]{2})/x';
private readonly PathInterface $path;
private readonly string $mimetype;
/** @var string[] */
private readonly array $parameters;
private readonly bool $isBinaryData;
private readonly string $document;
/**
* New instance.
*/
private function __construct(Stringable|string $path)
{
/** @var string $path */
$path = self::filterComponent($path);
$this->path = Path::new($this->filterPath($path));
[$mediaType, $this->document] = explode(',', $this->path->toString(), 2) + [1 => ''];
[$mimetype, $parameters] = explode(';', $mediaType, 2) + [1 => ''];
$this->mimetype = $this->filterMimeType($mimetype);
[$this->parameters, $this->isBinaryData] = $this->filterParameters($parameters);
$this->validateDocument();
}
/**
* Filter the data path.
*
* @throws SyntaxError If the path is null
* @throws SyntaxError If the path is not valid according to RFC2937
*/
private function filterPath(string $path): string
{
if ('' === $path || ',' === $path) {
return 'text/plain;charset=us-ascii,';
}
if (1 === preg_match(self::REGEXP_DATAPATH, $path)) {
$path = substr($path, 0, -1).'charset=us-ascii,';
}
if (strlen($path) !== strspn($path, self::ASCII) || !str_contains($path, ',')) {
throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937.', $path));
}
return $path;
}
/**
* Filter the mimeType property.
*
* @throws SyntaxError If the mimetype is invalid
*/
private function filterMimeType(string $mimetype): string
{
return match (true) {
'' === $mimetype => self::DEFAULT_MIMETYPE,
1 === preg_match(self::REGEXP_MIMETYPE, $mimetype) => $mimetype,
default => throw new SyntaxError(sprintf('Invalid mimeType, `%s`.', $mimetype)),
};
}
/**
* Extract and set the binary flag from the parameters if it exists.
*
* @throws SyntaxError If the mediatype parameters contain invalid data
*
* @return array{0:array<string>, 1:bool}
*/
private function filterParameters(string $parameters): array
{
if ('' === $parameters) {
return [[self::DEFAULT_PARAMETER], false];
}
$isBinaryData = false;
if (1 === preg_match(',(;|^)'.self::BINARY_PARAMETER.'$,', $parameters, $matches)) {
$parameters = substr($parameters, 0, - strlen($matches[0]));
$isBinaryData = true;
}
$params = array_filter(explode(';', $parameters), fn (string $param) => '' !== $param);
if ([] !== array_filter($params, $this->validateParameter(...))) {
throw new SyntaxError(sprintf('Invalid mediatype parameters, `%s`.', $parameters));
}
return [$params, $isBinaryData];
}
/**
* Validate mediatype parameter.
*/
private function validateParameter(string $parameter): bool
{
$properties = explode('=', $parameter);
return 2 !== count($properties) || self::BINARY_PARAMETER === strtolower($properties[0]);
}
/**
* Validate the path document string representation.
*
* @throws SyntaxError If the data is invalid
*/
private function validateDocument(): void
{
if (!$this->isBinaryData) {
return;
}
$res = base64_decode($this->document, true);
if (false === $res || $this->document !== base64_encode($res)) {
throw new SyntaxError(sprintf('Invalid document, `%s`.', $this->document));
}
}
/**
* Returns a new instance from a string or a stringable object.
*/
public static function new(Stringable|string $value = ''): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string $uri = ''): ?self
{
try {
return self::new($uri);
} catch (Throwable) {
return null;
}
}
/**
* Creates a new instance from a file path.
*
* @param null|resource $context
*
* @throws SyntaxError If the File is not readable
*/
public static function fromFileContents(string $path, $context = null): self
{
FeatureDetection::supportsFileDetection();
$fileArgs = [$path, false];
$mimeArgs = [$path, FILEINFO_MIME];
if (null !== $context) {
$fileArgs[] = $context;
$mimeArgs[] = $context;
}
$content = @file_get_contents(...$fileArgs);
if (false === $content) {
throw new SyntaxError(sprintf('`%s` failed to open stream: No such file or directory.', $path));
}
$mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mimeArgs);
return new self(
str_replace(' ', '', $mimetype)
.';base64,'.base64_encode($content)
);
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
return self::new(Path::fromUri($uri)->toString());
}
public function value(): ?string
{
return $this->path->value();
}
public function equals(mixed $value): bool
{
return $this->path->equals($value);
}
public function getData(): string
{
return $this->document;
}
public function isBinaryData(): bool
{
return $this->isBinaryData;
}
public function getMimeType(): string
{
return $this->mimetype;
}
public function getParameters(): string
{
return implode(';', $this->parameters);
}
public function getMediaType(): string
{
return $this->getMimeType().';'.$this->getParameters();
}
public function isAbsolute(): bool
{
return $this->path->isAbsolute();
}
public function hasTrailingSlash(): bool
{
return $this->path->hasTrailingSlash();
}
public function decoded(): string
{
return $this->path->decoded();
}
public function normalize(): self
{
return new self((string) $this->path->normalize()->value());
}
/**
* @param ?resource $context
*/
public function save(string $path, string $mode = 'w', $context = null): SplFileObject
{
$data = $this->isBinaryData ? base64_decode($this->document, true) : rawurldecode($this->document);
$file = new SplFileObject($path, $mode, context: $context);
$file->fwrite((string) $data);
return $file;
}
public function toBinary(): DataPathInterface
{
if ($this->isBinaryData) {
return $this;
}
return new self($this->formatComponent(
$this->mimetype,
$this->getParameters(),
true,
base64_encode(rawurldecode($this->document))
));
}
/**
* Format the DataURI string.
*/
private function formatComponent(
string $mimetype,
string $parameters,
bool $isBinaryData,
string $data
): string {
if ('' !== $parameters) {
$parameters = ';'.$parameters;
}
if ($isBinaryData) {
$parameters .= ';base64';
}
$path = $mimetype.$parameters.','.$data;
return preg_replace_callback(
self::REGEXP_DATAPATH_ENCODING,
static fn (array $matches): string => rawurlencode($matches[0]),
$path
) ?? $path;
}
public function toAscii(): DataPathInterface
{
return match (false) {
$this->isBinaryData => $this,
default => new self($this->formatComponent(
$this->mimetype,
$this->getParameters(),
false,
rawurlencode((string)base64_decode($this->document, true))
)),
};
}
public function withoutDotSegments(): PathInterface
{
return $this;
}
public function withLeadingSlash(): PathInterface
{
return new self($this->path->withLeadingSlash());
}
public function withoutLeadingSlash(): PathInterface
{
return $this;
}
public function withoutTrailingSlash(): PathInterface
{
$path = $this->path->withoutTrailingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withTrailingSlash(): PathInterface
{
$path = $this->path->withTrailingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withParameters(Stringable|string $parameters): DataPathInterface
{
$parameters = (string) $parameters;
return match ($this->getParameters()) {
$parameters => $this,
default => new self($this->formatComponent(
$this->mimetype,
$parameters,
$this->isBinaryData,
$this->document
)),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see DataPath::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\DataPath::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $path): self
{
return self::new($path);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see DataPath::fromFilePath()
*
* @codeCoverageIgnore
*
* Creates a new instance from a file path.
*
* @param null|resource $context
*
* @throws SyntaxError If the File is not readable
*/
#[Deprecated(message:'use League\Uri\Components\DataPath::fromFilePath() instead', since:'league/uri-components:7.0.0')]
public static function createFromFilePath(string $path, $context = null): self
{
return self::fromFileContents($path, $context);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see DataPath::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\DataPath::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,420 @@
<?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\Components;
use Deprecated;
use Iterator;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\DomainHostInterface;
use League\Uri\Contracts\HostInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\OffsetOutOfBounds;
use League\Uri\Exceptions\SyntaxError;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use TypeError;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_count_values;
use function array_filter;
use function array_keys;
use function array_reverse;
use function array_shift;
use function count;
use function explode;
use function implode;
use function sprintf;
final class Domain extends Component implements DomainHostInterface
{
private const SEPARATOR = '.';
private readonly HostInterface $host;
/** @var string[] */
private readonly array $labels;
private function __construct(Stringable|string|null $host)
{
$host = match (true) {
$host instanceof HostInterface => $host,
$host instanceof UriComponentInterface => Host::new($host->value()),
default => Host::new($host),
};
if (!$host->isDomain()) {
throw new SyntaxError(sprintf('`%s` is an invalid domain name.', $host->value() ?? 'null'));
}
$this->host = $host;
$this->labels = array_reverse(explode(self::SEPARATOR, $this->host->value() ?? ''));
}
/**
* Returns a new instance from a string or a stringable object.
*/
public static function new(Stringable|string|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Returns a new instance from an iterable structure.
*/
public static function fromLabels(Stringable|string ...$labels): self
{
return new self(match ([]) {
$labels => null,
default => implode(self::SEPARATOR, array_reverse(array_map(
fn ($label) => self::filterComponent($label),
$labels
))),
});
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
return self::new(Host::fromUri($uri));
}
/**
* Create a new instance from an Authority object.
*/
public static function fromAuthority(Stringable|string $authority): self
{
return self::new(Host::fromAuthority($authority));
}
public function value(): ?string
{
return $this->host->value();
}
public function equals(mixed $value): bool
{
return $this->host->equals($value);
}
public function toAscii(): ?string
{
return $this->host->toAscii();
}
public function toUnicode(): ?string
{
return $this->host->toUnicode();
}
public function encoded(): ?string
{
return $this->host->encoded();
}
public function isIp(): bool
{
return false;
}
public function isDomain(): bool
{
return true;
}
public function isRegisteredName(): bool
{
return true;
}
public function getIpVersion(): ?string
{
return null;
}
public function getIp(): ?string
{
return null;
}
public function count(): int
{
return count($this->labels);
}
public function getIterator(): Iterator
{
yield from $this->labels;
}
public function get(int $offset): ?string
{
if ($offset < 0) {
$offset += count($this->labels);
}
return $this->labels[$offset] ?? null;
}
public function keys(?string $label = null): array
{
return match (null) {
$label => array_keys($this->labels),
default => array_keys($this->labels, $label, true),
};
}
public function isAbsolute(): bool
{
return count($this->labels) > 1 && '' === $this->labels[array_key_first($this->labels)];
}
public function prepend(Stringable|string|int|null $label): DomainHostInterface
{
$label = self::filterComponent($label);
$value = $this->value();
return match (true) {
null === $label => $this,
null === $value => new self($label),
str_ends_with($label, self::SEPARATOR) => new self($label.$value),
default => new self($label.self::SEPARATOR.$value),
};
}
public function append(Stringable|string|int|null $label): DomainHostInterface
{
$label = self::filterComponent($label);
$value = $this->value();
return match (true) {
null === $label => $this,
null === $value => new self($label),
!$this->isAbsolute() => new self($value.self::SEPARATOR.$label),
str_ends_with($label, self::SEPARATOR) => new self($value.$label),
default => new self($value.$label.self::SEPARATOR),
};
}
public function withRootLabel(): DomainHostInterface
{
$key = array_key_first($this->labels);
return match ($this->labels[$key]) {
'' => $this,
default => $this->append(''),
};
}
public function slice(int $offset, ?int $length = null): self
{
$nbLabels = count($this->labels);
if ($offset < -$nbLabels || $offset > $nbLabels) {
throw new OffsetOutOfBounds(sprintf('No label can be found with at : `%s`.', $offset));
}
$labels = array_slice($this->labels, $offset, $length, true);
return match ($labels) {
$this->labels => $this,
default => self::fromLabels(...$labels),
};
}
public function withoutRootLabel(): DomainHostInterface
{
$key = array_key_first($this->labels);
if ('' !== $this->labels[$key]) {
return $this;
}
$labels = $this->labels;
array_shift($labels);
return self::fromLabels(...$labels);
}
/**
* @throws OffsetOutOfBounds
*/
public function withLabel(int $key, Stringable|string|int|null $label): DomainHostInterface
{
$nbLabels = count($this->labels);
if ($key < - $nbLabels - 1 || $key > $nbLabels) {
throw new OffsetOutOfBounds(sprintf('No label can be added with the submitted key : `%s`.', $key));
}
if (0 > $key) {
$key += $nbLabels;
}
if ($nbLabels === $key) {
return $this->append($label);
}
if (-1 === $key) {
return $this->prepend($label);
}
if (!$label instanceof HostInterface && null !== $label) {
$label = Host::new((string) $label)->value();
}
if ($label === $this->labels[$key]) {
return $this;
}
$labels = $this->labels;
$labels[$key] = $label;
return new self(implode(self::SEPARATOR, array_reverse($labels)));
}
public function withoutLabel(int ...$keys): DomainHostInterface
{
if ([] === $keys) {
return $this;
}
$nb_labels = count($this->labels);
foreach ($keys as &$offset) {
if (- $nb_labels > $offset || $nb_labels - 1 < $offset) {
throw new OffsetOutOfBounds(sprintf('No label can be removed with the submitted key : `%s`.', $offset));
}
if (0 > $offset) {
$offset += $nb_labels;
}
}
unset($offset);
$deleted_keys = array_keys(array_count_values($keys));
$filter = static fn ($key): bool => !in_array($key, $deleted_keys, true);
return self::fromLabels(...array_filter($this->labels, $filter, ARRAY_FILTER_USE_KEY));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::getIterator()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::getIterator() instead', since:'league/uri-components:7.0.0')]
public function labels(): array
{
return $this->labels;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $host): self
{
return self::new($host);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::fromLabels()
*
* @codeCoverageIgnore
*
* Returns a new instance from an iterable structure.
*
* @throws TypeError If a label is the null value
*/
#[Deprecated(message:'use League\Uri\Components\Domain::fromLabels() instead', since:'league/uri-components:7.0.0')]
public static function createFromLabels(iterable $labels): self
{
return self::fromLabels(...$labels);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::fromAuthority()
*
* @codeCoverageIgnore
*
* Create a new instance from an Authority object.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::fromAuthority() instead', since:'league/uri-components:7.0.0')]
public static function createFromAuthority(AuthorityInterface|Stringable|string $authority): self
{
return self::fromAuthority($authority);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from an iterable structure.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromHost(HostInterface $host): self
{
return self::new($host);
}
}
@@ -0,0 +1,146 @@
<?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\Components;
use Deprecated;
use League\Uri\Contracts\FragmentInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_string;
use function str_replace;
final class Fragment extends Component implements FragmentInterface
{
private readonly ?string $fragment;
/**
* New instance.
*/
private function __construct(Stringable|string|null $fragment)
{
$this->fragment = $this->validateComponent($fragment);
}
public static function new(Stringable|string|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
return match (true) {
$uri instanceof Rfc3986Uri => new self($uri->getRawFragment()),
$uri instanceof Psr7UriInterface => new self(UriString::parse($uri)['fragment']),
default => new self($uri->getFragment()),
};
}
public function value(): ?string
{
return Encoder::encodeQueryOrFragment($this->fragment);
}
public function getUriComponent(): string
{
return (null === $this->fragment ? '' : '#').$this->value();
}
/**
* Returns the decoded fragment.
*/
public function decoded(): ?string
{
if (null === $this->fragment) {
return null;
}
return str_replace('%20', ' ', $this->fragment);
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function normalize(): self
{
return new self(Encoder::normalizeFragment($this->value()));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Fragment::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Fragment::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $fragment): self
{
return self::new($fragment);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Fragment::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Fragment::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,391 @@
<?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\Components;
use Countable;
use IteratorAggregate;
use League\Uri\Components\FragmentDirectives\DirectiveString;
use League\Uri\Contracts\FragmentDirective;
use League\Uri\Contracts\FragmentInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\OffsetOutOfBounds;
use League\Uri\Modifier;
use League\Uri\Uri;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Throwable;
use Traversable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_count_values;
use function array_filter;
use function array_keys;
use function array_map;
use function array_slice;
use function array_values;
use function count;
use function explode;
use function implode;
use function in_array;
use function is_bool;
use function is_string;
use function sprintf;
use function str_replace;
use function strpos;
use function substr;
use const ARRAY_FILTER_USE_BOTH;
/**
* @see https://wicg.github.io/scroll-to-text-fragment/
*
* @implements IteratorAggregate<int, FragmentDirective>
*/
final class FragmentDirectives implements FragmentInterface, IteratorAggregate, Countable
{
public const DELIMITER = ':~:';
public const SEPARATOR = '&';
/** @var list<FragmentDirective> */
private readonly array $directives;
public function __construct(FragmentDirective|Stringable|string ...$directives)
{
$this->directives = array_values(array_map(self::filterDirective(...), $directives));
}
/**
* Create a new instance from a Fragment.
*
* If no delimiter is found, an empty collection is returned
*/
public static function fromFragment(Stringable|string|null $fragment): self
{
if ($fragment instanceof UriComponentInterface) {
$fragment = $fragment->value();
}
if (null === $fragment) {
return new self();
}
$fragment = (string) $fragment;
$pos = strpos($fragment, self::DELIMITER);
if (false === $pos) {
return new self();
}
return self::new(substr($fragment, $pos + 3));
}
/**
* Create a new instance from a string which only contains directives.
*/
public static function new(Stringable|string|null $value): self
{
return null === $value
? new self()
: new self(...explode(self::SEPARATOR, (string) $value));
}
private static function filterDirective(FragmentDirective|Stringable|string $directive): FragmentDirective
{
return $directive instanceof FragmentDirective ? $directive : DirectiveString::resolve($directive);
}
public static function tryNew(Stringable|string|null $value): ?self
{
try {
return self::new($value);
} catch (Throwable) {
return null;
}
}
/**
* Create a new instance from a URI string or object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
if ($uri instanceof Modifier) {
$uri = $uri->unwrap();
}
return self::fromFragment(match (true) {
$uri instanceof Psr7UriInterface => UriString::parse($uri)['fragment'],
$uri instanceof Rfc3986Uri => $uri->getRawFragment(),
$uri instanceof UriInterface, $uri instanceof WhatWgUrl => $uri->getFragment(),
default => Uri::new($uri)->getFragment(),
});
}
public function count(): int
{
return count($this->directives);
}
public function getIterator(): Traversable
{
yield from $this->directives;
}
public function __toString(): string
{
return $this->toString();
}
public function jsonSerialize(): string
{
return $this->toString();
}
public function value(): ?string
{
return [] === $this->directives
? null
: self::DELIMITER.implode(
self::SEPARATOR,
array_map(fn (FragmentDirective $directive): string => $directive->toString(), $this->directives)
);
}
public function toString(): string
{
return (string) $this->value();
}
public function getUriComponent(): string
{
$fragment = $this->value();
return (null === $fragment ? '' : '#').$fragment;
}
public function decoded(): ?string
{
return [] === $this->directives
? null
: str_replace('%20', ' ', (string) Encoder::decodeFragment($this->toString()));
}
/**
* Returns the Directive at a specified offset or null if none is defined.
*
* Negative offsets are supported.
*/
public function nth(int $offset): ?FragmentDirective
{
if ($offset < 0) {
$offset += count($this->directives);
}
return $this->directives[$offset] ?? null;
}
/**
* The first Directive defined on the fragment or null if none are defined.
*/
public function first(): ?FragmentDirective
{
return $this->nth(0);
}
/**
* The last Directive defined on the fragment or null if none are defined.
*/
public function last(): ?FragmentDirective
{
return $this->nth(-1);
}
/**
* Tells whether all the submitted keys are present in the collection.
*
* Negative offsets are supported.
*/
public function has(int ...$offsets): bool
{
$nbDirectives = count($this->directives);
foreach ($offsets as $offset) {
if ($offset < 0) {
$offset += $nbDirectives;
}
if (! isset($this->directives[$offset])) {
return false;
}
}
return [] !== $offsets;
}
public function isEmpty(): bool
{
return [] === $this->directives;
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function indexOf(FragmentDirective|Stringable|string $directive): ?int
{
$directive = self::filterDirective($directive);
foreach ($this->directives as $offset => $innerDirective) {
if ($innerDirective->equals($directive)) {
return $offset;
}
}
return null;
}
public function contains(FragmentDirective|Stringable|string $directive): bool
{
return null !== $this->indexOf($directive);
}
/**
* Append one or more Directives to the fragment.
*/
public function append(FragmentDirectives|FragmentDirective|Stringable|string ...$directives): self
{
$items = self::implodeDirectives(...$directives);
return [] === $items ? $this : new self(...$this->directives, ...$items);
}
/**
* Prepend one or more Directives to the fragment.
*/
public function prepend(FragmentDirectives|FragmentDirective|Stringable|string ...$directives): self
{
$items = self::implodeDirectives(...$directives);
return [] === $items ? $this : new self(...$items, ...$this->directives);
}
/**
* @return list<FragmentDirective|Stringable|string>
*/
private static function implodeDirectives(FragmentDirectives|FragmentDirective|Stringable|string ...$directives): array
{
return array_merge(...array_map(fn ($d) => $d instanceof FragmentDirectives ? [...$d] : [$d], $directives));
}
/**
* Removes one or more Directives by offset from the fragment.
*/
public function remove(int ...$keys): self
{
if ([] === $keys) {
return $this;
}
$nbDirectives = count($this->directives);
$deletedKeys = [];
foreach ($keys as $key) {
$value = $key;
if ($value < 0) {
$value += $nbDirectives;
}
isset($this->directives[$value]) || throw new OffsetOutOfBounds(sprintf('The key `%s` is invalid.', $key));
$deletedKeys[] = $value;
}
$deletedKeys = array_keys(array_count_values($deletedKeys));
return $this->filter(fn (FragmentDirective $directive, int $offset): bool => !in_array($offset, $deletedKeys, true)); /* @phpstan-ignore-line */
}
/**
* Slices the fragment to remove Directives portions.
*/
public function slice(int $offset, ?int $length = null): self
{
$nbDirectives = count($this->directives);
($offset >= -$nbDirectives && $offset <= $nbDirectives) || throw new OffsetOutOfBounds(sprintf('No directive can be found at : `%s`.', $offset));
$directives = array_slice($this->directives, $offset, $length);
return $directives === $this->directives ? $this : new self(...$directives);
}
/**
* Filter the Directives to return a new instance based on the callback.
*
* @param callable(FragmentDirective, int=): bool $callback
*/
public function filter(callable $callback): self
{
$directives = array_filter($this->directives, $callback, ARRAY_FILTER_USE_BOTH);
return $directives === $this->directives ? $this : new self(...$directives);
}
/**
* Replace the Directive define at a specific offset.
* Negative offsets are supported.
*
* If no Directive is found to the specified offset, an exception is thrown
*/
public function replace(int $offset, FragmentDirective|Stringable|string $directive): self
{
$currentDirective = $this->nth($offset);
null !== $currentDirective || throw new OffsetOutOfBounds(sprintf('The key `%s` is invalid.', $offset));
$directive = self::filterDirective($directive);
if ($directive::class === $currentDirective::class && $currentDirective->equals($directive)) {
return $this;
}
if ($offset < 0) {
$offset += count($this->directives);
}
$directives = $this->directives;
$directives[$offset] = $directive;
return new self(...$directives);
}
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
}
@@ -0,0 +1,39 @@
<?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\Components\FragmentDirectives;
use League\Uri\Contracts\FragmentDirective;
use Stringable;
use function preg_match;
final class DirectiveString
{
/**
* Parse a Directive string representation.
*
* A Directive syntax is name[=value] where the
* separator `=` is not present when no value
* is attached to it
*/
public static function resolve(Stringable|string $directive): FragmentDirective
{
$directive = (string) $directive;
return match (true) {
1 === preg_match('/^text(?:=|$)/i', $directive) => TextDirective::fromString($directive),
default => GenericDirective::fromString($directive),
};
}
}
@@ -0,0 +1,96 @@
<?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\Components\FragmentDirectives;
use League\Uri\Contracts\FragmentDirective;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use Throwable;
use function explode;
use function str_replace;
final class GenericDirective implements FragmentDirective
{
/**
* @param non-empty-string $name
*/
private function __construct(
private readonly string $name,
private readonly ?string $value = null,
) {
}
/**
* Create a new instance from a string without the Directive delimiter (:~:) or a separator (&).
*/
public static function fromString(Stringable|string $value): self
{
[$name, $value] = explode('=', (string) $value, 2) + [1 => null];
(null !== $name && '' !== $name && !str_contains($name, '&')) || throw new SyntaxError('The submitted text is not a valid directive.');
return new self($name, $value);
}
private static function decode(?string $value): ?string
{
return null !== $value ? str_replace('%20', ' ', (string) Encoder::decodeFragment($value)) : null;
}
public function name(): string
{
/** @var non-empty-string $name */
$name = (string) self::decode($this->name);
return $name;
}
public function value(): ?string
{
return self::decode($this->value);
}
public function toString(): string
{
$str = $this->name;
if (null === $this->value) {
return $str;
}
return $str.'='.$this->value;
}
public function __toString(): string
{
return $this->toString();
}
public function equals(mixed $directive): bool
{
if (!$directive instanceof Stringable && !is_string($directive)) {
return false;
}
if (!$directive instanceof FragmentDirective) {
try {
$directive = self::fromString($directive);
} catch (Throwable) {
return false;
}
}
return $directive->toString() === $this->toString();
}
}
@@ -0,0 +1,246 @@
<?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\Components\FragmentDirectives;
use League\Uri\Contracts\FragmentDirective;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use Throwable;
use function explode;
use function is_string;
use function preg_match;
use function str_replace;
final class TextDirective implements FragmentDirective
{
private const NAME = 'text';
private const REGEXP_PATTERN = '/^
(?:(?<prefix>.+?)-,)? # optional prefix up to first "-,"
(?<start>[^,]+?) # required start (up to "," or end)
(?:,(?<end>[^,-]*),?)? # optional end, stop before ",-" if present
(?:,-(?<suffix>.+))? # optional suffix (to end)
$/x';
/**
* @param non-empty-string $start
* @param ?non-empty-string $end
* @param ?non-empty-string $prefix
* @param ?non-empty-string $suffix
*/
public function __construct(
public readonly string $start,
public readonly ?string $end = null,
public readonly ?string $prefix = null,
public readonly ?string $suffix = null,
) {
('' !== $this->start && '' !== $this->end && '' !== $this->prefix && '' !== $this->suffix)
|| throw new SyntaxError('The start part can not be the empty string.');
}
/**
* Create a new instance from a string without the Directive delimiter (:~:) or a separator (&).
*/
public static function fromString(Stringable|string $value): self
{
[$name, $value] = explode('=', (string) $value, 2) + [1 => ''];
self::NAME === $name || throw new SyntaxError('The submitted text is not a text directive.');
return self::fromValue($value);
}
/**
* Create a new instance from a string without the Directive name and the separator (=).
*/
public static function fromValue(Stringable|string $text): self
{
'' !== $text || throw new SyntaxError('The text directive value can not be the empty string.');
1 === preg_match(self::REGEXP_PATTERN, (string) $text, $matches) || throw new SyntaxError('The text directive is malformed.');
if ('' === $matches['prefix']) {
$matches['prefix'] = null;
}
/** @var non-empty-string $start */
$start = (string) self::decode($matches['start']);
/** @var ?non-empty-string $prefix */
$prefix = self::decode($matches['prefix']);
/** @var ?non-empty-string $suffix */
$suffix = self::decode($matches['suffix'] ?? null);
$matches['end'] ??= null;
if ('' === $matches['end']) {
$matches['end'] = null;
}
/** @var ?non-empty-string $end */
$end = self::decode($matches['end']);
return new self($start, $end, $prefix, $suffix);
}
private static function encode(?string $value): ?string
{
return null !== $value ? strtr((string) Encoder::encodeQueryOrFragment($value), ['-' => '%2D', ',' => '%2C', '&' => '%26']) : null;
}
private static function decode(?string $value): ?string
{
if (null === $value) {
return null;
}
return str_replace('%20', ' ', (string) Encoder::decodeFragment($value));
}
public function name(): string
{
return self::NAME;
}
public function value(): string
{
$str = $this->start;
if (null !== $this->prefix) {
$str = $this->prefix.'-,'.$str;
}
if (null !== $this->end) {
$str .= ','.$this->end;
}
if (null !== $this->suffix) {
$str .= ',-'.$this->suffix;
}
return $str;
}
public function toString(): string
{
$encodedValue = (string) self::encode($this->start);
$prefix = self::encode($this->prefix);
if (null !== $prefix) {
$encodedValue = $prefix.'-,'.$encodedValue;
}
$end = self::encode($this->end);
if (null !== $end) {
$encodedValue .= ','.$end;
}
$suffix = self::encode($this->suffix);
if (null !== $suffix) {
$encodedValue .= ',-'.$suffix;
}
return self::NAME.'='.$encodedValue;
}
public function __toString(): string
{
return $this->toString();
}
public function equals(mixed $directive): bool
{
if (!$directive instanceof Stringable && !is_string($directive)) {
return false;
}
if (!$directive instanceof FragmentDirective) {
try {
$directive = self::fromString($directive);
} catch (Throwable) {
return false;
}
}
return $directive->toString() === $this->toString();
}
/**
* Returns a new instance with a new start portion.
*
* The submitted string must be in its decoded form
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new start portion.
*
* @param non-empty-string $text
*/
public function startsWith(string $text): self
{
if ($this->start === $text) {
return $this;
}
return new self($text, $this->end, $this->prefix, $this->suffix);
}
/**
* Returns a new instance with a new end portion.
*
* The submitted string must be in its decoded form
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new end portion.
*
* @param ?non-empty-string $text
*/
public function endsWith(?string $text): self
{
if ($this->end === $text) {
return $this;
}
return new self($this->start, $text, $this->prefix, $this->suffix);
}
/**
* Returns a new instance with a new suffix portion.
*
* The submitted string must be in its decoded form
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new suffix portion.
*
* @param ?non-empty-string $text
*/
public function followedBy(?string $text): self
{
if ($this->suffix === $text) {
return $this;
}
return new self($this->start, $this->end, $this->prefix, $text);
}
/**
* Returns a new instance with a new prefix portion.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new prefix portion.
*
* @param ?non-empty-string $text
*/
public function precededBy(?string $text): self
{
if ($this->prefix === $text) {
return $this;
}
return new self($this->start, $this->end, $text, $this->suffix);
}
}
@@ -0,0 +1,569 @@
<?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\Components;
use Deprecated;
use Iterator;
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\SegmentedPathInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\OffsetOutOfBounds;
use League\Uri\Exceptions\SyntaxError;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use TypeError;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_count_values;
use function array_filter;
use function array_keys;
use function array_pop;
use function array_unshift;
use function count;
use function dirname;
use function explode;
use function implode;
use function ltrim;
use function rtrim;
use function sprintf;
use function str_contains;
use function str_replace;
use function str_starts_with;
use function strrpos;
use function substr;
use const ARRAY_FILTER_USE_KEY;
use const FILTER_VALIDATE_INT;
use const PATHINFO_EXTENSION;
final class HierarchicalPath extends Component implements SegmentedPathInterface
{
private const SEPARATOR = '/';
private const IS_ABSOLUTE = 1;
private const IS_RELATIVE = 0;
private readonly PathInterface $path;
/** @var array<string> */
private readonly array $segments;
private function __construct(Stringable|string $path)
{
if (!$path instanceof PathInterface) {
$path = Path::new($path);
}
$this->path = $path;
$segments = $this->path->decoded();
if ($this->path->isAbsolute()) {
$segments = substr($segments, 1);
}
$this->segments = explode(self::SEPARATOR, $segments);
}
/**
* Returns a new instance from a string or a stringable object.
*/
public static function new(Stringable|string $value = ''): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string $uri = ''): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
return new self(Path::fromUri($uri));
}
/**
* Returns a new instance from an iterable structure.
*
* @throws TypeError If the segments are malformed
*/
public static function fromRelative(string ...$segments): self
{
return self::fromSegments(self::IS_RELATIVE, $segments);
}
/**
* Returns a new instance from an iterable structure.
*
* @throws TypeError If the segments are malformed
*/
public static function fromAbsolute(string ...$segments): self
{
return self::fromSegments(self::IS_ABSOLUTE, $segments);
}
/**
* @param array<string> $segments
*/
private static function fromSegments(int $pathType, array $segments): self
{
$path = implode(self::SEPARATOR, $segments);
return match (true) {
self::IS_RELATIVE === $pathType => new self(ltrim($path, self::SEPARATOR)),
self::SEPARATOR !== ($path[0] ?? '') => new self(self::SEPARATOR.$path),
default => new self($path),
};
}
public function count(): int
{
return count($this->segments);
}
public function getIterator(): Iterator
{
yield from $this->segments;
}
public function isAbsolute(): bool
{
return $this->path->isAbsolute();
}
public function hasTrailingSlash(): bool
{
return $this->path->hasTrailingSlash();
}
public function value(): ?string
{
return $this->path->value();
}
public function equals(mixed $value): bool
{
return $this->path->equals($value);
}
public function decoded(): string
{
return $this->path->decoded();
}
public function normalize(): self
{
return new self((string) $this->path->normalize()->value());
}
public function getDirname(): string
{
$path = $this->path->decoded();
return str_replace(
['\\', "\0"],
[self::SEPARATOR, '\\'],
dirname(str_replace('\\', "\0", $path))
);
}
public function getBasename(): string
{
$data = $this->segments;
$basename = (string) array_pop($data);
$pos = strpos($basename, ';');
return match (false) {
$pos => $basename,
default => substr($basename, 0, $pos),
};
}
public function getExtension(): string
{
[$basename] = explode(';', $this->getBasename(), 2);
return pathinfo($basename, PATHINFO_EXTENSION);
}
public function get(int $offset): ?string
{
if ($offset < 0) {
$offset += count($this->segments);
}
return $this->segments[$offset] ?? null;
}
public function keys(Stringable|string|null $segment = null): array
{
$segment = self::filterComponent($segment);
return match (null) {
$segment => array_keys($this->segments),
default => array_keys($this->segments, $segment, true),
};
}
public function withoutDotSegments(): PathInterface
{
$path = $this->path->withoutDotSegments();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withLeadingSlash(): PathInterface
{
$path = $this->path->withLeadingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withoutLeadingSlash(): PathInterface
{
$path = $this->path->withoutLeadingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withoutTrailingSlash(): PathInterface
{
$path = $this->path->withoutTrailingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withTrailingSlash(): PathInterface
{
$path = $this->path->withTrailingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function append(Stringable|string $segment): SegmentedPathInterface
{
/** @var string $segment */
$segment = self::filterComponent($segment);
return new self(
rtrim($this->path->toString(), self::SEPARATOR)
.self::SEPARATOR
.ltrim($segment, self::SEPARATOR)
);
}
public function prepend(Stringable|string $segment): SegmentedPathInterface
{
/** @var string $segment */
$segment = self::filterComponent($segment);
return new self(
rtrim($segment, self::SEPARATOR)
.self::SEPARATOR
.ltrim($this->path->toString(), self::SEPARATOR)
);
}
public function withSegment(int $key, Stringable|string $segment): SegmentedPathInterface
{
$nbSegments = count($this->segments);
if ($key < - $nbSegments - 1 || $key > $nbSegments) {
throw new OffsetOutOfBounds(sprintf('The given key `%s` is invalid.', $key));
}
if (0 > $key) {
$key += $nbSegments;
}
if ($nbSegments === $key) {
return $this->append($segment);
}
if (-1 === $key) {
return $this->prepend($segment);
}
if (!$segment instanceof PathInterface) {
$segment = new self($segment);
}
$segment = Encoder::decodeAll($segment);
if ($segment === $this->segments[$key]) {
return $this;
}
$segments = $this->segments;
$segments[$key] = $segment;
if ($this->isAbsolute()) {
array_unshift($segments, '');
}
return new self(implode(self::SEPARATOR, $segments));
}
public function withoutEmptySegments(): SegmentedPathInterface
{
/** @var string $path */
$path = preg_replace(',/+,', self::SEPARATOR, $this->toString());
return new self($path);
}
public function withoutSegment(int ...$keys): SegmentedPathInterface
{
if ([] === $keys) {
return $this;
}
$nb_segments = count($this->segments);
$options = ['options' => ['min_range' => - $nb_segments, 'max_range' => $nb_segments - 1]];
$deleted_keys = [];
foreach ($keys as $value) {
/** @var false|int $offset */
$offset = filter_var($value, FILTER_VALIDATE_INT, $options);
if (false === $offset) {
throw new OffsetOutOfBounds(sprintf('The key `%s` is invalid.', $value));
}
if ($offset < 0) {
$offset += $nb_segments;
}
$deleted_keys[] = $offset;
}
$deleted_keys = array_keys(array_count_values($deleted_keys));
$filter = static fn ($key): bool => !in_array($key, $deleted_keys, true);
$path = implode(self::SEPARATOR, array_filter($this->segments, $filter, ARRAY_FILTER_USE_KEY));
if ($this->isAbsolute()) {
return new self(self::SEPARATOR.$path);
}
return new self($path);
}
public function slice(int $offset, ?int $length = null): self
{
$nbSegments = count($this->segments);
if ($offset < -$nbSegments || $offset > $nbSegments) {
throw new OffsetOutOfBounds(sprintf('No segment can be found with at : `%s`.', $offset));
}
$segments = array_slice($this->segments, $offset, $length, true);
if ($this->hasTrailingSlash()) {
$segments[] = '';
}
return match (true) {
$segments === $this->segments => $this,
$this->isAbsolute() => self::fromAbsolute(...$segments),
default => self::fromRelative(...$segments),
};
}
public function withDirname(Stringable|string $path): SegmentedPathInterface
{
if (!$path instanceof PathInterface) {
$path = Path::new($path);
}
if ($path->value() === $this->getDirname()) {
return $this;
}
$segments = $this->segments;
return new self(
rtrim($path->toString(), self::SEPARATOR)
.self::SEPARATOR
.array_pop($segments)
);
}
public function withBasename(Stringable|string $basename): SegmentedPathInterface
{
/** @var string $basename */
$basename = $this->validateComponent($basename);
return match (true) {
str_contains($basename, self::SEPARATOR) => throw new SyntaxError('The basename cannot contain the path separator.'),
default => $this->withSegment(count($this->segments) - 1, $basename),
};
}
public function withExtension(Stringable|string $extension): SegmentedPathInterface
{
/** @var string $extension */
$extension = $this->validateComponent($extension);
if (str_contains($extension, self::SEPARATOR)) {
throw new SyntaxError('An extension sequence cannot contain a path delimiter.');
}
if (str_starts_with($extension, '.')) {
throw new SyntaxError('An extension sequence cannot contain a leading `.` character.');
}
/** @var string $basename */
$basename = $this->segments[array_key_last($this->segments)];
[$ext, $param] = explode(';', $basename, 2) + [1 => null];
if ('' === $ext) {
return $this;
}
return $this->withBasename($this->buildBasename($extension, (string) $ext, $param));
}
/**
* Creates a new basename with a new extension.
*/
private function buildBasename(string $extension, string $ext, ?string $param = null): string
{
$length = strrpos($ext, '.'.pathinfo($ext, PATHINFO_EXTENSION));
if (false !== $length) {
$ext = substr($ext, 0, $length);
}
if (null !== $param && '' !== $param) {
$param = ';'.$param;
}
$extension = trim($extension);
if ('' === $extension) {
return $ext.$param;
}
return $ext.'.'.$extension.$param;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::getIterator()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::getIterator() instead', since:'league/uri-components:7.0.0')]
public function segments(): array
{
return $this->segments;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $path): self
{
return self::new($path);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromPath(PathInterface $path): self
{
return self::new($path);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws TypeError If the segments are malformed
*@see HierarchicalPath::fromRelative()
*
* @codeCoverageIgnore
*
* Returns a new instance from an iterable structure.
*
* @deprecated Since version 7.0.0
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::fromRelative() instead', since:'league/uri-components:7.0.0')]
public static function createRelativeFromSegments(iterable $segments): self
{
return self::fromRelative(...$segments);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws TypeError If the segments are malformed
*@see HierarchicalPath::fromAbsolute()
*
* @codeCoverageIgnore
*
* Returns a new instance from an iterable structure.
*
* @deprecated Since version 7.0.0
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::fromAbsolute() instead', since:'league/uri-components:7.0.0')]
public static function createAbsoluteFromSegments(iterable $segments): self
{
return self::fromAbsolute(...$segments);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,541 @@
<?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\Components;
use Deprecated;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\IpHostInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\ConversionFailed;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnConverter;
use League\Uri\IPv4\Converter as IPv4Converter;
use League\Uri\IPv4Normalizer;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function explode;
use function filter_var;
use function in_array;
use function inet_pton;
use function is_string;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function sprintf;
use function strpos;
use function strtolower;
use function strtoupper;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
final class Host extends Component implements IpHostInterface
{
protected const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* invalid characters in host regular expression
*/
private const REGEXP_INVALID_HOST_CHARS = '/
[:\/?#\[\]@ ] # gen-delims characters as well as the space character
/ix';
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$
/ix';
/**
* Domain name regular expression.
*
* Everything but the domain name length is validated
*
* @see https://tools.ietf.org/html/rfc1034#section-3.5
* @see https://tools.ietf.org/html/rfc1123#section-2.1
* @see https://regex101.com/r/71j6rt/1
*/
private const REGEXP_DOMAIN_NAME = '/
(?(DEFINE)
(?<let_dig> [a-z0-9]) # alpha digit
(?<let_dig_hyp> [a-z0-9-]) # alpha digit and hyphen
(?<ldh_str> (?&let_dig_hyp){0,61}(?&let_dig)) # domain label end
(?<label> (?&let_dig)((?&ldh_str))?) # domain label
(?<domain> (?&label)(\.(?&label)){0,126}\.?) # domain name
)
^(?&domain)$
/ix';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* IPvFuture regular expression
*/
private const REGEXP_IP_FUTURE = '/^
v(?<version>[A-F0-9]+)\.
(?:
(?<unreserved>[a-z0-9_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
private const REGEXP_GEN_DELIMS = '/[:\/?#\[\]@]/';
private const ADDRESS_BLOCK = "\xfe\x80";
private readonly ?string $host;
private readonly bool $isDomain;
private readonly ?string $ipVersion;
private readonly bool $hasZoneIdentifier;
private function __construct(Stringable|int|string|null $host)
{
[
'host' => $this->host,
'is_domain' => $this->isDomain,
'ip_version' => $this->ipVersion,
'has_zone_identifier' => $this->hasZoneIdentifier,
] = $this->parse($host);
}
/**
* @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form
*
* @return array{host:string|null, is_domain:bool, ip_version:string|null, has_zone_identifier:bool}
*/
private function parse(Stringable|int|string|null $host): array
{
$host = self::filterComponent($host);
if (null === $host) {
return [
'host' => null,
'is_domain' => true,
'ip_version' => null,
'has_zone_identifier' => false,
];
}
if ('' === $host) {
return [
'host' => '',
'is_domain' => false,
'ip_version' => null,
'has_zone_identifier' => false,
];
}
static $inMemoryCache = [];
if (isset($inMemoryCache[$host])) {
return $inMemoryCache[$host];
}
if (100 < count($inMemoryCache)) {
unset($inMemoryCache[array_key_first($inMemoryCache)]);
}
if (false !== filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $inMemoryCache[$host] = [
'host' => $host,
'is_domain' => false,
'ip_version' => '4',
'has_zone_identifier' => false,
];
}
if ('[' === $host[0] && str_ends_with($host, ']')) {
$ip_host = substr($host, 1, -1);
if ($this->isValidIpv6Hostname($ip_host)) {
return $inMemoryCache[$host] = [
'host' => $host,
'is_domain' => false,
'ip_version' => '6',
'has_zone_identifier' => str_contains($ip_host, '%'),
];
}
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ip_host, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
return $inMemoryCache[$host] = [
'host' => $host,
'is_domain' => false,
'ip_version' => $matches['version'],
'has_zone_identifier' => false,
];
}
throw new SyntaxError(sprintf('`%s` is an invalid IP literal format.', $host));
}
$domainName = rawurldecode($host);
$isAscii = false;
if (1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $domainName)) {
$domainName = strtolower($domainName);
$isAscii = true;
}
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $domainName)) {
return $inMemoryCache[$domainName] = [
'host' => $domainName,
'is_domain' => $this->isValidDomain($domainName),
'ip_version' => null,
'has_zone_identifier' => false,
];
}
if ($isAscii || 1 === preg_match(self::REGEXP_INVALID_HOST_CHARS, $domainName)) {
throw new SyntaxError(sprintf('`%s` is an invalid domain name : the host contains invalid characters.', $host));
}
$host = IdnConverter::toAsciiOrFail($domainName);
return $inMemoryCache[$host] = [
'host' => $host,
'is_domain' => $this->isValidDomain($host),
'ip_version' => null,
'has_zone_identifier' => false,
];
}
/**
* Tells whether the registered name is a valid domain name according to RFC1123.
*
* @see http://man7.org/linux/man-pages/man7/hostname.7.html
* @see https://tools.ietf.org/html/rfc1123#section-2.1
*/
private function isValidDomain(string $hostname): bool
{
$domainMaxLength = str_ends_with($hostname, '.') ? 254 : 253;
return !isset($hostname[$domainMaxLength])
&& 1 === preg_match(self::REGEXP_DOMAIN_NAME, $hostname);
}
/**
* Validates an Ipv6 as Host.
*
* @see http://tools.ietf.org/html/rfc6874#section-2
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
private function isValidIpv6Hostname(string $host): bool
{
[$ipv6, $scope] = explode('%', $host, 2) + [1 => null];
if (null === $scope) {
return (bool) filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
$scope = rawurldecode('%'.$scope);
return 1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $scope)
&& 1 !== preg_match(self::REGEXP_GEN_DELIMS, $scope)
&& false !== filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& str_starts_with((string)inet_pton((string)$ipv6), self::ADDRESS_BLOCK);
}
public static function new(Stringable|string|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Returns a host from an IP address.
*
* @throws MissingFeature If detecting IPv4 is not possible
* @throws SyntaxError If the $ip cannot be converted into a Host
*/
public static function fromIp(Stringable|string $ip, string $version = ''): self
{
if ('' !== $version) {
return new self('[v'.$version.'.'.$ip.']');
}
$ip = (string) $ip;
if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return new self('['.$ip.']');
}
if (str_contains($ip, '%')) {
[$ipv6, $zoneId] = explode('%', rawurldecode($ip), 2) + [1 => ''];
return new self('['.$ipv6.'%25'.rawurlencode($zoneId).']');
}
$host = IPv4Converter::fromEnvironment()->toDecimal($ip);
if (null === $host) {
throw new SyntaxError(sprintf('`%s` is an invalid IP Host.', $ip));
}
return new self($host);
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
return match (true) {
$uri instanceof Rfc3986Uri => new self($uri->getRawHost()),
$uri instanceof WhatWgUrl => new self($uri->getAsciiHost()),
$uri instanceof Psr7UriInterface => new self(UriString::parse($uri)['host']),
default => new self($uri->getHost()),
};
}
/**
* Create a new instance from an Authority object.
*/
public static function fromAuthority(Stringable|string $authority): self
{
return match (true) {
$authority instanceof AuthorityInterface => new self($authority->getHost()),
default => new self(Authority::new($authority)->getHost()),
};
}
public function value(): ?string
{
return $this->host;
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function toAscii(): ?string
{
return $this->value();
}
public function toUnicode(): ?string
{
return match (true) {
null !== $this->ipVersion,
null === $this->host => $this->host,
default => IdnConverter::toUnicode($this->host)->domain(),
};
}
public function encoded(): ?string
{
return match (true) {
null !== $this->ipVersion,
null === $this->host => $this->host,
default => (string) preg_replace_callback(
'/%[0-9A-F]{2}/i',
fn (array $matches) => strtoupper($matches[0]),
strtolower(rawurlencode(IdnConverter::toUnicode($this->host)->domain()))
),
};
}
public function getIpVersion(): ?string
{
return $this->ipVersion;
}
public function getIp(): ?string
{
if (null === $this->ipVersion) {
return null;
}
if ('4' === $this->ipVersion) {
return $this->host;
}
$ip = substr((string) $this->host, 1, -1);
if ('6' !== $this->ipVersion) {
return substr($ip, (int) strpos($ip, '.') + 1);
}
$pos = strpos($ip, '%');
if (false === $pos) {
return $ip;
}
return substr($ip, 0, $pos).'%'.rawurldecode(substr($ip, $pos + 3));
}
public function isRegisteredName(): bool
{
return !$this->isIp();
}
public function isDomain(): bool
{
return $this->isDomain;
}
public function isIp(): bool
{
return null !== $this->ipVersion;
}
public function isIpv4(): bool
{
return '4' === $this->ipVersion;
}
public function isIpv6(): bool
{
return '6' === $this->ipVersion;
}
public function isIpFuture(): bool
{
return !in_array($this->ipVersion, [null, '4', '6'], true);
}
public function hasZoneIdentifier(): bool
{
return $this->hasZoneIdentifier;
}
public function withoutZoneIdentifier(): IpHostInterface
{
if (!$this->hasZoneIdentifier) {
return $this;
}
[$ipv6] = explode('%', substr((string) $this->host, 1, -1));
return self::fromIp($ipv6);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Host::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Host::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string|null $host): self
{
return self::new($host);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Host::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from null.
*/
#[Deprecated(message:'use League\Uri\Components\Host::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromNull(): self
{
return self::new();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws MissingFeature If detecting IPv4 is not possible
* @throws SyntaxError If the $ip cannot be converted into a Host
* @deprecated Since version 7.0.0
* @see Host::fromIp()
*
* @codeCoverageIgnore
*
* Returns a host from an IP address.
*
*/
#[Deprecated(message:'use League\Uri\Components\Host::fromIp() instead', since:'league/uri-components:7.0.0')]
public static function createFromIp(string $ip, string $version = '', ?IPv4Normalizer $normalizer = null): self
{
return self::fromIp($ip, $version);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Host::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Host::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Host::fromAuthority()
*
* @codeCoverageIgnore
*
* Create a new instance from an Authority object.
*/
#[Deprecated(message:'use League\Uri\Components\Host::fromAuthority() instead', since:'league/uri-components:7.0.0')]
public static function createFromAuthority(Stringable|string $authority): self
{
return self::fromAuthority($authority);
}
}
@@ -0,0 +1,205 @@
<?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\Components;
use Deprecated;
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Throwable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_string;
use function substr;
final class Path extends Component implements PathInterface
{
private const SEPARATOR = '/';
private readonly string $path;
/**
* New instance.
*/
private function __construct(Stringable|string $path)
{
$this->path = $this->validate($path);
}
/**
* Validate the component content.
*/
private function validate(Stringable|string $path): string
{
return (string) $this->validateComponent($path);
}
/**
* Returns a new instance from a string or a stringable object.
*/
public static function new(Stringable|string $value = ''): self
{
return new self($value);
}
/**
* Create a new instance from a string or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string $uri = ''): ?self
{
try {
return self::new($uri);
} catch (Throwable) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatwgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
if ($uri instanceof Rfc3986Uri) {
return self::new($uri->getRawPath());
}
if ($uri instanceof WhatwgUrl) {
return self::new($uri->getPath());
}
$path = $uri->getPath();
$authority = $uri->getAuthority();
return match (true) {
null === $authority, '' === $authority, '' === $path, '/' === $path[0] => new self($path),
default => new self('/'.$path),
};
}
public function value(): string
{
return Encoder::encodePath($this->path);
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value)) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function decoded(): string
{
return $this->path;
}
public function normalize(): self
{
return new self((string) Encoder::normalizePath($this->withoutDotSegments()));
}
public function isAbsolute(): bool
{
return self::SEPARATOR === ($this->path[0] ?? '');
}
public function hasTrailingSlash(): bool
{
return '' !== $this->path && self::SEPARATOR === substr($this->path, -1);
}
public function withoutDotSegments(): PathInterface
{
$current = $this->toString();
$new = UriString::removeDotSegments($current);
if ($current === $new) {
return $this;
}
return new self($new);
}
/**
* Returns an instance with a trailing slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component with a trailing slash
*/
public function withTrailingSlash(): PathInterface
{
return $this->hasTrailingSlash() ? $this : new self($this->toString().self::SEPARATOR);
}
public function withoutTrailingSlash(): PathInterface
{
return !$this->hasTrailingSlash() ? $this : new self(substr($this->toString(), 0, -1));
}
public function withLeadingSlash(): PathInterface
{
return $this->isAbsolute() ? $this : new self(self::SEPARATOR.$this->toString());
}
public function withoutLeadingSlash(): PathInterface
{
return !$this->isAbsolute() ? $this : new self(substr($this->toString(), 1));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string|int $path): self
{
return self::new((string) $path);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,201 @@
<?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\Components;
use Deprecated;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\PortInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriScheme;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function filter_var;
use function is_string;
use const FILTER_VALIDATE_INT;
final class Port extends Component implements PortInterface
{
private readonly ?int $port;
private ?array $cachedDefaultSchemes = null;
/**
* New instance.
*/
private function __construct(Stringable|string|int|null $port = null)
{
$this->port = $this->validate($port);
}
public static function new(Stringable|string|int|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
return new self(self::filterUri($uri)->getPort());
}
/**
* Create a new instance from an Authority object.
*/
public static function fromAuthority(Stringable|string $authority): self
{
return match (true) {
$authority instanceof AuthorityInterface => new self($authority->getPort()),
default => new self(Authority::new($authority)->getPort()),
};
}
/**
* Validate a port.
*
* @throws SyntaxError if the port is invalid
*/
private function validate(Stringable|int|string|null $port): ?int
{
$port = self::filterComponent($port);
if (null === $port) {
return null;
}
$fport = filter_var($port, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]);
if (false !== $fport) {
return $fport;
}
throw new SyntaxError('Expected port to be a positive integer or 0; received '.$port.'.');
}
public function value(): ?string
{
return match (null) {
$this->port => $this->port,
default => (string) $this->port,
};
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function getUriComponent(): string
{
return match (null) {
$this->port => '',
default => ':'.$this->value(),
};
}
public function toInt(): ?int
{
return $this->port;
}
public function defaultScheme(): ?Scheme
{
return $this->defaultSchemes()[0] ?? null;
}
/**
* @return list<Scheme>
*/
public function defaultSchemes(): array
{
return $this->cachedDefaultSchemes ??= array_map(
fn (UriScheme $schemePort): Scheme => Scheme::new($schemePort->value),
UriScheme::fromPort($this->port)
);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Port::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Port::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Port::fromAuthority()
*
* @codeCoverageIgnore
*
* Create a new instance from an Authority object.
*/
#[Deprecated(message:'use League\Uri\Components\Port::fromAuthority() instead', since:'league/uri-components:7.0.0')]
public static function createFromAuthority(AuthorityInterface|Stringable|string $authority): self
{
return self::fromAuthority($authority);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Port::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Port::new() instead', since:'league/uri-components:7.0.0')]
public static function fromInt(int $port): self
{
return self::new($port);
}
}
@@ -0,0 +1,868 @@
<?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\Components;
use Deprecated;
use Iterator;
use League\Uri\Contracts\QueryInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\KeyValuePair\Converter;
use League\Uri\QueryString;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Traversable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use ValueError;
use function array_column;
use function array_count_values;
use function array_filter;
use function array_flip;
use function array_intersect;
use function array_is_list;
use function array_map;
use function array_merge;
use function count;
use function get_object_vars;
use function http_build_query;
use function implode;
use function in_array;
use function is_int;
use function is_object;
use function is_string;
use function preg_match;
use function preg_quote;
use function preg_replace;
use const JSON_PRESERVE_ZERO_FRACTION;
use const PREG_SPLIT_NO_EMPTY;
final class Query extends Component implements QueryInterface
{
private const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
/** @var array<int, array{0:string, 1:string|null}> */
private readonly array $pairs;
/** @var non-empty-string */
private readonly string $separator;
private readonly array $parameters;
/**
* Returns a new instance.
*/
private function __construct(Stringable|string|null $query, ?Converter $converter = null)
{
$converter ??= Converter::fromRFC3986();
$this->pairs = QueryString::parseFromValue($query, $converter);
$this->parameters = QueryString::extractFromValue($query, $converter);
$this->separator = $converter->separator();
}
public static function new(Stringable|string|null $value = null): self
{
return self::fromRFC3986($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Returns a new instance from the input of http_build_query.
*
* @param non-empty-string $separator
*/
public static function fromVariable(object|array $parameters, string $separator = '&', string $prefix = ''): self
{
$params = is_object($parameters) ? get_object_vars($parameters) : $parameters;
$data = [];
foreach ($params as $name => $value) {
$data[$prefix.$name] = $value;
}
return new self(http_build_query(data: $data, arg_separator: $separator), Converter::fromRFC1738($separator));
}
/**
* Returns a new instance from the result of QueryString::parse.
*
* @param iterable<int, array{0:string, 1:string|null}> $pairs
* @param non-empty-string $separator
*/
public static function fromPairs(iterable $pairs, string $separator = '&', string $prefix = ''): self
{
$data = [];
foreach ($pairs as $pair) {
if (!is_array($pair) || !array_is_list($pair) || 2 !== count($pair)) {
throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.');
}
$data[] = [$prefix.$pair[0], $pair[1]];
}
$converter = Converter::fromRFC3986($separator);
return new self(QueryString::buildFromPairs($data, $converter), $converter);
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
return match (true) {
$uri instanceof Rfc3986Uri => new self($uri->getRawQuery()),
$uri instanceof Psr7UriInterface => new self(UriString::parse($uri)['query']),
default => new self($uri->getQuery()),
};
}
/**
* Returns a new instance.
*
* @param non-empty-string $separator
*/
public static function fromRFC3986(Stringable|string|null $query = null, string $separator = '&'): self
{
return new self($query, Converter::fromRFC3986($separator));
}
/**
* Returns a new instance.
*
* @param non-empty-string $separator
*/
public static function fromRFC1738(Stringable|string|null $query = null, string $separator = '&'): self
{
return new self($query, Converter::fromRFC1738($separator));
}
/**
* Returns a new instance.
*
* @param non-empty-string $separator
*/
public static function fromFormData(Stringable|string|null $query = null, string $separator = '&'): self
{
return new self($query, Converter::fromFormData($separator));
}
public function getSeparator(): string
{
return $this->separator;
}
public function toRFC3986(): ?string
{
return QueryString::buildFromPairs($this->pairs, Converter::fromRFC3986($this->separator));
}
public function toRFC1738(): ?string
{
return QueryString::buildFromPairs($this->pairs, Converter::fromRFC1738($this->separator));
}
public function toFormData(): ?string
{
return QueryString::buildFromPairs($this->pairs, Converter::fromFormData($this->separator));
}
public function decoded(): ?string
{
return Converter::new($this->separator)->toValue($this);
}
public function normalize(): self
{
return self::new(Encoder::normalizeQuery($this->value()));
}
public function value(): ?string
{
return $this->toRFC3986();
}
public function getUriComponent(): string
{
return match ([]) {
$this->pairs => '',
default => '?'.$this->value(),
};
}
public function jsonSerialize(): ?string
{
return $this->toFormData();
}
public function count(): int
{
return count($this->pairs);
}
public function getIterator(): Iterator
{
yield from $this->pairs;
}
public function pairs(): iterable
{
foreach ($this->pairs as $pair) {
yield $pair[0] => $pair[1];
}
}
public function has(string ...$keys): bool
{
foreach ($keys as $key) {
if (!isset(array_flip(array_column($this->pairs, 0))[$key])) {
return false;
}
}
return [] !== $keys;
}
public function hasPair(string $key, ?string $value): bool
{
return in_array([$key, $value], $this->pairs, true);
}
public function get(string $key): ?string
{
foreach ($this->pairs as $pair) {
if ($key === $pair[0]) {
return $pair[1];
}
}
return null;
}
public function getAll(string $key): array
{
return array_column(array_filter($this->pairs, fn (array $pair): bool => $key === $pair[0]), 1);
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function parameters(): array
{
return $this->parameters;
}
public function parameter(string $name): mixed
{
return $this->parameters[$name] ?? null;
}
public function hasParameter(string ...$names): bool
{
foreach ($names as $name) {
if (!isset($this->parameters[$name])) {
return false;
}
}
return [] !== $names;
}
public function mergeParameters(object|array $parameter, string $prefix = ''): self
{
$params = is_object($parameter) ? get_object_vars($parameter) : $parameter;
$data = [];
foreach ($params as $name => $value) {
$data[$prefix.$name] = $value;
}
return in_array($data, [$this->parameters, []], true) ? $this : new self(
http_build_query(data: array_merge($this->parameters, $data), arg_separator: $this->separator),
Converter::fromRFC1738($this->separator)
);
}
public function replaceParameter(string $name, mixed $parameter): self
{
$this->hasParameter($name) || throw new ValueError('The specified name does not exist');
if ($parameter === $this->parameters[$name]) {
return $this;
}
$parameters = $this->parameters;
$parameters[$name] = $parameter;
return new self(http_build_query(data: $parameters, arg_separator: $this->separator), Converter::fromRFC1738($this->separator));
}
public function withSeparator(string $separator): self
{
return match ($separator) {
$this->separator => $this,
'' => throw new SyntaxError('The separator character cannot be the empty string.'),
default => self::fromPairs($this->pairs, $separator),
};
}
public function sort(): self
{
$codepoints = fn (?string $str): string => in_array($str, ['', null], true) ? '' : implode('.', array_map(
mb_ord(...), /* @phpstan-ignore-line */
(array) preg_split(pattern:'//u', subject: $str, flags: PREG_SPLIT_NO_EMPTY)
));
$compare = fn (string $name1, string $name2): int => match (1) {
preg_match(self::REGEXP_NON_ASCII_PATTERN, $name1.$name2) => strcmp($codepoints($name1), $codepoints($name2)),
default => strcmp($name1, $name2),
};
$parameters = array_reduce($this->pairs, function (array $carry, array $pair) {
$carry[$pair[0]] ??= [];
$carry[$pair[0]][] = $pair[1];
return $carry;
}, []);
uksort($parameters, $compare);
$pairs = [];
foreach ($parameters as $key => $values) {
$pairs = [...$pairs, ...array_map(fn ($value) => [$key, $value], $values)];
}
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs),
};
}
public function withoutDuplicates(): self
{
if (count($this->pairs) === count(array_count_values(array_column($this->pairs, 0)))) {
return $this;
}
$pairs = array_reduce($this->pairs, $this->removeDuplicates(...), []);
if ($pairs === $this->pairs) {
return $this;
}
return self::fromPairs($pairs, $this->separator);
}
/**
* @template TInitial
*
* @param callable(TInitial|null, array{0:array-key, 1:mixed}, array-key=): TInitial $callback
* @param TInitial|null $initial
*
* @return TInitial|null
*/
public function reduce(callable $callback, mixed $initial = null): mixed
{
foreach ($this->pairs as $offset => $pair) {
$initial = $callback($initial, $pair, $offset);
}
return $initial;
}
/**
* Adds a query pair only if it is not already present in a given array.
*/
private function removeDuplicates(array $pairs, array $pair): array
{
return match (true) {
in_array($pair, $pairs, true) => $pairs,
default => [...$pairs, $pair],
};
}
public function withoutEmptyPairs(): self
{
$pairs = array_filter($this->pairs, $this->filterEmptyPair(...));
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs),
};
}
/**
* Empty Pair filtering.
*/
private function filterEmptyPair(array $pair): bool
{
return '' !== $pair[0] && null !== $pair[1] && '' !== $pair[1];
}
public function withoutNumericIndices(): self
{
$pairs = array_map($this->encodeNumericIndices(...), $this->pairs);
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs, $this->separator),
};
}
/**
* Remove numeric indices from pairs.
*
* @param array{0:string, 1:string|null} $pair
*
* @return array{0:string, 1:string|null}
*/
private function encodeNumericIndices(array $pair): array
{
static $regexp = ',\[\d+],';
$pair[0] = (string) preg_replace($regexp, '[]', $pair[0]);
return $pair;
}
public function withPair(string $key, Stringable|string|int|float|bool|null $value): QueryInterface
{
$pairs = $this->addPair($this->pairs, [$key, $this->filterPair($value)]);
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs, $this->separator),
};
}
/**
* Add a new pair to the query key/value list.
*
* If there are any key/value pair whose kay is kay, in the list,
* set the value of the first such key/value pair to value and remove the others.
* Otherwise, append a new key/value pair whose key is key and value is value, to the list.
*/
private function addPair(array $list, array $pair): array
{
$found = false;
$reducer = static function (array $pairs, array $srcPair) use ($pair, &$found): array {
if ($pair[0] !== $srcPair[0]) {
$pairs[] = $srcPair;
return $pairs;
}
if (!$found) {
$pairs[] = $pair;
$found = true;
return $pairs;
}
return $pairs;
};
$pairs = array_reduce($list, $reducer, []);
if (!$found) {
$pairs[] = $pair;
}
return $pairs;
}
public function merge(Stringable|string|null $query): QueryInterface
{
$pairs = $this->pairs;
foreach (QueryString::parse(self::filterComponent($query), $this->separator) as $pair) {
$pairs = $this->addPair($pairs, $pair);
}
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs, $this->separator),
};
}
/**
* Validate the given pair.
*
* To be valid, the pair must be the null value, a scalar or a collection of scalar and null values.
*/
private function filterPair(Stringable|string|int|float|bool|null $value): ?string
{
return match (true) {
$value instanceof UriComponentInterface => $value->value(),
null === $value => null,
true === $value => 'true',
false === $value => 'false',
is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
default => (string) $value,
};
}
public function withoutPairByKey(string ...$keys): QueryInterface
{
if ([] === $keys) {
return $this;
}
$keysToRemove = array_intersect($keys, array_column($this->pairs, 0));
return match ([]) {
$keysToRemove => $this,
default => self::fromPairs(
array_filter($this->pairs, static fn (array $pair): bool => !in_array($pair[0], $keysToRemove, true)),
$this->separator
),
};
}
public function withoutPairByValue(Stringable|string|int|float|bool|null ...$values): self
{
if ([] === $values) {
return $this;
}
$values = array_map($this->filterPair(...), $values);
$newPairs = array_filter($this->pairs, fn (array $pair) => !in_array($pair[1], $values, true));
return match ($this->pairs) {
$newPairs => $this,
default => self::fromPairs($newPairs, $this->separator),
};
}
public function withoutPairByKeyValue(string $key, Stringable|string|int|float|bool|null $value): self
{
$pair = [$key, $this->filterPair($value)];
$newPairs = array_filter($this->pairs, fn (array $currentPair) => $currentPair !== $pair);
return match ($this->pairs) {
$newPairs => $this,
default => self::fromPairs($newPairs, $this->separator),
};
}
public function appendTo(string $key, Stringable|string|int|float|bool|null $value): QueryInterface
{
return self::fromPairs([...$this->pairs, [$key, $this->filterPair($value)]], $this->separator);
}
public function append(Stringable|string|null $query): QueryInterface
{
if ($query instanceof UriComponentInterface) {
$query = $query->value();
}
$pairs = array_merge($this->pairs, QueryString::parse($query, $this->separator));
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs(array_filter($pairs, $this->filterEmptyValue(...)), $this->separator),
};
}
public function prepend(Stringable|string|null $query): QueryInterface
{
return Query::new($query)->append($this);
}
/**
* Replace a pair based on its offset.
*/
public function replace(int $offset, string $key, Stringable|string|int|float|bool|null $value): QueryInterface
{
$index = $offset < 0 ? count($this->pairs) + $offset : $offset;
$pair = $this->pairs[$index] ?? [];
[] !== $pair || throw new ValueError('The given offset "'.$offset.'" does not exist');
$newPair = [$key, $this->filterPair($value)];
if ($pair === $newPair) {
return $this;
}
$newPairs = $this->pairs;
$newPairs[$index] = $newPair;
return self::fromPairs($newPairs, $this->separator);
}
/**
* Returns the offset of the pair based on its key and its nth occurrence.
*
* negative occurrences are supported
*/
public function indexOf(string $key, int $nth = 0): ?int
{
if ([] === $this->pairs) {
return null;
}
if ($nth < 0) {
$matchCount = 0;
for ($offset = count($this->pairs) - 1; $offset >= 0; --$offset) {
if ($this->pairs[$offset][0] === $key) {
if (++$matchCount === -$nth) {
return $offset;
}
}
}
return null;
}
$matchCount = 0;
foreach ($this->pairs as $offset => $pair) {
if ($pair[0] === $key) {
if ($nth === $matchCount) {
return $offset;
}
++$matchCount;
}
}
return null;
}
/**
* Empty Pair filtering.
*/
private function filterEmptyValue(array $pair): bool
{
return '' !== $pair[0] || null !== $pair[1];
}
public function withoutParameters(string ...$names): QueryInterface
{
if ([] === $names) {
return $this;
}
$mapper = static fn (string $offset): string => preg_quote($offset, ',').'(\[.*\].*)?';
$regexp = ',^('.implode('|', array_map($mapper, $names)).')?$,';
$filter = fn (array $pair): bool => 1 !== preg_match($regexp, $pair[0]);
$pairs = array_filter($this->pairs, $filter);
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs, $this->separator),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromParameters()
*
* @codeCoverageIgnore
*
* @param non-empty-string $separator
*
* Returns a new instance from the result of PHP's parse_str.
*
* @deprecated Since version 7.0.0
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromVariables() instead', since:'league/uri-components:7.0.0')]
public static function createFromParams(iterable|object $params, string $separator = '&'): self
{
return self::fromParameters($params, $separator);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromPairs()
*
* @codeCoverageIgnore
*
*
* Returns a new instance from the result of QueryString::parse.
*
* @param iterable<int, array{0:string, 1:string|null}> $pairs
* @param non-empty-string $separator
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromPairs() instead', since:'league/uri-components:7.0.0')]
public static function createFromPairs(iterable $pairs, string $separator = '&'): self
{
return self::fromPairs($pairs, $separator);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromRFC3986()
*
* @codeCoverageIgnore
*
* Returns a new instance.
*
* @param non-empty-string $separator
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromRFC3986() instead', since:'league/uri-components:7.0.0')]
public static function createFromRFC3986(Stringable|string|int|null $query = '', string $separator = '&'): self
{
if (null !== $query) {
$query = (string) $query;
}
return self::fromRFC3986($query, $separator);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromRFC1738()
*
* @codeCoverageIgnore
*
* Returns a new instance.
*
* @param non-empty-string $separator
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromRFC1738() instead', since:'league/uri-components:7.0.0')]
public static function createFromRFC1738(Stringable|string|int|null $query = '', string $separator = '&'): self
{
if (is_int($query)) {
$query = (string) $query;
}
return self::fromRFC1738($query, $separator);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::parameters()
* @see Query::parameter()
*
* @codeCoverageIgnore
*
* Returns the query as a collection of PHP variables or a single variable assign to a specific key
*/
#[Deprecated(message:'use League\Uri\Components\Query::parameter() or League\Uri\Components\Query::parameters() instead', since:'league/uri-components:7.0.0')]
public function params(?string $key = null): mixed
{
return match (null) {
$key => $this->parameters(),
default => $this->parameter($key),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::withoutParameters()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Query::withoutParameters() instead', since:'league/uri-components:7.0.0')]
public function withoutParams(string ...$names): QueryInterface
{
return $this->withoutParameters(...$names);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.3.0
* @see Query::withoutPairByKey()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Query::withoutPairByKey() instead', since:'league/uri-components:7.3.0')]
public function withoutPair(string ...$keys): QueryInterface
{
return $this->withoutPairByKey(...$keys);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @param non-empty-string $separator
*
* @see Query::fromVariable()
*
* @codeCoverageIgnore
* Returns a new instance from the result of PHP's parse_str.
*
* @deprecated Since version 7.0.0
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromVariable() instead', since:'league/uri-components:7.0.0')]
public static function fromParameters(object|array $parameters, string $separator = '&'): self
{
if ($parameters instanceof QueryInterface) {
return self::fromPairs($parameters, $separator);
}
$parameters = match (true) {
$parameters instanceof Traversable => iterator_to_array($parameters),
default => $parameters,
};
$query = match ([]) {
$parameters => null,
default => http_build_query(data: $parameters, arg_separator: $separator),
};
return new self($query, Converter::fromRFC1738($separator));
}
}
@@ -0,0 +1,209 @@
<?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\Components;
use Deprecated;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\SchemeType;
use League\Uri\UriScheme;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Throwable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function in_array;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;
final class Scheme extends Component
{
private const REGEXP_SCHEME = ',^[a-z]([-a-z0-9+.]+)?$,i';
private readonly ?string $scheme;
private readonly ?UriScheme $uriScheme;
private function __construct(Stringable|string|null $scheme)
{
$this->scheme = $this->validate($scheme);
$this->uriScheme = UriScheme::tryFrom((string) $this->scheme);
}
public function isWebsocket(): bool
{
return in_array($this->scheme, ['ws', 'wss'], true);
}
public function isHttp(): bool
{
return in_array($this->scheme, ['http', 'https'], true);
}
public function isSsl(): bool
{
return in_array($this->scheme, ['https', 'wss'], true);
}
public function isSpecial(): bool
{
return $this->isWhatWgSpecial() || in_array($this->scheme, ['data', 'file'], true);
}
public function isWhatWgSpecial(): bool
{
return $this->uriScheme?->isWhatWgSpecial() ?? false;
}
public function defaultPort(): Port
{
return Port::new($this->uriScheme?->port());
}
public function hasDefaultPort(): bool
{
static $emptyPort = null;
$emptyPort ??= Port::new();
return !$emptyPort->equals($this->defaultPort());
}
public function type(): SchemeType
{
return $this->uriScheme?->type() ?? SchemeType::Unknown;
}
/**
* Validate a scheme.
*
* @throws SyntaxError if the scheme is invalid
*/
private function validate(Stringable|string|null $scheme): ?string
{
$scheme = self::filterComponent($scheme);
if (null === $scheme) {
return null;
}
$fScheme = strtolower($scheme);
/** @var array<string> $inMemoryCache */
static $inMemoryCache = [];
if (isset($inMemoryCache[$fScheme])) {
return $fScheme;
}
if (1 !== preg_match(self::REGEXP_SCHEME, $fScheme)) {
throw new SyntaxError(sprintf("The scheme '%s' is invalid.", $scheme));
}
if (100 < count($inMemoryCache)) {
unset($inMemoryCache[array_key_first($inMemoryCache)]);
}
$inMemoryCache[$fScheme] = 1;
return $fScheme;
}
public static function new(Stringable|string|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (Throwable) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
return new self(
$uri instanceof Psr7UriInterface
? UriString::parse($uri)['scheme']
: $uri->getScheme()
);
}
public function value(): ?string
{
return $this->scheme;
}
public function getUriComponent(): string
{
return $this->value().(null === $this->scheme ? '' : ':');
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Scheme::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Scheme::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $scheme): self
{
return self::new($scheme);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Scheme::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Scheme::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,553 @@
<?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\Components;
use ArgumentCountError;
use Closure;
use Countable;
use Deprecated;
use Iterator;
use IteratorAggregate;
use League\Uri\Contracts\QueryInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\KeyValuePair\Converter;
use League\Uri\QueryString;
use League\Uri\Uri;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_is_list;
use function array_key_exists;
use function array_keys;
use function array_map;
use function count;
use function func_get_arg;
use function func_num_args;
use function get_object_vars;
use function is_array;
use function is_bool;
use function is_iterable;
use function is_object;
use function is_scalar;
use function iterator_to_array;
use function json_encode;
use function spl_object_hash;
use function str_starts_with;
use const JSON_PRESERVE_ZERO_FRACTION;
/**
* @see https://url.spec.whatwg.org/#interface-urlsearchparams
*
* @implements IteratorAggregate<array{0:string, 1:string}>
*/
final class URLSearchParams implements Countable, IteratorAggregate, UriComponentInterface
{
private QueryInterface $pairs;
/**
* New instance.
*
* A string, which will be parsed from application/x-www-form-urlencoded format. A leading '?' character is ignored.
* A literal sequence of name-value string pairs, or any object with an iterator that produces a sequence of string pairs.
* A record of string keys and string values. Note that nesting is not supported.
*/
public function __construct(object|array|string|null $init = '')
{
$pairs = self::filterPairs(match (true) {
$init instanceof self,
$init instanceof QueryInterface => $init,
$init instanceof UriComponentInterface => self::parsePairs($init->value()),
is_iterable($init) => self::formatIterable($init),
$init instanceof Stringable, !is_object($init) => self::parsePairs(self::formatQuery($init)),
default => self::yieldPairs($init),
});
$this->pairs = Query::fromPairs($pairs);
}
/**
* @return array<int, array{0:string, 1:string|null}>
*/
private static function parsePairs(string|null $query): array
{
return QueryString::parseFromValue($query, Converter::fromFormData());
}
/**
* @return iterable<array{0:string, 1:string}>
*/
private static function formatIterable(iterable $iterable): iterable
{
if (!is_array($iterable)) {
$iterable = iterator_to_array($iterable);
}
return match (true) {
array_is_list($iterable) => $iterable,
default => self::yieldPairs($iterable)
};
}
/**
* Generates an Iterator containing pairs as items from an object or array.
*
* If an iterable is given, foreach will loop over the iterable structure
* If an object is give, foreach will loop over the object public properties if they are defined
*
* @param object|iterable<array-key, Stringable|string|float|int|bool|null> $associative
*
* @return Iterator<int, array{0:string, 1:string}>
*/
private static function yieldPairs(object|array $associative): Iterator
{
foreach ($associative as $key => $value) { /* @phpstan-ignore-line */
yield [self::uvString($key), self::uvString($value)];
}
}
/**
* @return Iterator<int, array{0:string, 1:string}>
*/
private static function filterPairs(iterable $pairs): iterable
{
$filter = static fn ($pair): ?array => match (true) {
!is_array($pair),
[0, 1] !== array_keys($pair) => throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.'),
null !== $pair[1] => [self::uvString($pair[0]), self::uvString($pair[1])],
'' !== $pair[0] => [self::uvString($pair[0]), ''],
default => null,
};
foreach ($pairs as $pair) {
if (null !== ($filteredPair = $filter($pair))) {
yield $filteredPair;
}
}
}
private static function formatQuery(Stringable|string|null $query): string
{
return match (true) {
null === $query => '',
str_starts_with((string) $query, '?') => substr((string) $query, 1),
default => (string) $query,
};
}
/**
* Normalizes type to UVString.
*
* @see https://webidl.spec.whatwg.org/#idl-USVString
*/
private static function uvString(Stringable|string|float|int|bool|null $value): string
{
return match (true) {
null === $value => 'null',
false === $value => 'false',
true === $value => 'true',
is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
default => (string) $value,
};
}
/**
* Returns a new instance from a string or a stringable object.
*
* The input will be parsed from application/x-www-form-urlencoded format.
* The leading '?' character if present is ignored.
*/
public static function new(Stringable|string|null $query = null): self
{
return new self(Query::fromFormData(self::formatQuery($query)));
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Returns a new instance from a literal sequence of name-value string pairs,
* or any object with an iterator that produces a sequence of string pairs.
*
* @param iterable<int, array{0:string, 1:string|null}> $pairs
*/
public static function fromPairs(iterable $pairs): self
{
return new self(Query::fromPairs($pairs));
}
/**
* Returns a new instance from a record of string keys and string values.
*
* A record can be, an iterable or any object with scalar or null public properties. Nesting is not supported.
*
* @param object|iterable<array-key, Stringable|string|float|int|bool|null> $associative
*/
public static function fromAssociative(object|array $associative): self
{
return new self(Query::fromPairs(self::yieldPairs($associative)));
}
/**
* Returns a new instance from a URI.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$query = match (true) {
$uri instanceof Rfc3986Uri => $uri->getRawQuery(),
$uri instanceof WhatWgUrl, $uri instanceof UriInterface => $uri->getQuery(),
$uri instanceof Psr7UriInterface => UriString::parse($uri)['query'],
default => Uri::new($uri)->getQuery(),
};
return new self(Query::fromPairs(QueryString::parseFromValue($query, Converter::fromFormData())));
}
/**
* Returns a new instance from the input of PHP's http_build_query.
*/
public static function fromVariable(object|array $parameters): self
{
return self::fromPairs(self::parametersToPairs($parameters));
}
private static function parametersToPairs(array|object $data, string|int $prefix = '', array &$recursive = []): array
{
$yieldParameters = static fn (object|array $data): array => is_array($data) ? $data : get_object_vars($data);
$pairs = [];
foreach ($yieldParameters($data) as $name => $value) {
if (is_object($data)) {
$id = spl_object_hash($data);
if (!array_key_exists($id, $recursive)) {
$recursive[$id] = 1;
}
}
if (is_object($value)) {
$id = spl_object_hash($value);
if (array_key_exists($id, $recursive)) {
return [];
}
$recursive[$id] = 1;
}
if ('' !== $prefix) {
$name = $prefix.'['.$name.']';
}
$pairs = match (true) {
is_array($value),
is_object($value) => [...$pairs, ...self::parametersToPairs($value, $name, $recursive)],
is_scalar($value) => [...$pairs, [$name, self::uvString($value)]],
default => $pairs,
};
}
return $pairs;
}
public function value(): ?string
{
return $this->pairs->toFormData();
}
public function equals(mixed $value): bool
{
return $this->pairs->equals($value);
}
/**
* Returns a query string suitable for use in a URL.
*/
public function toString(): string
{
return $this->value() ?? '';
}
public function decoded(): string
{
return (string) Query::fromPairs($this->pairs)->decoded();
}
public function __toString(): string
{
return $this->toString();
}
public function jsonSerialize(): string
{
return $this->toString();
}
public function getUriComponent(): string
{
$value = $this->value() ?? '';
return match ('') {
$value => $value,
default => '?'.$value,
};
}
/**
* Returns an iterator allowing iteration through all keys contained in this object.
*
* @return iterable<string>
*/
public function keys(): iterable
{
foreach ($this->pairs as [$key, $__]) {
yield $key;
}
}
/**
* Returns an iterator allowing iteration through all values contained in this object.
*
* @return iterable<string>
*/
public function values(): iterable
{
foreach ($this->pairs as [$__, $value]) {
yield $value ?? '';
}
}
/**
* Tells whether the specified parameter is in the search parameters.
*
* The method requires at least one parameter as the pair name (string or null)
* and an optional second and last parameter as the pair value (Stringable|string|float|int|bool|null)
* <code>
* $params = new URLSearchParams('a=b&c);
* $params->has('c'); // return true
* $params->has('a', 'b'); // return true
* $params->has('a', 'c'); // return false
* </code>
*/
public function has(?string $name): bool
{
$name = self::uvString($name);
return match (func_num_args()) {
1 => $this->pairs->has($name),
2 => $this->pairs->hasPair($name, self::uvString(func_get_arg(1))), /* @phpstan-ignore-line */
default => throw new ArgumentCountError(__METHOD__.' requires at least one argument as the pair name and a second optional argument as the pair value.'),
};
}
/**
* Returns the first value associated to the given search parameter or null if none exists.
*/
public function get(?string $name): ?string
{
return match (true) {
$this->has($name) => $this->pairs->get(self::uvString($name)) ?? '',
default => null,
};
}
/**
* Returns all the values associated with a given search parameter as an array.
*
* @return array<string>
*/
public function getAll(?string $name): array
{
return array_map(
fn (?string $value): string => $value ?? '',
$this->pairs->getAll(self::uvString($name))
);
}
/**
* Tells whether the instance has some parameters.
*/
public function isNotEmpty(): bool
{
return ! $this->isEmpty();
}
/**
* Tells whether the instance has no parameters.
*/
public function isEmpty(): bool
{
return 0 === $this->size();
}
/**
* Returns the total number of distinct search parameter keys.
*/
public function uniqueKeyCount(): int
{
return count(
array_count_values(
array_column([...$this->pairs], 0)
)
);
}
/**
* Returns the total number of search parameter entries.
*/
public function size(): int
{
return count($this->pairs);
}
/**
* @see URLSearchParams::size()
*/
public function count(): int
{
return $this->size();
}
/**
* Allowing iteration through all key/value pairs contained in this object.
*
* The iterator returns key/value pairs in the same order as they appear in the query string.
* The key and value of each pair are string objects.
*/
public function entries(): Iterator
{
yield from $this->pairs;
}
/**
* @see URLSearchParams::entries()
*/
public function getIterator(): Iterator
{
return $this->entries();
}
/**
* Allows iteration through all values contained in this object via a callback function.
*
* @param Closure(string $value, string $key): void $callback
*/
public function each(Closure $callback): void
{
foreach ($this->pairs->pairs() as $key => $value) {
$callback($value ?? '', $key);
}
}
private function updateQuery(QueryInterface $query): void
{
if ($query->value() !== $this->pairs->value()) {
$this->pairs = $query;
}
}
/**
* Sets the value associated with a given search parameter to the given value.
*
* If there were several matching values, this method deletes the others.
* If the search parameter doesn't exist, this method creates it.
*/
public function set(?string $name, Stringable|string|float|int|bool|null $value): void
{
$this->updateQuery($this->pairs->withPair(self::uvString($name), self::uvString($value)));
}
/**
* Appends a specified key/value pair as a new search parameter.
*/
public function append(?string $name, Stringable|string|float|int|bool|null $value): void
{
$this->updateQuery($this->pairs->appendTo(self::uvString($name), self::uvString($value)));
}
/**
* Deletes specified parameters and their associated value(s) from the list of all search parameters.
*
* The method requires at least one parameter as the pair name (string or null)
* and an optional second and last parameter as the pair value (Stringable|string|float|int|bool|null)
* <code>
* $params = new URLSearchParams('a=b&c);
* $params->delete('c'); //delete all parameters with the key 'c'
* $params->delete('a', 'b') //delete all pairs with the key 'a' and the value 'b'
* </code>
*/
public function delete(?string $name): void
{
$name = self::uvString($name);
$this->updateQuery(match (func_num_args()) {
1 => $this->pairs->withoutPairByKey($name),
2 => $this->pairs->withoutPairByKeyValue($name, self::uvString(func_get_arg(1))), /* @phpstan-ignore-line */
default => throw new ArgumentCountError(__METHOD__.' requires at least one argument as the pair name and a second optional argument as the pair value.'),
});
}
/**
* 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
* uses a stable sorting algorithm (i.e. the relative order between
* key/value pairs with equal keys will be preserved).
*/
public function sort(): void
{
$this->updateQuery($this->pairs->sort());
}
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) ?? $this,
null !== $onFail => $onFail($this) ?? $this,
default => $this,
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.4.0
* @see URLSearchParams::fromVariable()
*
* @codeCoverageIgnore
*
*/
#[Deprecated(message:'use League\Uri\Components\URLSearchParams::fromVariable() instead', since:'league/uri-components:7.4.0')]
public static function fromParameters(object|array $parameters): self
{
return new self(Query::fromParameters($parameters));
}
}
@@ -0,0 +1,256 @@
<?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\Components;
use Deprecated;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Contracts\UserInfoInterface;
use League\Uri\Encoder;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use SensitiveParameter;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function explode;
use function is_string;
final class UserInfo extends Component implements UserInfoInterface
{
private readonly ?string $username;
private readonly ?string $password;
/**
* New instance.
*/
public function __construct(
Stringable|string|null $username,
#[SensitiveParameter] Stringable|string|null $password = null,
) {
$this->username = $this->validateComponent($username);
$this->password = $this->validateComponent($password);
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(Rfc3986Uri|WhatWgUrl|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
if ($uri instanceof Rfc3986Uri) {
return new self($uri->getRawUsername(), $uri->getRawPassword());
}
if ($uri instanceof WhatWgUrl || $uri instanceof UriInterface) {
return new self($uri->getUsername(), $uri->getPassword());
}
$components = UriString::parse($uri);
return new self($components['user'], $components['pass']);
}
/**
* Create a new instance from an Authority object.
*/
public static function fromAuthority(Stringable|string|null $authority): self
{
return match (true) {
$authority instanceof AuthorityInterface => self::new($authority->getUserInfo()),
default => self::new(Authority::new($authority)->getUserInfo()),
};
}
/**
* Create a new instance from a hash of parse_url parts.
*
* Create a new instance from a hash representation of the URI similar
* to PHP parse_url function result
*
* @param array{user? : ?string, pass? : ?string} $components
*/
public static function fromComponents(array $components): self
{
$components += ['user' => null, 'pass' => null];
return match (null) {
$components['user'] => new self(null),
default => new self($components['user'], $components['pass']),
};
}
/**
* Creates a new instance from an encoded string.
*/
public static function new(Stringable|string|null $value = null): self
{
if ($value instanceof UriComponentInterface) {
$value = $value->value();
}
if (null === $value) {
return new self(null);
}
$value = (string) $value;
[$user, $pass] = explode(':', $value, 2) + [1 => null];
return new self(Encoder::decodeAll($user), Encoder::decodeAll($pass));
}
/**
* Create a new instance from a string or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
public function value(): ?string
{
return match (true) {
null === $this->password => $this->getUsername(),
default => $this->getUsername().':'.$this->getPassword(),
};
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function getUriComponent(): string
{
return $this->value().(null === $this->username ? '' : '@');
}
public function getUser(): ?string
{
return $this->username;
}
public function getPass(): ?string
{
return $this->password;
}
public function getUsername(): ?string
{
return Encoder::encodeUser($this->username);
}
public function getPassword(): ?string
{
return Encoder::encodePassword($this->password);
}
/**
* @return array{user: ?string, pass: ?string}
*/
public function components(): array
{
return [
'user' => $this->username,
'pass' => $this->password,
];
}
public function withUser(Stringable|string|null $username): self
{
$username = $this->validateComponent($username);
if ($this->username === $username) {
return $this;
}
return new self($username, $this->password);
}
public function withPass(#[SensitiveParameter] Stringable|string|null $password): self
{
$password = $this->validateComponent($password);
if ($password === $this->password) {
return $this;
}
return new self($this->username, $password);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see UserInfo::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\UserInfo::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Rfc3986Uri|WhatWgUrl|Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see UserInfo::fromAuthority()
*
* @codeCoverageIgnore
*
* Create a new instance from an Authority object.
*/
#[Deprecated(message:'use League\Uri\Components\UserInfo::fromAuthority() instead', since:'league/uri-components:7.0.0')]
public static function createFromAuthority(AuthorityInterface|Stringable|string $authority): self
{
return self::fromAuthority($authority);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see UserInfo::new()
*
* @codeCoverageIgnore
*
* Creates a new instance from an encoded string.
*/
#[Deprecated(message:'use League\Uri\Components\UserInfo::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $userInfo): self
{
return self::new($userInfo);
}
}
@@ -0,0 +1,195 @@
<?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 League\Uri\Components\Host;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\HostInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\IPv4\Calculator;
use League\Uri\IPv4\Converter;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Converter
*
* @codeCoverageIgnore
*/
final class IPv4Normalizer
{
private readonly Converter $converter;
public function __construct(
Converter|Calculator $calculator
) {
if (!$calculator instanceof Converter) {
$calculator = new Converter($calculator);
}
$this->converter = $calculator;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Converter::toDecimal()
*
* @codeCoverageIgnore
*
* Normalizes the host content to a IPv4 dot-decimal notation if possible
* otherwise returns the Host instance unchanged.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function normalize(Stringable|string|null $host): ?string
{
return $this->converter->toDecimal($host);
}
/**
* Returns an instance using a GMP calculator.
*/
public static function createFromGMP(): self
{
return new self(Converter::fromGMP());
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Converter::fromBCMath()
*
* @codeCoverageIgnore
*
* Returns an instance using a Bcmath calculator.
*/
public static function createFromBCMath(): self
{
return new self(Converter::fromBCMath());
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Converter::fromNative()
*
* @codeCoverageIgnore
*
* Returns an instance using a PHP native calculator (requires 64bits PHP).
*/
public static function createFromNative(): self
{
return new self(Converter::fromNative());
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws MissingFeature If no IPv4Calculator implementing object can be used on the platform
*
* @codeCoverageIgnore
* @see Converter::fromEnvironment()
*
* @codeCoverageIgnore
*
* Returns an instance using a detected calculator depending on the PHP environment.
*
* @deprecated Since version 7.0.0
*/
public static function createFromServer(): self
{
return new self(Converter::fromEnvironment());
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Modifier::hostToDecimal()
*
* @codeCoverageIgnore
*
* Normalizes the URI host content to a IPv4 dot-decimal notation if possible
* otherwise returns the uri instance unchanged.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function normalizeUri(UriInterface|Psr7UriInterface $uri): UriInterface|Psr7UriInterface
{
$host = $uri->getHost();
$decimalIPv4 = $this->converter->toDecimal($host);
return match (true) {
null === $decimalIPv4,
$decimalIPv4 === $host => $uri,
default => $uri->withHost($decimalIPv4),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Modifier::hostToDecimal()
*
* @codeCoverageIgnore
*
* Normalizes the authority host content to a IPv4 dot-decimal notation if possible
* otherwise returns the uri instance unchanged.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function normalizeAuthority(AuthorityInterface $authority): AuthorityInterface
{
$host = $authority->getHost();
$decimalIpv4 = $this->converter->toDecimal($host);
return match (true) {
null === $decimalIpv4,
$decimalIpv4 === $host => $authority,
default => $authority->withHost($decimalIpv4),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Modifier::hostToDecimal()
*
* @codeCoverageIgnore
*
* Normalizes the host content to a IPv4 dot-decimal notation if possible
* otherwise returns the Host instance unchanged.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function normalizeHost(HostInterface $host): HostInterface
{
$decimalIPv4 = $this->converter->toDecimal($host->value());
return match (null) {
$decimalIPv4 => $host,
default => Host::new($decimalIPv4),
};
}
}
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2015 ignace nyamagana butera
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,333 @@
<?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 League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
/**
* @deprecated since version 7.0.0
* @codeCoverageIgnore
* @see Modifier
*/
class UriModifier
{
/*********************************
* Query resolution methods
*********************************/
/**
* Add the new query data to the existing URI query.
*/
public static function appendQuery(
Psr7UriInterface|UriInterface $uri,
Stringable|string|null $query
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->appendQuery($query)->getUri();
}
/**
* Merge a new query with the existing URI query.
*/
public static function mergeQuery(
Psr7UriInterface|UriInterface $uri,
Stringable|string|null $query
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->mergeQuery($query)->getUri();
}
/**
* Remove query data according to their key name.
*/
public static function removePairs(Psr7UriInterface|UriInterface $uri, string ...$keys): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeQueryPairsByKey(...$keys)->getUri();
}
/**
* Remove empty pairs from the URL query component.
*
* A pair is considered empty if it's name is the empty string
* and its value is either the empty string or the null value
*/
public static function removeEmptyPairs(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeEmptyQueryPairs()->getUri();
}
/**
* Remove query data according to their key name.
*/
public static function removeParams(Psr7UriInterface|UriInterface $uri, string ...$keys): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeQueryParameters(...$keys)->getUri();
}
/**
* Sort the URI query by keys.
*/
public static function sortQuery(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->sortQuery()->getUri();
}
/*********************************
* Host resolution methods
*********************************/
/**
* Add the root label to the URI.
*/
public static function addRootLabel(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->addRootLabel()->getUri();
}
/**
* Append a label or a host to the current URI host.
*
* @throws SyntaxError If the host cannot be appended
*/
public static function appendLabel(Psr7UriInterface|UriInterface $uri, Stringable|string|null $label): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->appendLabel($label)->getUri();
}
/**
* Convert the URI host part to its ascii value.
*/
public static function hostToAscii(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->hostToAscii()->getUri();
}
/**
* Convert the URI host part to its unicode value.
*/
public static function hostToUnicode(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->hostToUnicode()->getUri();
}
/**
* Prepend a label or a host to the current URI host.
*
* @throws SyntaxError If the host cannot be prepended
*/
public static function prependLabel(Psr7UriInterface|UriInterface $uri, Stringable|string|null $label): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->prependLabel($label)->getUri();
}
/**
* Remove host labels according to their offset.
*/
public static function removeLabels(Psr7UriInterface|UriInterface $uri, int ...$keys): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeLabels(...$keys)->getUri();
}
/**
* Remove the root label to the URI.
*/
public static function removeRootLabel(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeRootLabel()->getUri();
}
/**
* Remove the host zone identifier.
*/
public static function removeZoneId(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeZoneId()->getUri();
}
/**
* Replace a label of the current URI host.
*/
public static function replaceLabel(
Psr7UriInterface|UriInterface $uri,
int $offset,
Stringable|string|null $label
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->replaceLabel($offset, $label)->getUri();
}
/*********************************
* Path resolution methods
*********************************/
/**
* Add a new basepath to the URI path.
*/
public static function addBasePath(Psr7UriInterface|UriInterface $uri, Stringable|string $path): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->addBasePath($path)->getUri();
}
/**
* Add a leading slash to the URI path.
*/
public static function addLeadingSlash(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->addLeadingSlash()->getUri();
}
/**
* Add a trailing slash to the URI path.
*/
public static function addTrailingSlash(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->addTrailingSlash()->getUri();
}
/**
* Append a new segment or a new path to the URI path.
*/
public static function appendSegment(Psr7UriInterface|UriInterface $uri, Stringable|string $segment): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->appendSegment($segment)->getUri();
}
/**
* Convert the Data URI path to its ascii form.
*/
public static function dataPathToAscii(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->dataPathToAscii()->getUri();
}
/**
* Convert the Data URI path to its binary (base64encoded) form.
*/
public static function dataPathToBinary(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->dataPathToBinary()->getUri();
}
/**
* Prepend an new segment or a new path to the URI path.
*/
public static function prependSegment(
Psr7UriInterface|UriInterface $uri,
Stringable|string $segment
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->prependSegment($segment)->getUri();
}
/**
* Remove a basepath from the URI path.
*/
public static function removeBasePath(
Psr7UriInterface|UriInterface $uri,
Stringable|string $path
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->removeBasePath($path)->getUri();
}
/**
* Remove dot segments from the URI path.
*/
public static function removeDotSegments(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeDotSegments()->getUri();
}
/**
* Remove empty segments from the URI path.
*/
public static function removeEmptySegments(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeEmptySegments()->getUri();
}
/**
* Remove the leading slash from the URI path.
*/
public static function removeLeadingSlash(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeLeadingSlash()->getUri();
}
/**
* Remove the trailing slash from the URI path.
*/
public static function removeTrailingSlash(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeTrailingSlash()->getUri();
}
/**
* Remove path segments from the URI path according to their offsets.
*/
public static function removeSegments(Psr7UriInterface|UriInterface $uri, int ...$keys): Psr7UriInterface|UriInterface
{
return Modifier::from($uri)->removeSegments(...$keys)->getUri();
}
/**
* Replace the URI path basename.
*/
public static function replaceBasename(
Psr7UriInterface|UriInterface $uri,
Stringable|string $basename
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->replaceBasename($basename)->getUri();
}
/**
* Replace the data URI path parameters.
*/
public static function replaceDataUriParameters(
Psr7UriInterface|UriInterface $uri,
Stringable|string $parameters
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->replaceDataUriParameters($parameters)->getUri();
}
/**
* Replace the URI path dirname.
*/
public static function replaceDirname(
Psr7UriInterface|UriInterface $uri,
Stringable|string $dirname
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->replaceDirname($dirname)->getUri();
}
/**
* Replace the URI path basename extension.
*/
public static function replaceExtension(
Psr7UriInterface|UriInterface $uri,
Stringable|string $extension
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->replaceExtension($extension)->getUri();
}
/**
* Replace a segment from the URI path according its offset.
*/
public static function replaceSegment(
Psr7UriInterface|UriInterface $uri,
int $offset,
Stringable|string $segment
): Psr7UriInterface|UriInterface {
return Modifier::from($uri)->replaceSegment($offset, $segment)->getUri();
}
}
@@ -0,0 +1,65 @@
{
"name": "league/uri-components",
"type": "library",
"description" : "URI components manipulation library",
"keywords": [
"url",
"uri",
"rfc3986",
"components",
"scheme",
"userinfo",
"host",
"port",
"authority",
"path",
"query",
"fragment",
"modifier",
"middleware"
],
"license": "MIT",
"homepage": "http://uri.thephpleague.com",
"authors": [
{
"name" : "Ignace Nyamagana Butera",
"email" : "nyamsprod@gmail.com",
"homepage" : "https://nyamsprod.com"
}
],
"require": {
"php": "^8.1",
"league/uri": "^7.6"
},
"suggest": {
"ext-bcmath": "to improve IPV4 host parsing",
"ext-mbstring": "to use the sorting algorithm of URLSearchParams",
"ext-fileinfo": "to create Data URI from file contennts",
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain",
"league/uri-polyfill" : "Needed to backport the PHP URI extension for older versions of PHP",
"php-64bit": "to improve IPV4 host parsing",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present",
"rowbot/url": "to handle WHATWG URL",
"bakame/aide-uri": "A polyfill for PHP8.1 until PHP8.4 to add support to PHP Native URI parser"
},
"autoload": {
"psr-4": {
"League\\Uri\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"support": {
"forum": "https://thephpleague.slack.com",
"docs": "https://uri.thephpleague.com",
"issues": "https://github.com/thephpleague/uri-src/issues"
},
"config": {
"sort-packages": true
}
}