538 lines
15 KiB
PHP
Executable file
538 lines
15 KiB
PHP
Executable file
<?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 Iterator;
|
|
use League\Uri\Contracts\PathInterface;
|
|
use League\Uri\Contracts\SegmentedPathInterface;
|
|
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 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 URI object.
|
|
*/
|
|
public static function fromUri(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
|
|
{
|
|
$pathSegments = array_map(fn (Stringable|string $segment): string => (string) $segment, $segments);
|
|
$path = implode(self::SEPARATOR, $pathSegments);
|
|
|
|
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 decoded(): string
|
|
{
|
|
return $this->path->decoded();
|
|
}
|
|
|
|
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 Domain::getIterator()
|
|
*
|
|
* @codeCoverageIgnore
|
|
*
|
|
* Returns a new instance from a string or a stringable object.
|
|
*/
|
|
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.
|
|
*/
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
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.
|
|
*/
|
|
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
|
|
{
|
|
return self::fromUri($uri);
|
|
}
|
|
}
|