diff --git a/composer.json b/composer.json index 60d8b8d..2cc8e35 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "require": { "php": "^7.4 | ^8.0", "nikic/iter": "^2.0", - "symfony/polyfill-php80": "^1.17" + "symfony/polyfill-php80": "^1.22", + "symfony/polyfill-php81": "^1.22" }, "require-dev": { "phpunit/phpunit": ">= 7 < 10", diff --git a/src/AbstractPath.php b/src/AbstractPath.php index 2846cf0..2275975 100644 --- a/src/AbstractPath.php +++ b/src/AbstractPath.php @@ -4,21 +4,26 @@ declare(strict_types=1); namespace Arokettu\Path; +use Arokettu\Path\Helpers\DataTypeHelper; + +/** + * @internal + */ abstract class AbstractPath implements PathInterface { protected string $prefix; protected \SplDoublyLinkedList $components; - abstract protected function parsePath(string $path): \SplDoublyLinkedList; + abstract protected function parsePath(string $path): void; public function __construct(string $path) { - $this->components = $this->parsePath($path); + $this->parsePath($path); } /** - * @param RelativePath|string $path - * @static + * @param RelativePathInterface|string $path + * @return static */ public function resolveRelative($path, bool $strict = false): self { @@ -37,20 +42,31 @@ abstract class AbstractPath implements PathInterface /** * @return static */ - protected function doResolveRelative(RelativePath $path, bool $strict): self + protected function doResolveRelative(RelativePathInterface $path, bool $strict): self { - $relativeComponents = $path->components; + if ($path instanceof RelativePath) { + // optimize + $relativeComponents = $path->components; + } else { + // allow external implementations + $relativeComponents = $path->getComponents(); + if (!array_is_list($relativeComponents)) { + throw new \InvalidArgumentException( + 'Poor RelativePathInterface implementation: getComponents() must return a list' + ); + } + } if ($path->isRoot()) { $newPath = clone $this; - $newPath->components = clone $relativeComponents; + $newPath->components = DataTypeHelper::iterableToNewListInstance($relativeComponents); return $newPath; } $components = clone $this->components; - $numComponents = $relativeComponents->count(); + $numComponents = \count($relativeComponents); for ($i = 0; $i < $numComponents; $i++) { if ($relativeComponents[$i] === '.') { continue; @@ -120,6 +136,11 @@ abstract class AbstractPath implements PathInterface return $components; } + public function toString(): string + { + return $this->prefix . \iter\join('/', $this->components); + } + public function __toString(): string { return $this->toString(); diff --git a/src/Helpers/DataTypeHelper.php b/src/Helpers/DataTypeHelper.php new file mode 100644 index 0000000..eb8b8de --- /dev/null +++ b/src/Helpers/DataTypeHelper.php @@ -0,0 +1,26 @@ +push($value); + } + + return $list; + } +} diff --git a/src/PathInterface.php b/src/PathInterface.php index 5f975dd..8bf69c9 100644 --- a/src/PathInterface.php +++ b/src/PathInterface.php @@ -10,7 +10,7 @@ interface PathInterface extends \Stringable public function toString(): string; /** - * @param RelativePath|string $path + * @param RelativePathInterface|string $path * @return static */ public function resolveRelative($path, bool $strict = false): self; diff --git a/src/RelativePath.php b/src/RelativePath.php index 50a6b5a..2cf5ca0 100644 --- a/src/RelativePath.php +++ b/src/RelativePath.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Arokettu\Path; -final class RelativePath extends AbstractPath +final class RelativePath extends AbstractPath implements RelativePathInterface { private bool $windows; @@ -25,7 +25,7 @@ final class RelativePath extends AbstractPath return new self($path, true); } - protected function parsePath(string $path): \SplDoublyLinkedList + protected function parsePath(string $path): void { $components = explode('/', $path); @@ -45,7 +45,8 @@ final class RelativePath extends AbstractPath $parsedComponents->unshift('.'); } - return $parsedComponents; + $this->prefix = ''; + $this->components = $parsedComponents; } protected function buildRelative(string $path): RelativePath diff --git a/src/RelativePathInterface.php b/src/RelativePathInterface.php new file mode 100644 index 0000000..3865b87 --- /dev/null +++ b/src/RelativePathInterface.php @@ -0,0 +1,10 @@ +resolveRelative($rp, true); } + + public function testExternalRelativeImplementations(): void + { + $p = new RelativePath('i/am/test/path'); + + $rp1 = new class implements RelativePathInterface { + public function __toString(): string { + return ''; + } + + public function getComponents(): array + { + return explode('/', '../../i/am/test/path'); + } + + public function toString(): string + { + throw new \BadMethodCallException('Irrelevant'); + } + + public function resolveRelative($path, bool $strict = false): PathInterface + { + throw new \BadMethodCallException('Irrelevant'); + } + + public function isRoot(): bool + { + return false; + } + }; + + $rp2 = new class implements RelativePathInterface { + public function __toString(): string { + return ''; + } + + public function getComponents(): array + { + return explode('/', 'i/am/test/path'); + } + + public function toString(): string + { + throw new \BadMethodCallException('Irrelevant'); + } + + public function resolveRelative($path, bool $strict = false): PathInterface + { + throw new \BadMethodCallException('Irrelevant'); + } + + public function isRoot(): bool + { + return true; + } + }; + + self::assertEquals('i/am/i/am/test/path', $p->resolveRelative($rp1)->toString()); + self::assertEquals('/i/am/test/path', $p->resolveRelative($rp2)->toString()); + } }