RelativePath

master
Anton Smirnov 11 months ago
parent 3c599bd4c7
commit bf3838dcbf
  1. 127
      src/AbstractPath.php
  2. 17
      src/PathInterface.php
  3. 79
      src/RelativePath.php
  4. 41
      tests/RelativePathTest.php

@ -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…
Cancel
Save