Compare commits

...

14 Commits

  1. 3
      .gitignore
  2. 69
      .gitlab-ci.yml
  3. 7
      CHANGELOG.md
  4. 25
      LICENSE.md
  5. 55
      README.md
  6. 4
      composer.json
  7. 22
      docs/_templates/sidebar/brand.html
  8. 10
      docs/conf.py
  9. 73
      docs/helper_classes.rst
  10. 41
      docs/index.rst
  11. 159
      docs/path_classes.rst
  12. 131
      docs/path_interfaces.rst
  13. 1
      docs/requirements.txt
  14. 1
      src/AbstractAbsolutePath.php
  15. 2
      src/PathFactory.php
  16. 66
      src/PathUtils.php
  17. 8
      src/RelativePath.php
  18. 60
      tests/PathUtilsTest.php

3
.gitignore vendored

@ -8,3 +8,6 @@
# phpunit
/.phpunit.result.cache
/reports
# docs
/docs/build

69
.gitlab-ci.yml

@ -0,0 +1,69 @@
stages:
- test
- report
cache:
key: composer-cache
paths:
- .composer-cache/
.test:
before_script:
# install system packages
- apt-get update && apt-get install -y git unzip
# install extensions
- if [ "$INSTALL_XDEBUG" -eq 1 ]; then curl --location https://github.com/FriendsOfPHP/pickle/releases/latest/download/pickle.phar --output pickle.phar; php pickle.phar install --defaults xdebug; docker-php-ext-enable xdebug; fi
# install composer
- php -r "copy('https://composer.github.io/installer.sig', '/tmp/composer.sig');"
- php -r "copy('https://getcomposer.org/installer', '/tmp/composer-setup.php');"
- php -r '$expected = file_get_contents("/tmp/composer.sig"); $actual = hash_file("sha384", "/tmp/composer-setup.php"); exit(intval(!hash_equals($expected, $actual)));'
- php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
- chmod +x /usr/local/bin/composer
- rm /tmp/composer-setup.php /tmp/composer.sig
# cache dependencies
- composer config -g cache-dir "$(pwd)/.composer-cache"
script:
- composer update
- vendor/bin/phpunit
# lowest version with lowest dependencies
test-7.4-lowest:
extends: .test
stage: test
image: php:7.4
script:
- composer update --prefer-lowest
- vendor/bin/phpunit
# lowest version
test-7.4:
extends: .test
stage: test
image: php:7.4
## current release
test-8.0:
extends: .test
stage: test
image: php:8.0
# latest unstable
test-rc:
extends: .test
stage: test
image: php:rc
allow_failure: true
# coverage
coverage:
variables:
INSTALL_XDEBUG: 1
extends: .test
stage: report
only:
- master
image: php:8.0
script:
- composer update
- XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover coverage.xml
- bash <(curl -s https://codecov.io/bash)

7
CHANGELOG.md

@ -0,0 +1,7 @@
# Changelog
## 1.0.0
*Nov 6, 2021*
Initial release

25
LICENSE.md

@ -0,0 +1,25 @@
The MIT License (MIT)
=====================
Copyright © 2021 Anton Smirnov
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.

55
README.md

@ -0,0 +1,55 @@
# PHP Path Library
[![Packagist](https://img.shields.io/packagist/v/arokettu/path.svg?style=flat-square)](https://packagist.org/packages/arokettu/path)
[![PHP](https://img.shields.io/packagist/php-v/arokettu/path.svg?style=flat-square)](https://packagist.org/packages/arokettu/path)
[![License](https://img.shields.io/packagist/l/arokettu/path.svg?style=flat-square)](https://opensource.org/licenses/MIT)
[![Gitlab pipeline status](https://img.shields.io/gitlab/pipeline/sandfox/php-path/master.svg?style=flat-square)](https://gitlab.com/sandfox/php-path/-/pipelines)
[![Codecov](https://img.shields.io/codecov/c/gl/sandfox/php-path?style=flat-square)](https://codecov.io/gl/sandfox/php-path/)
A PHP library to work with absolute and relative paths.
## Usage
```php
<?php
use Arokettu\Path\PathUtils;
use Arokettu\Path\RelativePath;
use Arokettu\Path\UrlPath;
// simple interface
PathUtils::resolveRelativePath('/some/path', '../other/path');
// => /some/other/path
PathUtils::makeRelativePath('/some/path', '/some/other/path');
// => ../other/path
// OOP interface, more control
$url = UrlPath::parse('https://example.com/some/path');
$rel = RelativePath::unix('../other/path');
$url->resolveRelative($rel)->toString();
// => https://example.com/some/other/path
```
## Installation
```bash
composer require arokettu/path
```
## Documentation
Read full documentation here: <https://sandfox.dev/php/path.html>
Also on Read the Docs: <https://php-path.readthedocs.io/>
## Support
Please file issues on our main repo at GitLab: <https://gitlab.com/sandfox/path/-/issues>
## License
The library is available as open source under the terms of the [MIT License].
[MIT License]: https://opensource.org/licenses/MIT

4
composer.json

@ -4,6 +4,7 @@
"keywords": ["paths", "relative path", "filesystem"],
"type": "library",
"license": "MIT",
"homepage": "https://sandfox.dev/php/path.html",
"authors": [
{
"name": "Anton Smirnov",
@ -14,7 +15,8 @@
],
"support": {
"source": "https://gitlab.com/sandfox/php-path",
"issues": "https://gitlab.com/sandfox/php-path/-/issues"
"issues": "https://gitlab.com/sandfox/php-path/-/issues",
"docs": "https://php-path.readthedocs.io/"
},
"config": {
"sort-packages": true

22
docs/_templates/sidebar/brand.html vendored

@ -0,0 +1,22 @@
<a class="sidebar-brand{% if logo %} centered{% endif %}" href="{{ pathto(master_doc) }}">
{% block brand_content %}
{%- if logo %}
<div class="sidebar-logo-container">
<img class="sidebar-logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
</div>
{%- endif %}
{%- if theme_light_logo and theme_dark_logo %}
<div class="sidebar-logo-container">
<img class="sidebar-logo only-light" src="{{ pathto('_static/' + theme_light_logo, 1) }}" alt="Light Logo"/>
<img class="sidebar-logo only-dark" src="{{ pathto('_static/' + theme_dark_logo, 1) }}" alt="Dark Logo"/>
</div>
{%- endif %}
{% if not theme_sidebar_hide_name %}
<span class="sidebar-brand-text">{{ docstitle }}</span>
{%- endif %}
{% endblock brand_content %}
</a>
{%- if current_version -%}
<div class="sidebar-brand">{{ current_version }}</div>
{%- endif -%}

10
docs/conf.py

@ -0,0 +1,10 @@
from datetime import datetime
project = 'Path Library'
author = 'Anton Smirnov'
copyright = '{} {}'.format(datetime.now().year, author)
language = 'en'
html_title = project
html_theme = 'furo'
templates_path = ["_templates"]

73
docs/helper_classes.rst

@ -0,0 +1,73 @@
Helper Classes
##############
PathFactory
===========
``parse()``
-----------
.. code-block:: php
<?php
PathFactory::parse(
string $path,
array $urlSchemes = [],
array $streamSchemes = []
): PathInterface;
The ``parse`` function tries to detect the type of the given string path in the following order:
* Unix path
* Windows path
* Url/Scheme paths:
* If both ``$urlSchemes`` and ``$streamSchemes`` are empty, all scheme prefixed paths are parsed as URLs.
* If at least one of the lists is non-empty, all unknown schemes throw an exception.
* Known schemes are parsed according to which list they belong.
* Relative path of the current OS type.
* Since there is no way to separate root relative path from Unix path, root relative paths are never returned.
All paths are returned by the ``parse()`` constructor in a non strict mode.
PathUtils
=========
``resolveRelativePath()``
-------------------------
.. code-block:: php
<?php
PathUtils::resolveRelativePath(
string|PathInterface $basePath,
string|PathInterface $relativePath
): string;
Resolves relative path from the base path.
If a string is passed: it goes through ``PathFactory::parse()``.
If ``relativePath`` is an absolute path, it is converted to string and returned.
Otherwise ``$basePath->resolveRelative($relativePath)->toString()`` is returned.
``makeRelativePath()``
----------------------
.. code-block:: php
<?php
PathUtils::makeRelativePath(
string|AbsolutePathInterface $basePath,
string|AbsolutePathInterface $targetPath
): string;
Makes relative path from two absolute paths.
If a string is passed: it goes through ``PathFactory::parse()``.
If any of the paths is a relative path, an exception is thrown.
Otherwise ``$basePath->makeRelative($targetPath)->toString()`` is returned.

41
docs/index.rst

@ -0,0 +1,41 @@
Path Library
############
|Packagist| |GitLab| |GitHub| |Bitbucket| |Gitea|
A PHP library to work with absolute and relative paths.
Installation
============
.. code-block:: bash
composer require arokettu/path
Documentation
=============
.. toctree::
:maxdepth: 2
path_interfaces
path_classes
helper_classes
License
=======
The library is available as open source under the terms of the `MIT License`_.
.. _MIT License: https://opensource.org/licenses/MIT
.. |Packagist| image:: https://img.shields.io/packagist/v/arokettu/path.svg?style=flat-square
:target: https://packagist.org/packages/arokettu/path
.. |GitHub| image:: https://img.shields.io/badge/get%20on-GitHub-informational.svg?style=flat-square&logo=github
:target: https://github.com/arokettu/php-path
.. |GitLab| image:: https://img.shields.io/badge/get%20on-GitLab-informational.svg?style=flat-square&logo=gitlab
:target: https://gitlab.com/sandfox/php-path
.. |Bitbucket| image:: https://img.shields.io/badge/get%20on-Bitbucket-informational.svg?style=flat-square&logo=bitbucket
:target: https://bitbucket.org/sandfox/php-path
.. |Gitea| image:: https://img.shields.io/badge/get%20on-Gitea-informational.svg?style=flat-square&logo=gitea
:target: https://sandfox.org/sandfox/php-path

159
docs/path_classes.rst

@ -0,0 +1,159 @@
Path Classes
############
RelativePath
============
The only concrete implementation of ``RelativePathInterface``.
In non-root relative paths first component returned by ``getComponents()`` is either ``'.'`` or ``'..'``.
When resolving relative, windows-ness of the resulting relative will be inherited from the base path.
Available constructors:
.. code-block:: php
<?php
new RelativePath(string $path, bool $windows = false);
``$windows = false``: Unix-like path. Path separators are slashes.
``$windows = true``: Windows-like path. Path separators are both slashes and backslashes.
When exporting a string, backslashes are used.
.. code-block:: php
<?php
RelativePath::unix(string $path): self;
Same as ``new RelativePath($path, windows: false)``
.. code-block:: php
<?php
RelativePath::windows(string $path): self;
Same as ``new RelativePath($path, windows: true)``
.. code-block:: php
<?php
RelativePath::currentOS(string $path): self;
If Windows is detected, create a Windows-like path, otherwise create Unix-like path.
.. note:: Windows is detected by the ``DIRECTORY_SEPARATOR`` constant.
.. code-block:: php
<?php
RelativePath::parse(string $path): self;
Alias of ``currentOS()``.
FilesystemPath
==============
A base class for ``UnixPath`` and ``WindowsPath``.
No default constructor, only a named constructor is available:
.. code-block:: php
<?php
FilesystemPath::parse(string $path, bool $strict = false): self;
If Windows is detected, create a Windows path, otherwise create a Unix path.
Strict mode does not allow to have relative components that traverse beyond root.
.. note:: Windows is detected by the ``DIRECTORY_SEPARATOR`` constant.
.. code-block:: php
<?php
use Arokettu\Path\FilesystemPath;
// on windows
FilesystemPath::parse('C:\Windows\..\..\..\Users'); // C:\Users
FilesystemPath::parse('C:\Windows\..\..\..\Users' strict: true); // exception
UnixPath
--------
A class for Unix paths.
The prefix is ``'/'``
.. code-block:: php
<?php
// these are equal
new UnixPath(string $path, bool $strict = false);
UnixPath::parse(string $path, bool $strict = false): self;
WindowsPath
-----------
.. warning::
Windows usually have much more restrictions on file path than unix-like operating systems
like forbidding characters like ``|`` and ``:``.
The library doesn't check for that even in strict mode.
A class for Windows paths.
``makeRelative()`` returns relatives of the Windows-like type.
Supported paths:
* DOS-like paths.
The classic paths with a drive letter: ``C:\Path``.
Both slashes and backslashes are supported as component separators.
Relative components are resolved on creation like in most other classes here.
The prefix here is a drive letter.
* UNC paths.
Examples:
* Local paths like ``\\*\C:\Path``. The prefix here is ``\\*\C:\``.
* Network paths like ``\\AROKETTUPC\c$``. The prefix here is ``\\AROKETTUPC\``.
UNC paths do not allow forward slashes and relative components.
.. note::
Relative paths with drive letter like ``C:Path\Path`` are valid in Windows
but are not supported by the library in any way.
.. code-block:: php
<?php
// these are equal
new WindowsPath(string $path, bool $strict = false);
WindowsPath::parse(string $path, bool $strict = false): self;
UrlPath
=======
A class for URL paths.
The prefix is scheme + hostname.
.. code-block:: php
<?php
// these are equal
new UrlPath(string $path, bool $strict = false);
UrlPath::parse(string $path, bool $strict = false): self;
StreamPath
==========
A class for PHP stream like paths.
Examples include php streams like ``php://temp``.
It can be useful with libraries that create virtual file systems like `adlawson/vfs`_ and `mikey179/vfsstream`_.
The prefix is scheme.
.. _adlawson/vfs: https://packagist.org/packages/adlawson/vfs
.. _mikey179/vfsstream: https://packagist.org/packages/mikey179/vfsstream
.. code-block:: php
<?php
// these are equal
new StreamPath(string $path, bool $strict = false);
StreamPath::parse(string $path, bool $strict = false): self;

131
docs/path_interfaces.rst

@ -0,0 +1,131 @@
Path Interfaces
###############
PathInterface
=============
``resolveRelative()``
---------------------
.. code-block:: php
<?php
public function resolveRelative(RelativePathInterface $path, bool $strict = false): self;
Convert relative path to absolute or combine two relative paths using the caller object as base.
.. code-block:: php
<?php
use Arokettu\Path\RelativePath;
use Arokettu\Path\UnixPath;
$path = UnixPath::parse('/some/path');
$rel1 = RelativePath::parse('../other/path');
// trailing slashes are preserved
$rel2 = RelativePath::parse('../diff/path/');
$path->resolveRelative($rel1); // /some/other/path
// trailing slash will be present if target path has it
$rel1->resolveRelative($rel2); // ../other/diff/path/
Strict mode throws exception if traversal happens beyond root (no effect if the base path is relative):
.. code-block:: php
<?php
use Arokettu\Path\RelativePath;
use Arokettu\Path\UnixPath;
$path = UnixPath::parse('/some/path');
$rel = RelativePath::parse('../../../../etc/passwd');
$path->resolveRelative($rel); // /etc/passwd
$path->resolveRelative($rel, strict: true); // exception
``getPrefix()``
---------------
Path prefix that you can't traverse beyond like root unix path, windows drive path (C:\\), and url hostname.
``getComponents()``
-------------------
An array of path components excluding prefix.
The last component of the path is empty string if path has trailing (back)slash
``isAbsolute()``
----------------
``true`` for instances of ``AbsolutePathInterface``.
``false`` for instances of ``RelativePathInterface``.
``isRelative()``
----------------
``false`` for instances of ``AbsolutePathInterface``.
``true`` for instances of ``RelativePathInterface``.
``toString()`` & ``__toString()``
---------------------------------
Get string value of the path.
AbsolutePathInterface
=====================
``makeRelative()``
------------------
.. code-block:: php
<?php
public function makeRelative(self $targetPath, ?\Closure $equals = null): RelativePathInterface;
Make relative path from base path and target path of the same type having equal prefixes.
The paths are treated as case sensitive unless ``$equals`` callback is provided.
.. code-block:: php
<?php
use Arokettu\Path\UnixPath;
use Arokettu\Path\UrlPath;
use Arokettu\Path\WindowsPath;
$path1 = UnixPath::parse('/home/arokettu');
$path2 = UnixPath::parse('/home/sandfox/');
// there will be a trailing slash if target path has it
$path1->makeRelative($path2); // ../sandfox/
// ignore case on Windows
$path1 = WindowsPath::parse('c:\users\arokettu');
$path2 = WindowsPath::parse('C:\Users\SandFox');
$path1->makeRelative(
$path2,
fn ($a, $b) => strtoupper($a) === strtoupper($b)
); // ..\SandFox
// resolve urlencoded url path
$path1 = UrlPath::parse('https://example.com/some%20path/child%20dir');
$path2 = UrlPath::parse('https://example.com/some path/child dir');
$path1->makeRelative(
$path2,
fn ($a, $b) => urldecode($a) === urldecode($b)
); // .
RelativePathInterface
=====================
``isRoot()``
------------
``true`` if the relative path is 'root path', i.e. full path excluding prefix.
Examples:
* ``\Users\SandFox`` for Windows path ``C:\Users\SandFox``
* ``/some path/child dir`` for UrlPath ``https://example.com/some path/child dir``
* Functionally equal to Unix path
When applying root path in ``resolveRelative()``, it replaces the whole path excluding prefix.

1
docs/requirements.txt

@ -0,0 +1 @@
furo

1
src/AbstractAbsolutePath.php

@ -20,6 +20,7 @@ abstract class AbstractAbsolutePath extends AbstractPath implements AbsolutePath
/**
* @param static $targetPath
* @param \Closure $equals(string $a, string $b): bool
*/
public function makeRelative(AbsolutePathInterface $targetPath, ?\Closure $equals = null): RelativePathInterface
{

2
src/PathFactory.php

@ -20,7 +20,7 @@ final class PathFactory
return self::parseUrlLike($path, $matches[1], $urlSchemes, $streamSchemes);
}
return DIRECTORY_SEPARATOR === '\\' ? RelativePath::windows($path) : RelativePath::unix($path);
return RelativePath::currentOS($path);
}
private static function parseUrlLike(

66
src/PathUtils.php

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Arokettu\Path;
final class PathUtils
{
/**
* @param string|PathInterface $basePath
* @param string|PathInterface $relativePath
* @return string
*/
public static function resolveRelativePath($basePath, $relativePath): string
{
if (\is_string($basePath)) {
$basePath = PathFactory::parse($basePath);
}
if (\is_string($relativePath)) {
$relativePath = PathFactory::parse($relativePath);
}
if (!($basePath instanceof PathInterface)) {
throw new \InvalidArgumentException('basePath must be a string or an instance of PathInterface');
}
if ($relativePath instanceof RelativePathInterface) {
return $basePath->resolveRelative($relativePath)->toString();
} elseif ($relativePath instanceof AbsolutePathInterface) {
return $relativePath->toString();
}
throw new \InvalidArgumentException('relativePath must be a string or an instance of PathInterface');
}
/**
* @param string|AbsolutePathInterface $basePath
* @param string|AbsolutePathInterface $targetPath
* @return string
*/
public static function makeRelativePath($basePath, $targetPath): string
{
if (\is_string($basePath)) {
$basePath = PathFactory::parse($basePath);
}
if (\is_string($targetPath)) {
$targetPath = PathFactory::parse($targetPath);
}
if (!($basePath instanceof AbsolutePathInterface) || !$basePath->isAbsolute()) {
throw new \InvalidArgumentException(
'basePath must be a string containing absolute path or an instance of AbsolutePathInterface'
);
}
if (!($targetPath instanceof AbsolutePathInterface) || !$targetPath->isAbsolute()) {
throw new \InvalidArgumentException(
'targetPath must be a string containing absolute path or an instance of AbsolutePathInterface'
);
}
return $basePath->makeRelative($targetPath)->toString();
}
}

8
src/RelativePath.php

@ -33,6 +33,14 @@ final class RelativePath extends AbstractPath implements RelativePathInterface
return new self($path, DIRECTORY_SEPARATOR === '\\');
}
/**
* @codeCoverageIgnore OS specific
*/
public static function parse(string $path): self
{
return self::currentOS($path);
}
public function isAbsolute(): bool
{
return false;

60
tests/PathUtilsTest.php

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Arokettu\Path\Tests;
use Arokettu\Path\PathUtils;
use PHPUnit\Framework\TestCase;
class PathUtilsTest extends TestCase
{
public function testMakeRelative(): void
{
self::assertEquals(
'../../.config/composer',
PathUtils::makeRelativePath(
'/home/arokettu/tmp/test',
'/home/arokettu/.config/composer',
),
);
self::assertEquals(
'..\..\AppData\Roaming',
PathUtils::makeRelativePath(
'C:\Users\Arokettu\tmp\test',
'C:\Users\Arokettu\AppData\Roaming',
),
);
}
public function testResolveRelative(): void
{
// any, absolute
self::assertEquals(
'/home/arokettu/.config/composer',
PathUtils::resolveRelativePath(
'/home/arokettu/tmp/test',
'/home/arokettu/.config/composer',
),
);
// absolute, relative
self::assertEquals(
'/home/arokettu/.config/composer',
PathUtils::resolveRelativePath(
'/home/arokettu/tmp/test',
'../../.config/composer',
),
);
// relative, relative
self::assertEquals(
'.config/composer',
PathUtils::resolveRelativePath(
'./tmp/test',
'../../.config/composer',
),
);
}
}
Loading…
Cancel
Save