RelativePath
parent
3c599bd4c7
commit
bf3838dcbf
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Arokettu\Path;
|
||||
|
||||
abstract class AbstractPath implements PathInterface
|
||||
{
|
||||
protected string $prefix;
|
||||
protected \SplDoublyLinkedList $components;
|
||||
|
||||
abstract protected function parsePath(string $path): \SplDoublyLinkedList;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->components = $this->parsePath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RelativePath|string $path
|
||||
* @static
|
||||
*/
|
||||
public function resolveRelative($path, bool $strict = false): self
|
||||
{
|
||||
if (\is_string($path)) {
|
||||
$path = $this->buildRelative($path);
|
||||
}
|
||||
|
||||
return $this->doResolveRelative($path, $strict);
|
||||
}
|
||||
|
||||
protected function buildRelative(string $path): RelativePath
|
||||
{
|
||||
return new RelativePath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
private function doResolveRelative(RelativePath $path, bool $strict): self
|
||||
{
|
||||
$relativeComponents = $path->components;
|
||||
|
||||
if ($path->isRoot()) {
|
||||
$newPath = clone $this;
|
||||
$newPath->components = clone $relativeComponents;
|
||||
|
||||
return $newPath;
|
||||
}
|
||||
|
||||
$components = clone $this->components;
|
||||
|
||||
$numComponents = $relativeComponents->count();
|
||||
for ($i = 0; $i < $numComponents; $i++) {
|
||||
if ($relativeComponents[$i] === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($relativeComponents[$i] === '..') {
|
||||
$components->pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
$components->push($relativeComponents[$i]);
|
||||
}
|
||||
|
||||
$newPath = clone $this;
|
||||
$newPath->components = $this->normalizeHead($components, $strict);
|
||||
|
||||
return $newPath;
|
||||
}
|
||||
|
||||
protected function normalize(array $components): \SplDoublyLinkedList
|
||||
{
|
||||
$componentsList = new \SplDoublyLinkedList();
|
||||
|
||||
$prevComponent = null;
|
||||
foreach ($components as $component) {
|
||||
if ($component === '.' || $component === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($component === '..' && $prevComponent !== '..' && $prevComponent !== null) {
|
||||
$componentsList->pop();
|
||||
} else {
|
||||
$componentsList->push($component);
|
||||
}
|
||||
|
||||
$prevComponent = $component;
|
||||
}
|
||||
|
||||
return $componentsList;
|
||||
}
|
||||
|
||||
protected function normalizeHead(\SplDoublyLinkedList $components, bool $strict): \SplDoublyLinkedList
|
||||
{
|
||||
while (!$components->isEmpty()) {
|
||||
if ($components[0] === '.') {
|
||||
$components->shift();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($components[0] === '..') {
|
||||
if ($strict) {
|
||||
throw new \InvalidArgumentException('Relative path went beyond root');
|
||||
}
|
||||
|
||||
$components->shift();
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
public function getComponents(): array
|
||||
{
|
||||
return iterator_to_array($this->components, false);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Arokettu\Path;
|
||||
|
||||
interface PathInterface extends \Stringable
|
||||
{
|
||||
public function getComponents(): array;
|
||||
public function toString(): string;
|
||||
|
||||
/**
|
||||
* @param RelativePath|string $path
|
||||
* @return static
|
||||
*/
|
||||
public function resolveRelative($path, bool $strict = false): self;
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Arokettu\Path;
|
||||
|
||||
final class RelativePath extends AbstractPath
|
||||
{
|
||||
private bool $windows;
|
||||
|
||||
public function __construct(string $path, bool $windows = false)
|
||||
{
|
||||
$this->windows = $windows;
|
||||
|
||||
parent::__construct($path);
|
||||
}
|
||||
|
||||
public static function unix(string $path): self
|
||||
{
|
||||
return new self($path, false);
|
||||
}
|
||||
|
||||
public static function windows(string $path): self
|
||||
{
|
||||
return new self($path, true);
|
||||
}
|
||||
|
||||
protected function parsePath(string $path): \SplDoublyLinkedList
|
||||
{
|
||||
$components = explode('/', $path);
|
||||
|
||||
// forward slash is also a valid path separator on Windows
|
||||
// just also parse backslashes
|
||||
if ($this->windows) {
|
||||
$components = array_merge(
|
||||
...array_map(fn ($a) => explode('\\', $a), $components)
|
||||
);
|
||||
}
|
||||
|
||||
$parsedComponents = $this->normalize($components);
|
||||
|
||||
// absolute-ish relative path
|
||||
$isRoot = $path[0] === '/' || $this->windows && $path[0] === '\\';
|
||||
if (!$isRoot) {
|
||||
$parsedComponents->unshift('.');
|
||||
}
|
||||
|
||||
return $parsedComponents;
|
||||
}
|
||||
|
||||
protected function buildRelative(string $path): RelativePath
|
||||
{
|
||||
return new RelativePath($path, $this->windows);
|
||||
}
|
||||
|
||||
public function isRoot(): bool
|
||||
{
|
||||
return $this->components[0] !== '.' && $this->components[0] !== '..';
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
$directorySeparator = $this->windows ? '\\' : '/';
|
||||
$components = $this->components;
|
||||
|
||||
if ($components[0] === '.') {
|
||||
$components = clone $components;
|
||||
$components->shift();
|
||||
}
|
||||
|
||||
$path = \iter\join($directorySeparator, $components);
|
||||
|
||||
if ($this->isRoot()) {
|
||||
$path = $directorySeparator . $path;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Arokettu\Path\Tests;
|
||||
|
||||
use Arokettu\Path\RelativePath;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RelativePathTest extends TestCase
|
||||
{
|
||||
public function testCreate(): void
|
||||
{
|
||||
// 'absolute' relative path
|
||||
$path = RelativePath::unix('/i/am/./skipme/../test/./path');
|
||||
self::assertEquals('/i/am/test/path', $path->toString());
|
||||
|
||||
// relative path from the current directory
|
||||
$path = RelativePath::unix('./i/am/./skipme/../test/./path');
|
||||
self::assertEquals('i/am/test/path', $path->toString());
|
||||
|
||||
// relative path from the parent directory
|
||||
$path = RelativePath::unix('.././../i/am/./skipme/../test/./path');
|
||||
self::assertEquals('../../i/am/test/path', $path->toString());
|
||||
}
|
||||
|
||||
public function testCreateWindows(): void
|
||||
{
|
||||
// 'absolute' relative path
|
||||
$path = RelativePath::windows('\i\am\./skipme\..\test\.\path');
|
||||
self::assertEquals('\i\am\test\path', $path->toString());
|
||||
|
||||
// relative path from the current directory
|
||||
$path = RelativePath::windows('.\i\am/.\skipme\..\test\.\path');
|
||||
self::assertEquals('i\am\test\path', $path->toString());
|
||||
|
||||
// relative path from the parent directory
|
||||
$path = RelativePath::windows('..\.\..\i\am\.\skipme\../test\.\path');
|
||||
self::assertEquals('..\..\i\am\test\path', $path->toString());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue