refactor: Cleanup git state - commit all staged changes

Major refactoring cleanup:
- Add new controller architecture (class-controller-*.php)
- Add new settings-v2 UI (views/settings-v2/)
- Add new CSS architecture (agentic-sidebar.css, tokens)
- Add esbuild build pipeline (scripts/build.js, package.json)
- Add composer dependencies (vendor/)
- Add frontend src directory (assets/js/src/index.jsx)
- Add documentation files
- Remove old/obsolete files (class-settings.php, old CSS)

This commits all pending changes from previous refactoring efforts.
This commit is contained in:
Dwindi Ramadhana
2026-06-17 05:27:58 +07:00
parent d3f142222c
commit 690991c526
7963 changed files with 941566 additions and 67372 deletions

View File

@@ -0,0 +1,23 @@
======================================
Contribute to the phpDocumentor Guides
======================================
Go to the mono-repository
=========================
This project is developed in the mono-repository `phpDocumentor Guides <https://github.com/phpDocumentor/guides>`__.
The repository you are currently in gets auto-created by splitting the mono-repository. You **must not** contribute
to this repository directly but always to the mono-repository linked above.
Create Issues
=============
* If you find something missing or something is wrong in this library, you are welcome to write an issue
describing the problem: `Issues on GitHub <https://github.com/phpDocumentor/guides/issues>`__.
* If you can, please try to fix the problem yourself.
Make changes (create pull requests)
===================================
See the `Contribution chapter <https://docs.phpdoc.org/components/guides/guides/contributions/index.html>`__ in the
`Documentation` <https://docs.phpdoc.org/components/guides/guides/index.html>`__.

View File

@@ -0,0 +1,297 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace League\Flysystem;
use function interface_exists;
if (interface_exists('League\Flysystem\FilesystemInterface') === false) {
interface FilesystemInterface
{
/**
* Check whether a file exists.
*
* @param string $path
*
* @return bool
*/
public function has($path);
/**
* Read a file.
*
* @param string $path The path to the file.
*
* @throws FileNotFoundException
*
* @return string|false The file contents or false on failure.
*/
public function read($path);
/**
* Retrieves a read-stream for a path.
*
* @param string $path The path to the file.
*
* @throws FileNotFoundException
*
* @return resource|false The path resource or false on failure.
*/
public function readStream($path);
/**
* List contents of a directory.
*
* @param string $directory The directory to list.
* @param bool $recursive Whether to list recursively.
*
* @return array A list of file metadata.
*/
public function listContents($directory = '', $recursive = false);
/**
* Get a file's metadata.
*
* @param string $path The path to the file.
*
* @throws FileNotFoundException
*
* @return array|false The file metadata or false on failure.
*/
public function getMetadata($path);
/**
* Get a file's size.
*
* @param string $path The path to the file.
*
* @throws FileNotFoundException
*
* @return int|false The file size or false on failure.
*/
public function getSize($path);
/**
* Get a file's mime-type.
*
* @param string $path The path to the file.
*
* @throws FileNotFoundException
*
* @return string|false The file mime-type or false on failure.
*/
public function getMimetype($path);
/**
* Get a file's timestamp.
*
* @param string $path The path to the file.
*
* @throws FileNotFoundException
*
* @return int|false The timestamp or false on failure.
*/
public function getTimestamp($path);
/**
* Get a file's visibility.
*
* @param string $path The path to the file.
*
* @throws FileNotFoundException
*
* @return string|false The visibility (public|private) or false on failure.
*/
public function getVisibility($path);
/**
* Write a new file.
*
* @param string $path The path of the new file.
* @param string $contents The file contents.
* @param array $config An optional configuration array.
*
* @throws FileExistsException
*
* @return bool True on success, false on failure.
*/
public function write($path, $contents, array $config = []);
/**
* Write a new file using a stream.
*
* @param string $path The path of the new file.
* @param resource $resource The file handle.
* @param array $config An optional configuration array.
*
* @throws InvalidArgumentException If $resource is not a file handle.
* @throws FileExistsException
*
* @return bool True on success, false on failure.
*/
public function writeStream($path, $resource, array $config = []);
/**
* Update an existing file.
*
* @param string $path The path of the existing file.
* @param string $contents The file contents.
* @param array $config An optional configuration array.
*
* @throws FileNotFoundException
*
* @return bool True on success, false on failure.
*/
public function update($path, $contents, array $config = []);
/**
* Update an existing file using a stream.
*
* @param string $path The path of the existing file.
* @param resource $resource The file handle.
* @param array $config An optional configuration array.
*
* @throws InvalidArgumentException If $resource is not a file handle.
* @throws FileNotFoundException
*
* @return bool True on success, false on failure.
*/
public function updateStream($path, $resource, array $config = []);
/**
* Rename a file.
*
* @param string $path Path to the existing file.
* @param string $newpath The new path of the file.
*
* @throws FileExistsException Thrown if $newpath exists.
* @throws FileNotFoundException Thrown if $path does not exist.
*
* @return bool True on success, false on failure.
*/
public function rename($path, $newpath);
/**
* Copy a file.
*
* @param string $path Path to the existing file.
* @param string $newpath The new path of the file.
*
* @throws FileExistsException Thrown if $newpath exists.
* @throws FileNotFoundException Thrown if $path does not exist.
*
* @return bool True on success, false on failure.
*/
public function copy($path, $newpath);
/**
* Delete a file.
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return bool True on success, false on failure.
*/
public function delete($path);
/**
* Delete a directory.
*
* @param string $dirname
*
* @throws RootViolationException Thrown if $dirname is empty.
*
* @return bool True on success, false on failure.
*/
public function deleteDir($dirname);
/**
* Create a directory.
*
* @param string $dirname The name of the new directory.
* @param array $config An optional configuration array.
*
* @return bool True on success, false on failure.
*/
public function createDir($dirname, array $config = []);
/**
* Set the visibility for a file.
*
* @param string $path The path to the file.
* @param string $visibility One of 'public' or 'private'.
*
* @throws FileNotFoundException
*
* @return bool True on success, false on failure.
*/
public function setVisibility($path, $visibility);
/**
* Create a file or update if exists.
*
* @param string $path The path to the file.
* @param string $contents The file contents.
* @param array $config An optional configuration array.
*
* @return bool True on success, false on failure.
*/
public function put($path, $contents, array $config = []);
/**
* Create a file or update if exists.
*
* @param string $path The path to the file.
* @param resource $resource The file handle.
* @param array $config An optional configuration array.
*
* @throws InvalidArgumentException Thrown if $resource is not a resource.
*
* @return bool True on success, false on failure.
*/
public function putStream($path, $resource, array $config = []);
/**
* Read and delete a file.
*
* @param string $path The path to the file.
*
* @throws FileNotFoundException
*
* @return string|false The file contents, or false on failure.
*/
public function readAndDelete($path);
/**
* Get a file/directory handler.
*
* @deprecated
*
* @param string $path The path to the file.
* @param Handler $handler An optional existing handler to populate.
*
* @return Handler Either a file or directory handler.
*/
public function get($path, Handler $handler = null);
/**
* Register a plugin.
*
* @param PluginInterface $plugin The plugin to register.
*
* @return $this
*/
public function addPlugin(PluginInterface $plugin);
}
}

21
vendor/phpdocumentor/filesystem/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2010 Mike van Riel
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.

View File

@@ -0,0 +1,34 @@
.. image:: https://poser.pugx.org/phpdocumentor/filesystem/require/php
:alt: PHP Version Require
:target: https://packagist.org/packages/phpdocumentor/filesystem
.. image:: https://poser.pugx.org/phpdocumentor/filesystem/v/stable
:alt: Latest Stable Version
:target: https://packagist.org/packages/phpdocumentor/filesystem
.. image:: https://poser.pugx.org/phpdocumentor/filesystem/v/unstable
:alt: Latest Unstable Version
:target: https://packagist.org/packages/phpdocumentor/filesystem
.. image:: https://poser.pugx.org/phpdocumentor/filesystem/d/total
:alt: Total Downloads
:target: https://packagist.org/packages/phpdocumentor/filesystem
.. image:: https://poser.pugx.org/phpdocumentor/filesystem/d/monthly
:alt: Monthly Downloads
:target: https://packagist.org/packages/phpdocumentor/filesystem
========================
phpDocumentor FileSystem
========================
This repository is part of `phpDocumentor's Guides library <https://github.com/phpDocumentor/guides>`__.
The package `phpdocumentor/filesystem <https://packagist.org/packages/phpdocumentor/filesystem>`__ provides
filesystem abstractions for phpDocumentor libraries.
:Mono-Repository: https://github.com/phpDocumentor/guides
:Documentation: https://docs.phpdoc.org/components/guides/guides/index.html
:Packagist: https://packagist.org/packages/phpdocumentor/filesystem
:Contribution: https://github.com/phpDocumentor/guides/tree/main/CONTRIBUTING.rst

View File

@@ -0,0 +1,35 @@
{
"name": "phpdocumentor/filesystem",
"description": "Filesystem abstraction for phpdocumentor projects.",
"type": "library",
"license": "MIT",
"homepage": "https://www.phpdoc.org",
"config": {
"sort-packages": true
},
"autoload": {
"psr-4": {
"phpDocumentor\\FileSystem\\": "src/"
},
"classmap": [
"Flysystem/"
]
},
"autoload-dev": {
"psr-4": {
"phpDocumentor\\FileSystem\\": [
"tests/unit/"
]
}
},
"minimum-stability": "stable",
"require": {
"php": "^8.1",
"webmozart/assert": "^1.3"
},
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem;
use Exception;
class FileNotFoundException extends Exception
{
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem;
use Flyfinder\Specification\SpecificationInterface;
interface FileSystem
{
/**
* Check whether a file exists.
*/
public function has(string $path): bool;
/**
* Retrieves a read-stream for a path.
*
* @param string $path The path to the file.
*
* @return resource|false The path resource or false on failure.
*
* @throws FileNotFoundException
*/
public function readStream(string $path): mixed;
/**
* Read a file.
*
* @param string $path The path to the file.
*
* @return string|false The file contents or false on failure.
*
* @throws FileNotFoundException
*/
public function read(string $path): string|false;
public function put(string $path, string $contents): bool;
/** @param resource $resource */
public function putStream(string $path, $resource): void;
/** @return StorageAttributes[] */
public function listContents(string $directory = '', bool $recursive = false): array;
/** @return StorageAttributes[] */
public function find(SpecificationInterface $specification): iterable;
public function isDirectory(string $path): bool;
/** return the unix timestamp file was modified. */
public function lastModified(string $path): int;
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem\Finder;
use phpDocumentor\FileSystem\Path;
use function array_map;
use function array_values;
use function str_starts_with;
final class Exclude
{
/** @var list<string> */
private readonly array $paths;
/** @param list<string|Path> $paths */
public function __construct(
array $paths = [],
private readonly bool $hidden = false,
private readonly bool $symlinks = false,
) {
$this->paths = array_values(
array_map(
static function (string|Path $path): string {
if (str_starts_with((string) $path, '/')) {
return (string) $path;
}
return '/' . $path;
},
$paths,
),
);
}
/** @return list<string> */
public function getPaths(): array
{
return $this->paths;
}
public function excludeHidden(): bool
{
return $this->hidden;
}
public function followSymlinks(): bool
{
return $this->symlinks;
}
/** @param list<string|path> $excludePaths */
public function withPaths(array $excludePaths): self
{
return new self(
$excludePaths,
$this->hidden,
$this->symlinks,
);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem\Finder;
use Flyfinder\Path as FlyFinderPath;
use Flyfinder\Specification\Glob;
use Flyfinder\Specification\HasExtension;
use Flyfinder\Specification\InPath;
use Flyfinder\Specification\IsHidden;
use Flyfinder\Specification\NotSpecification;
use Flyfinder\Specification\SpecificationInterface;
use phpDocumentor\FileSystem\Path;
/**
* Factory class to build Specification used by FlyFinder when reading files to process.
*/
final class SpecificationFactory implements SpecificationFactoryInterface
{
/**
* Creates a SpecificationInterface object based on the ignore and extension parameters.
*
* @param list<string|Path> $paths
* @param list<string> $extensions
*/
public function create(array $paths, Exclude $ignore, array $extensions): SpecificationInterface
{
/** @var ?Glob $pathSpec */
$pathSpec = null;
foreach ($paths as $path) {
if ($path instanceof Path) {
$condition = new InPath(new FlyFinderPath((string) $path));
} else {
$condition = new Glob($path);
}
if ($pathSpec === null) {
$pathSpec = $condition;
continue;
}
$pathSpec = $pathSpec->orSpecification($condition);
}
/** @var ?Glob $ignoreSpec */
$ignoreSpec = null;
foreach ($ignore->getPaths() as $path) {
if ($ignoreSpec === null) {
$ignoreSpec = new Glob($path);
continue;
}
$ignoreSpec = $ignoreSpec->orSpecification(new Glob($path));
}
if ($ignore->excludeHidden()) {
$ignoreSpec = $ignoreSpec === null
? new IsHidden()
: $ignoreSpec->orSpecification(new IsHidden());
}
$result = new HasExtension($extensions);
if ($ignoreSpec !== null) {
$result = $result->andSpecification(new NotSpecification($ignoreSpec));
}
if ($pathSpec !== null) {
$result = $result->andSpecification($pathSpec);
}
return $result;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem\Finder;
use Flyfinder\Specification\SpecificationInterface;
use phpDocumentor\FileSystem\Path;
/**
* Interface for Specifications used to filter the FileSystem.
*/
interface SpecificationFactoryInterface
{
/**
* Creates a SpecificationInterface object based on the ignore and extension parameters.
*
* @param list<string|Path> $paths
* @param list<string> $extensions
*/
public function create(array $paths, Exclude $ignore, array $extensions): SpecificationInterface;
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem;
use Flyfinder\Specification\SpecificationInterface;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem as LeagueFilesystem;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\Local\LocalFilesystemAdapter;
use phpDocumentor\FileSystem\FlysystemV1\FlysystemV1;
use phpDocumentor\FileSystem\FlysystemV3\FlysystemV3;
use function class_exists;
class FlySystemAdapter implements FileSystem
{
public function __construct(
private FileSystem $filesystem,
) {
}
public static function createForPath(string $path): self
{
if (class_exists(Local::class)) {
/** @phpstan-ignore-next-line */
$filesystem = new FlysystemV1(new LeagueFilesystem(new Local($path)));
} else {
$filesystem = new FlysystemV3(
new LeagueFilesystem(
new LocalFilesystemAdapter($path),
),
);
}
return new self($filesystem);
}
public static function createFromFileSystem(LeagueFilesystem|FilesystemInterface $filesystem): self
{
if ($filesystem instanceof FilesystemInterface) {
return new self(new FlysystemV1($filesystem));
}
return new self(new FlysystemV3($filesystem));
}
public function has(string $path): bool
{
return $this->filesystem->has($path);
}
public function readStream(string $path): mixed
{
return $this->filesystem->readStream($path);
}
public function read(string $path): string|false
{
return $this->filesystem->read($path);
}
public function put(string $path, string $contents): bool
{
return $this->filesystem->put($path, $contents);
}
/** @param resource $resource */
public function putStream(string $path, $resource): void
{
$this->filesystem->putStream($path, $resource);
}
/** @return StorageAttributes[] */
public function listContents(string $directory = '', bool $recursive = false): array
{
return $this->filesystem->listContents($directory, $recursive);
}
/** @return StorageAttributes[] */
public function find(SpecificationInterface $specification): iterable
{
return $this->filesystem->find($specification);
}
public function isDirectory(string $path): bool
{
return $this->filesystem->isDirectory($path);
}
public function lastModified(string $path): int
{
return $this->filesystem->lastModified($path);
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem\FlysystemV1;
use Flyfinder\Finder;
use Flyfinder\Specification\SpecificationInterface;
use League\Flysystem\FilesystemInterface;
use phpDocumentor\FileSystem\FileNotFoundException;
use phpDocumentor\FileSystem\FileSystem;
use phpDocumentor\FileSystem\StorageAttributes;
use function array_map;
class FlysystemV1 implements Filesystem
{
private FilesystemInterface $filesystem;
public function __construct(FilesystemInterface $wrappedFilesystem)
{
$this->filesystem = $wrappedFilesystem;
/** @phpstan-ignore-next-line */
$this->filesystem->addPlugin(new Finder());
}
public function has(string $path): bool
{
return $this->filesystem->has($path);
}
public function readStream(string $path): mixed
{
return $this->filesystem->readStream($path);
}
public function read(string $path): string|false
{
return $this->filesystem->read($path);
}
public function put(string $path, string $contents): bool
{
return $this->filesystem->put($path, $contents);
}
/** @param resource $resource */
public function putStream(string $path, $resource): void
{
$this->filesystem->putStream($path, $resource);
}
/** @return StorageAttributes[] */
public function listContents(string $directory = '', bool $recursive = false): array
{
return array_map(
static fn (array $attr) => new \phpDocumentor\FileSystem\FlysystemV1\StorageAttributes($attr),
$this->filesystem->listContents($directory, $recursive),
);
}
/** @return StorageAttributes[] */
public function find(SpecificationInterface $specification): iterable
{
/** @phpstan-ignore-next-line */
foreach ($this->filesystem->find($specification) as $file) {
yield new \phpDocumentor\FileSystem\FlysystemV1\StorageAttributes($file);
}
}
public function isDirectory(string $path): bool
{
$metadata = $this->filesystem->getMetadata($path);
if ($metadata === false) {
return false;
}
return $metadata['type'] === 'dir';
}
public function lastModified(string $path): int
{
$timestamp = $this->filesystem->getTimestamp($path);
if ($timestamp === false) {
throw new FileNotFoundException('File not found: ' . $path);
}
return $timestamp;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem\FlysystemV1;
use phpDocumentor\FileSystem\MethodNotAllowedException;
use phpDocumentor\FileSystem\StorageAttributes as StorageAttributesInterface;
class StorageAttributes implements StorageAttributesInterface
{
/** @param string[] $attributes */
public function __construct(private array $attributes = [])
{
}
public function offsetExists(mixed $offset): bool
{
return isset($this->attributes[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return $this->attributes[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void
{
throw new MethodNotAllowedException();
}
public function offsetUnset(mixed $offset): void
{
throw new MethodNotAllowedException();
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem\FlysystemV3;
use phpDocumentor\FileSystem\MethodNotAllowedException;
use phpDocumentor\FileSystem\StorageAttributes;
use function basename;
use function dirname;
use function in_array;
use function pathinfo;
final class FileAttributes implements StorageAttributes
{
public function __construct(
private readonly \League\Flysystem\StorageAttributes $attributes,
) {
}
public function offsetExists(mixed $offset): bool
{
return $this->attributes->offsetExists($offset);
}
public function offsetGet(mixed $offset): mixed
{
if ($offset === 'basename') {
return basename($this->attributes->path());
}
if ($offset === 'dirname') {
$dirname = dirname($this->attributes->path());
if ($dirname === '.') {
return '';
}
return $dirname;
}
if (in_array($offset, ['filename'], true)) {
return pathinfo($this->attributes->path())[$offset];
}
return $this->attributes->offsetGet($offset);
}
public function offsetSet(mixed $offset, mixed $value): void
{
throw new MethodNotAllowedException('Cannot set attributes on storage attributes');
}
public function offsetUnset(mixed $offset): void
{
throw new MethodNotAllowedException('Cannot unset attributes on storage attributes');
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem\FlysystemV3;
use Flyfinder\Finder;
use Flyfinder\Specification\SpecificationInterface;
use League\Flysystem\Filesystem as LeagueFilesystem;
use League\Flysystem\StorageAttributes;
use phpDocumentor\FileSystem\FileSystem;
final class FlysystemV3 implements FileSystem
{
private LeagueFilesystem $filesystem;
private Finder $finder;
public function __construct(LeagueFilesystem $wrappedFilesystem)
{
$this->filesystem = $wrappedFilesystem;
$this->finder = new Finder($this->filesystem);
}
public function has(string $path): bool
{
return $this->filesystem->has($path);
}
public function isDirectory(string $path): bool
{
return $this->filesystem->directoryExists($path);
}
public function readStream(string $path): mixed
{
return $this->filesystem->readStream($path);
}
public function read(string $path): string|false
{
return $this->filesystem->read($path);
}
public function put(string $path, string $contents): bool
{
$this->filesystem->write($path, $contents);
return true;
}
/** @param resource $resource */
public function putStream(string $path, $resource): void
{
$this->filesystem->writeStream($path, $resource);
}
/** @return FileAttributes[] */
public function listContents(string $directory = '', bool $recursive = false): array
{
return $this->filesystem->listContents($directory, $recursive)->map(
static fn (StorageAttributes $attributes) => new FileAttributes($attributes),
)->toArray();
}
/** @return FileAttributes[] */
public function find(SpecificationInterface $specification): iterable
{
foreach ($this->finder->find($specification) as $file) {
/** @phpstan-ignore-next-line */
yield new FileAttributes($file);
}
}
public function lastModified(string $path): int
{
return $this->filesystem->lastModified($path);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem;
use Exception;
class MethodNotAllowedException extends Exception
{
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem;
use Stringable;
use Webmozart\Assert\Assert;
use function array_pop;
use function ctype_alpha;
use function explode;
use function implode;
use function parse_url;
use function sprintf;
use function strlen;
use function strspn;
use const PHP_URL_SCHEME;
/**
* Value Object for paths.
* This can be absolute or relative.
*/
final class Path implements Stringable
{
/**
* Initializes the path.
*/
public function __construct(private readonly string $path)
{
Assert::notEmpty(
$path,
sprintf('"%s" is not a valid path', $path),
);
}
/**
* Verifies if another Path object has the same identity as this one.
*/
public function equals(self $otherPath): bool
{
return $this->path === (string) $otherPath;
}
/**
* returns a string representation of the path.
*/
public function __toString(): string
{
return $this->path;
}
/**
* Returns whether the file path is an absolute path.
*
* @param string $file A file path
*/
public static function isAbsolutePath(string $file): bool
{
return strspn($file, '/\\', 0, 1)
|| (strlen($file) > 3 && ctype_alpha($file[0])
&& $file[1] === ':'
&& strspn($file, '/\\', 2, 1)
)
|| parse_url($file, PHP_URL_SCHEME) !== null;
}
public static function dirname(Path $input): self
{
$parts = explode('/', (string) $input);
array_pop($parts);
$path = implode('/', $parts);
if ($path === '') {
return new self('/');
}
return new self($path);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\FileSystem;
use ArrayAccess;
/** @extends ArrayAccess<string, mixed> */
interface StorageAttributes extends ArrayAccess
{
/** @return ($offset is 'filename' ? string : mixed) */
public function offsetGet(mixed $offset): mixed;
}

View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10

View File

@@ -0,0 +1,203 @@
on:
push:
branches:
- master
pull_request:
name: Qa workflow
jobs:
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: composer
uses: docker://composer
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: install --no-interaction --prefer-dist --optimize-autoloader
- name: composer-require-checker
uses: docker://phpga/composer-require-checker-ga
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: check --config-file ./composer-require-config.json composer.json
- name: Install phive
run: make install-phive
- name: Install PHAR dependencies
run: tools/phive.phar --no-progress install --copy --trust-gpg-keys 4AA394086372C20A,D2CCAC42F6295E7D,8A03EA3B385DBAA1,CF1A108D0E7AE720 --force-accept-unsigned
phpunit-with-coverage:
runs-on: ubuntu-latest
name: Unit tests
needs: setup
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: Setup PHP
uses: shivammathur/setup-php@master
with:
php-version: 7.2
coverage: xdebug
pecl: false
- name: Run PHPUnit
run: php tools/phpunit
codestyle:
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: Code style check
uses: docker://phpdoc/phpcs-ga:latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: -d memory_limit=1024M
phpstan:
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: PHPStan
uses: docker://phpdoc/phpstan-ga:latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: analyse src --level max --configuration phpstan.neon
psalm:
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: Setup PHP
uses: shivammathur/setup-php@master
with:
php-version: 7.2
coverage: xdebug
pecl: false
- name: Run psalm
run: php tools/psalm
phpunit:
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system:
- ubuntu-latest
- windows-latest
- macOS-latest
php-versions: ['7.2', '7.3', '7.4', '8.0']
name: Unit tests for PHP version ${{ matrix.php-versions }} on ${{ matrix.operating-system }}
needs:
- setup
- phpunit-with-coverage
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: Setup PHP
uses: shivammathur/setup-php@master
with:
php-version: ${{ matrix.php-versions }}
extension-csv: mbstring, intl, iconv, libxml, dom, json, simplexml, zlib
ini-values-csv: memory_limit=2G, display_errors=On, error_reporting=-1
pecl: false
- name: Run PHPUnit
continue-on-error: true
run: php tools/phpunit

View File

@@ -0,0 +1,18 @@
# IDE Shizzle; it is recommended to use a global .gitignore for this but since this is an OSS project we want to make
# it easy to contribute
.idea
/nbproject/private/
.buildpath
.project
.settings
# Build folder and vendor folder are generated code; no need to version this
build/
tools/
temp/
vendor/
*.phar
.phpunit.result.cache
# By default the phpunit.xml.dist is provided; you can override this using a local config file
phpunit.xml

View File

@@ -0,0 +1,53 @@
before_commands:
- "composer install --no-dev --prefer-source"
checks:
php:
excluded_dependencies:
- phpstan/phpstan
tools:
external_code_coverage:
enabled: true
timeout: 300
filter:
excluded_paths: ["examples", "tests", "vendor"]
php_code_sniffer:
enabled: true
config:
standard: PSR2
filter:
paths: ["src/*", "tests/*"]
excluded_paths: []
php_cpd:
enabled: true
excluded_dirs: ["examples", "tests", "vendor"]
php_cs_fixer:
enabled: true
config:
level: all
filter:
paths: ["src/*", "tests/*"]
php_loc:
enabled: true
excluded_dirs: ["examples", "tests", "vendor"]
php_mess_detector:
enabled: true
config:
ruleset: phpmd.xml.dist
design_rules: { eval_expression: false }
filter:
paths: ["src/*"]
php_pdepend:
enabled: true
excluded_dirs: ["examples", "tests", "vendor"]
php_analyzer:
enabled: true
filter:
paths: ["src/*", "tests/*"]
sensiolabs_security_checker: true
checks:
php:
excluded_dependencies:
- phpstan/phpstan

View File

@@ -0,0 +1,6 @@
2020-02-07: Version unreleased
# FIXED
- #25: `InPath(new Path("bar"))` unexpectedly satisfied by `"bartholomew/TODO.txt"` thanks to [Szczepan Hołyszewski]
[Szczepan Hołyszewski]: https://github.com/rulatir

22
vendor/phpdocumentor/flyfinder/LICENSE vendored Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 phpDocumentor
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.

32
vendor/phpdocumentor/flyfinder/Makefile vendored Normal file
View File

@@ -0,0 +1,32 @@
.PHONY: install-phive
install-phive:
mkdir tools; \
wget -O tools/phive.phar https://phar.io/releases/phive.phar; \
wget -O tools/phive.phar.asc https://phar.io/releases/phive.phar.asc; \
gpg --keyserver pool.sks-keyservers.net --recv-keys 0x9D8A98B29B2D5D79; \
gpg --verify tools/phive.phar.asc tools/phive.phar; \
chmod +x tools/phive.phar
.PHONY: setup
setup: install-phive
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phar-ga:latest php tools/phive.phar install --force-accept-unsigned
.PHONY: phpcs
phpcs:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phpcs-ga:latest -d memory_limit=1024M
.PHONY: phpstan
phpstan:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phpstan-ga:latest analyse src --no-progress --level max --configuration phpstan.neon
.PHONY: psaml
psalm:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.4 tools/psalm
.PHONY: test
test:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.2 tools/phpunit
.PHONY: pre-commit-test
pre-commit-test: test phpcs phpstan psalm

View File

@@ -0,0 +1,80 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Coveralls Coverage](https://img.shields.io/coveralls/github/phpDocumentor/FlyFinder.svg)](https://coveralls.io/github/phpDocumentor/FlyFinder?branch=master)
[![Scrutinizer Code Coverage](https://img.shields.io/scrutinizer/coverage/g/phpDocumentor/FlyFinder.svg)](https://scrutinizer-ci.com/g/phpDocumentor/FlyFinder/?branch=master)
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/phpDocumentor/FlyFinder.svg)](https://scrutinizer-ci.com/g/phpDocumentor/FlyFinder/?branch=master)
[![Stable Version](https://img.shields.io/packagist/v/phpDocumentor/FlyFinder.svg)](https://packagist.org/packages/phpDocumentor/FlyFinder)
[![Unstable Version](https://img.shields.io/packagist/vpre/phpDocumentor/FlyFinder.svg)](https://packagist.org/packages/phpDocumentor/FlyFinder)
FlyFinder
=========
FlyFinder is a plugin for [Flysystem](http://flysystem.thephpleague.com/) that will enable you to find files
based on certain criteria.
FlyFinder can search for files that are hidden (either because they are hidden files themselves, or because they are
inside a hidden directory), that have a certain extension, or that exist in a certain path.
Flyfinder does *not* return directories themselves... only files.
## Installation
The easiest way to install this library is with [Composer](https://getcomposer.org) using the following command:
$ composer require phpdocumentor/flyfinder
## Examples
Ready to dive in and don't want to read through all that text below? Just consult the [examples](examples) folder and
check which type of action that your want to accomplish.
## Usage
In order to use the FlyFinder plugin you first need a Flyfinder filesystem with an adapter,
for instance the local adapter.
use League\Flysystem\Filesystem;
use League\Flysystem\Adapter;
use Flyfinder\Finder;
$filesystem = new Filesystem(new Adapter\Local(__DIR__.'/path/to/files/'));
Now you can add the plugin as follows:
$filesystem->addPlugin(new Finder());
FlyFinder will need specifications to know what to look for. The following specifications are available:
- IsHidden (this specification will return `true` when a file or directory is hidden,
- HasExtension (this specification will return `true` when a file or directory has the specified extension),
- InPath (this specification will return `true` when a file is in the given path. Wildcards are allowed.)
- note that this path should be considered relative to the `$filesystem`'s path
Specifications can be instantiated as follows:
use Flyfinder\Path;
use Flyfinder\Specification\IsHidden;
use Flyfinder\Specification\HasExtension;
use Flyfinder\Specification\InPath;
$isHidden = new IsHidden();
$hasExtension = new HasExtension(['txt']);
$inPath = new InPath(new Path('mydir'));
### Combining specifications
You can search on more criteria by combining the specifications. Specifications can be chained as follows:
`$isHidden->andSpecification($hasExtension)` will find all files and directories that are hidden AND have the given
extension.
`$isHidden->orSpecification($hasExtension)` will find all files and directories that are hidden OR have the given
extension.
`$isHidden->notSpecification` will find all files and directories that are NOT hidden.
You can also make longer chains like this:
` $specification = $inPath->andSpecification($hasExtension)->andSpecification($isHidden->notSpecification());`
This will find all files in the given path, that have the given extension and are not hidden.

View File

@@ -0,0 +1,15 @@
{
"symbol-whitelist" : [
"null", "true", "false",
"static", "self", "parent",
"array", "string", "int", "float", "bool", "iterable", "callable", "void", "object"
],
"php-core-extensions" : [
"Core",
"pcre",
"Reflection",
"tokenizer",
"SPL",
"standard"
]
}

View File

@@ -0,0 +1,34 @@
{
"name": "phpdocumentor/flyfinder",
"description": "Flysystem plugin to add file finding capabilities to the Filesystem entity",
"keywords": ["flysystem", "phpdoc"],
"homepage": "http://www.phpdoc.org",
"license": "MIT",
"autoload": {
"psr-4": {
"Flyfinder\\": ["src/"]
}
},
"autoload-dev": {
"psr-4": {
"Flyfinder\\": [
"tests/integration/",
"tests/unit/"
]
}
},
"require": {
"php": "^7.2||^8.0",
"league/flysystem": "^1.0"
},
"minimum-stability": "stable",
"require-dev": {
"mockery/mockery": "^1.3",
"league/flysystem-memory": "~1"
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
}
}

351
vendor/phpdocumentor/flyfinder/composer.lock generated vendored Normal file
View File

@@ -0,0 +1,351 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c0efac4e2b09809ebdee1c870f3ee817",
"packages": [
{
"name": "league/flysystem",
"version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "9be3b16c877d477357c015cec057548cf9b2a14a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a",
"reference": "9be3b16c877d477357c015cec057548cf9b2a14a",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"league/mime-type-detection": "^1.3",
"php": "^7.2.5 || ^8.0"
},
"conflict": {
"league/flysystem-sftp": "<1.0.6"
},
"require-dev": {
"phpspec/prophecy": "^1.11.1",
"phpunit/phpunit": "^8.5.8"
},
"suggest": {
"ext-fileinfo": "Required for MimeType",
"ext-ftp": "Allows you to use FTP server storage",
"ext-openssl": "Allows you to use FTPS server storage",
"league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
"league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
"league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
"league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
"league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
"league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
"league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
"league/flysystem-webdav": "Allows you to use WebDAV storage",
"league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
"spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
"srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"autoload": {
"psr-4": {
"League\\Flysystem\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frenky.net"
}
],
"description": "Filesystem abstraction: Many filesystems, one API.",
"keywords": [
"Cloud Files",
"WebDAV",
"abstraction",
"aws",
"cloud",
"copy.com",
"dropbox",
"file systems",
"files",
"filesystem",
"filesystems",
"ftp",
"rackspace",
"remote",
"s3",
"sftp",
"storage"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/1.x"
},
"funding": [
{
"url": "https://offset.earth/frankdejonge",
"type": "other"
}
],
"time": "2020-08-23T07:39:11+00:00"
},
{
"name": "league/mime-type-detection",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/mime-type-detection.git",
"reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
"reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"php": "^7.2 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.18",
"phpstan/phpstan": "^0.12.68",
"phpunit/phpunit": "^8.5.8 || ^9.3"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\MimeTypeDetection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "Mime-type detection for Flysystem",
"support": {
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0"
},
"funding": [
{
"url": "https://github.com/frankdejonge",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/flysystem",
"type": "tidelift"
}
],
"time": "2021-01-18T20:58:21+00:00"
}
],
"packages-dev": [
{
"name": "hamcrest/hamcrest-php",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/hamcrest/hamcrest-php.git",
"reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3",
"reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3",
"shasum": ""
},
"require": {
"php": "^5.3|^7.0|^8.0"
},
"replace": {
"cordoval/hamcrest-php": "*",
"davedevelopment/hamcrest-php": "*",
"kodova/hamcrest-php": "*"
},
"require-dev": {
"phpunit/php-file-iterator": "^1.4 || ^2.0",
"phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
},
"autoload": {
"classmap": [
"hamcrest"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "This is the PHP port of Hamcrest Matchers",
"keywords": [
"test"
],
"support": {
"issues": "https://github.com/hamcrest/hamcrest-php/issues",
"source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1"
},
"time": "2020-07-09T08:09:16+00:00"
},
{
"name": "league/flysystem-memory",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-memory.git",
"reference": "d0e87477c32e29f999b4de05e64c1adcddb51757"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-memory/zipball/d0e87477c32e29f999b4de05e64c1adcddb51757",
"reference": "d0e87477c32e29f999b4de05e64c1adcddb51757",
"shasum": ""
},
"require": {
"league/flysystem": "~1.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7.10"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"League\\Flysystem\\Memory\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Leppanen",
"email": "chris.leppanen@gmail.com",
"role": "Developer"
}
],
"description": "An in-memory adapter for Flysystem.",
"homepage": "https://github.com/thephpleague/flysystem-memory",
"keywords": [
"Flysystem",
"adapter",
"memory"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-memory/issues",
"source": "https://github.com/thephpleague/flysystem-memory/tree/1.0.2"
},
"time": "2019-05-30T21:34:13+00:00"
},
{
"name": "mockery/mockery",
"version": "1.4.3",
"source": {
"type": "git",
"url": "https://github.com/mockery/mockery.git",
"reference": "d1339f64479af1bee0e82a0413813fe5345a54ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mockery/mockery/zipball/d1339f64479af1bee0e82a0413813fe5345a54ea",
"reference": "d1339f64479af1bee0e82a0413813fe5345a54ea",
"shasum": ""
},
"require": {
"hamcrest/hamcrest-php": "^2.0.1",
"lib-pcre": ">=7.0",
"php": "^7.3 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<8.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5 || ^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4.x-dev"
}
},
"autoload": {
"psr-0": {
"Mockery": "library/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Pádraic Brady",
"email": "padraic.brady@gmail.com",
"homepage": "http://blog.astrumfutura.com"
},
{
"name": "Dave Marshall",
"email": "dave.marshall@atstsolutions.co.uk",
"homepage": "http://davedevelopment.co.uk"
}
],
"description": "Mockery is a simple yet flexible PHP mock object framework",
"homepage": "https://github.com/mockery/mockery",
"keywords": [
"BDD",
"TDD",
"library",
"mock",
"mock objects",
"mockery",
"stub",
"test",
"test double",
"testing"
],
"support": {
"issues": "https://github.com/mockery/mockery/issues",
"source": "https://github.com/mockery/mockery/tree/1.4.3"
},
"time": "2021-02-24T09:51:49+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.2||^8.0"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

View File

@@ -0,0 +1,31 @@
<?php
require_once(__DIR__ . '/../vendor/autoload.php');
use League\Flysystem\Filesystem;
use League\Flysystem\Memory\MemoryAdapter as Adapter;
use Flyfinder\Finder;
use Flyfinder\Specification\IsHidden;
/*
* First create a new Filesystem and add the FlySystem plugin
* In this example we are using a filesystem with the memory adapter
*/
$filesystem = new Filesystem(new Adapter());
$filesystem->addPlugin(new Finder());
// Create some demo files
$filesystem->write('test.txt', 'test');
$filesystem->write('.hiddendir/.test.txt', 'test');
//In order to tell FlyFinder what to find, you need to give it a specification
//In this example the specification will be satisfied by files and directories that are hidden
$specification = new IsHidden();
//FlyFinder will yield a generator object with the files that are found
$generator = $filesystem->find($specification);
$result = [];
foreach ($generator as $value) {
$result[] = $value;
}

View File

@@ -0,0 +1,42 @@
<?php
require_once(__DIR__ . '/../vendor/autoload.php');
use League\Flysystem\Filesystem;
use League\Flysystem\Memory\MemoryAdapter as Adapter;
use Flyfinder\Finder;
use Flyfinder\Path;
use Flyfinder\Specification\IsHidden;
use Flyfinder\Specification\HasExtension;
use Flyfinder\Specification\InPath;
/*
* First create a new Filesystem and add the FlySystem plugin
* In this example we are using a filesystem with the memory adapter
*/
$filesystem = new Filesystem(new Adapter());
$filesystem->addPlugin(new Finder());
// Create some demo files
$filesystem->write('test.txt', 'test');
$filesystem->write('.hiddendir/.test.txt', 'test');
$filesystem->write('.hiddendir/found.txt', 'test');
$filesystem->write('.hiddendir/normaldir/example.txt', 'test');
/*
* In order to tell FlyFinder what to find, you need to give it a specification
* In this example the specification will be satisfied by *.txt files
* within the .hidden directory and its subdirectories that are not hidden
*/
$isHidden = new IsHidden();
$hasExtension = new HasExtension(['txt']);
$inPath = new InPath(new Path('.hiddendir'));
$specification = $inPath->andSpecification($hasExtension)->andSpecification($isHidden->notSpecification());
//FlyFinder will yield a generator object with the files that are found
$generator = $filesystem->find($specification);
$result = [];
foreach ($generator as $value) {
$result[] = $value;
}

View File

@@ -0,0 +1,2 @@
<?php
// dummy file

View File

@@ -0,0 +1,2 @@
<?php
// dummy file

View File

@@ -0,0 +1,2 @@
<?php
// dummy file

View File

@@ -0,0 +1,2 @@
<?php
// dummy file

View File

@@ -0,0 +1,2 @@
<?php
// dummy file

View File

@@ -0,0 +1,2 @@
<?php
// dummy file

View File

@@ -0,0 +1,34 @@
<?php
require_once(__DIR__ . '/../vendor/autoload.php');
use League\Flysystem\Filesystem;
use League\Flysystem\Adapter\Local;
use Flyfinder\Finder;
use Flyfinder\Path;
use Flyfinder\Specification\IsHidden;
use Flyfinder\Specification\HasExtension;
use Flyfinder\Specification\InPath;
use Flyfinder\Specification\AndSpecification;
// (03-sample-files based on some phpDocumentor2 src files)
$filesystem = new Filesystem(new Local(__DIR__ . '/03-sample-files'));
$filesystem->addPlugin(new Finder());
/*
* "phpdoc -d src -i src/phpDocumentor/DomainModel"
* should result in src/Cilex and src/phpDocumentor/. files being found,
* but src/phpDocumentor/DomainModel files being left out
*/
$dashDirectoryPath = new InPath(new Path('src'));
$dashIgnorePath = new InPath(new Path('src/phpDocumentor/DomainModel'));
$isHidden = new IsHidden();
$isPhpFile = new HasExtension(['php']);
$spec = new AndSpecification($dashDirectoryPath, $dashIgnorePath->notSpecification());
$spec->andSpecification($isHidden->notSpecification());
$spec->andSpecification($isPhpFile);
$generator = $filesystem->find($spec);
$result = [];
foreach($generator as $value) {
$result[] = $value;
}

View File

@@ -0,0 +1,35 @@
<?php
require_once(__DIR__ . '/../vendor/autoload.php');
use Flyfinder\Specification\Glob;
use League\Flysystem\Filesystem;
use League\Flysystem\Adapter\Local;
use Flyfinder\Finder;
use Flyfinder\Path;
use Flyfinder\Specification\IsHidden;
use Flyfinder\Specification\HasExtension;
use Flyfinder\Specification\InPath;
use Flyfinder\Specification\AndSpecification;
// (03-sample-files based on some phpDocumentor2 src files)
$filesystem = new Filesystem(new Local(__DIR__ . '/03-sample-files'));
$filesystem->addPlugin(new Finder());
/*
* "phpdoc -d src -i src/phpDocumentor/DomainModel"
* should result in src/Cilex and src/phpDocumentor/. files being found,
* but src/phpDocumentor/DomainModel files being left out
*/
$dashDirectoryPath = new Glob('/src/**/*');
$dashIgnorePath = new InPath(new Path('src/phpDocumentor/DomainModel'));
$isHidden = new IsHidden();
$isPhpFile = new HasExtension(['php']);
$spec = new AndSpecification($dashDirectoryPath, $dashIgnorePath->notSpecification());
$spec->andSpecification($isHidden->notSpecification());
$spec->andSpecification($isPhpFile);
$generator = $filesystem->find($spec);
$result = [];
foreach($generator as $value) {
$result[] = $value;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="phpunit" version="^8.4.3" installed="8.5.15" location="./tools/phpunit" copy="true"/>
<phar name="phpstan" version="^0.12.1" installed="0.12.85" location="./tools/phpstan" copy="true"/>
<phar name="psalm" version="^3.7.2" installed="3.18.2" location="./tools/psalm" copy="true"/>
<phar name="composer-require-checker" version="^1.0.0" installed="1.1.0" location="./tools/composer-require-checker" copy="true"/>
</phive>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0"?>
<ruleset name="phpDocumentor">
<description>The coding standard for phpDocumentor.</description>
<file>src</file>
<file>tests/unit</file>
<file>tests/integration</file>
<arg value="p"/>
<rule ref="PSR2">
<include-pattern>*\.php</include-pattern>
</rule>
<rule ref="Doctrine">
<exclude name="SlevomatCodingStandard.TypeHints.UselessConstantTypeHint.UselessDocComment" />
</rule>
<rule ref="Generic.Files.LineLength.TooLong">
<exclude-pattern>*/src/Finder.php</exclude-pattern>
<exclude-pattern>*/src/Specification/SpecificationInterface.php</exclude-pattern>
<exclude-pattern>*/src/Specification/PrunableInterface.php</exclude-pattern>
<exclude-pattern>*/src/Specification/CompositeSpecification.php</exclude-pattern>
<exclude-pattern>*/tests/unit/Specification/GlobTest.php</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming.SuperfluousSuffix">
<exclude-pattern>*/src/Specification/SpecificationInterface.php</exclude-pattern>
<exclude-pattern>*/src/Specification/PrunableInterface.php</exclude-pattern>
</rule>
<rule ref="Generic.Formatting.SpaceAfterNot">
<properties>
<property name="spacing" value="0" />
</properties>
</rule>
</ruleset>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
name="ProxyManager rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"
>
<rule ref="rulesets/codesize.xml"/>
<rule ref="rulesets/unusedcode.xml"/>
<rule ref="rulesets/design.xml">
<!-- eval is needed to generate runtime classes -->
<exclude name="EvalExpression"/>
</rule>
<rule ref="rulesets/naming.xml">
<exclude name="LongVariable"/>
</rule>
<rule ref="rulesets/naming.xml/LongVariable">
<properties>
<property name="minimum">40</property>
</properties>
</rule>
</ruleset>

View File

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.0/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
verbose="true"
forceCoversAnnotation="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutTodoAnnotatedTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutChangesToGlobalState="true"
>
<testsuites>
<testsuite name="unit">
<directory>./tests/unit/</directory>
</testsuite>
<testsuite name="integration">
<directory>./tests/integration/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html"
target="build/coverage"
lowUpperBound="35"
highLowerBound="70" />
<log type="coverage-clover" target="build/logs/clover.xml"/>
<log type="junit" target="build/logs/junit.xml" />
</logging>
<listeners>
<listener class="Mockery\Adapter\Phpunit\TestListener"
file="vendor/mockery/mockery/library/Mockery/Adapter/Phpunit/TestListener.php"></listener>
</listeners>
</phpunit>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<psalm
totallyTyped="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
ensureArrayStringOffsetsExist="true"
ensureArrayIntOffsetsExist="false"
xsi:schemaLocation="https://getpsalm.org/schema/config config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
</issueHandlers>
</psalm>

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
use Flyfinder\Specification\CompositeSpecification;
use Flyfinder\Specification\SpecificationInterface;
use Generator;
use League\Flysystem\File;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\PluginInterface;
/**
* Flysystem plugin to add file finding capabilities to the filesystem entity.
*
* Note that found *directories* are **not** returned... only found *files*.
*/
class Finder implements PluginInterface
{
/**
* @var FilesystemInterface
* @psalm-suppress PropertyNotSetInConstructor
*/
private $filesystem;
/**
* Get the method name.
*/
public function getMethod() : string
{
return 'find';
}
/**
* Set the Filesystem object.
*/
public function setFilesystem(FilesystemInterface $filesystem) : void
{
$this->filesystem = $filesystem;
}
/**
* Find the specified files
*
* Note that only found *files* are yielded at this level,
* which go back to the caller.
*
* @see File
*
* @return Generator<mixed>
*/
public function handle(SpecificationInterface $specification) : Generator
{
foreach ($this->yieldFilesInPath($specification, '') as $path) {
if (isset($path['type']) && $path['type'] === 'file') {
yield $path;
}
}
}
/**
* Recursively yield files that meet the specification
*
* Note that directories are also yielded at this level,
* since they have to be recursed into. Yielded directories
* will not make their way back to the caller, as they are filtered out
* by {@link handle()}.
*
* @return Generator<mixed>
*
* @psalm-return Generator<array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string}>
*/
private function yieldFilesInPath(SpecificationInterface $specification, string $path) : Generator
{
$listContents = $this->filesystem->listContents($path);
/** @psalm-var array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $location */
foreach ($listContents as $location) {
if ($specification->isSatisfiedBy($location)) {
yield $location;
}
if ($location['type'] !== 'dir'
|| !CompositeSpecification::thatCanBeSatisfiedBySomethingBelow($specification, $location)
) {
continue;
}
yield from $this->yieldFilesInPath($specification, $location['path']);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
/**
* Value Object for paths.
* This can be absolute or relative.
*/
final class Path
{
/**
* file path
*
* @var string
*/
private $path;
/**
* Initializes the path.
*/
public function __construct(string $path)
{
$this->path = $path;
}
/**
* returns a string representation of the path.
*/
public function __toString() : string
{
return $this->path;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* @psalm-immutable
*/
final class AndSpecification extends CompositeSpecification
{
/** @var SpecificationInterface */
private $one;
/** @var SpecificationInterface */
private $other;
/**
* Initializes the AndSpecification object
*/
public function __construct(SpecificationInterface $one, SpecificationInterface $other)
{
$this->one = $one;
$this->other = $other;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return $this->one->isSatisfiedBy($value) && $this->other->isSatisfiedBy($value);
}
/** {@inheritDoc} */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
return self::thatCanBeSatisfiedBySomethingBelow($this->one, $value)
&& self::thatCanBeSatisfiedBySomethingBelow($this->other, $value);
}
/** {@inheritDoc} */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return self::thatWillBeSatisfiedByEverythingBelow($this->one, $value)
&& self::thatWillBeSatisfiedByEverythingBelow($this->other, $value);
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* Base class for specifications, allows for combining specifications
*
* @psalm-immutable
*/
abstract class CompositeSpecification implements SpecificationInterface, PrunableInterface
{
/**
* Returns a specification that satisfies the original specification
* as well as the other specification
*/
public function andSpecification(SpecificationInterface $other) : AndSpecification
{
return new AndSpecification($this, $other);
}
/**
* Returns a specification that satisfies the original specification
* or the other specification
*/
public function orSpecification(SpecificationInterface $other) : OrSpecification
{
return new OrSpecification($this, $other);
}
/**
* Returns a specification that is the inverse of the original specification
* i.e. does not meet the original criteria
*/
public function notSpecification() : NotSpecification
{
return new NotSpecification($this);
}
/** {@inheritDoc} */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
return true;
}
/** {@inheritDoc} */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return false;
}
/**
* Provide default {@see canBeSatisfiedBySomethingBelow()} logic for specification classes
* that don't implement PrunableInterface
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
* @psalm-mutation-free
*/
public static function thatCanBeSatisfiedBySomethingBelow(SpecificationInterface $that, array $value) : bool
{
return $that instanceof PrunableInterface
? $that->canBeSatisfiedBySomethingBelow($value)
: true;
}
/**
* Provide default {@see willBeSatisfiedByEverythingBelow()} logic for specification classes
* that don't implement PrunableInterface
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
* @psalm-mutation-free
*/
public static function thatWillBeSatisfiedByEverythingBelow(SpecificationInterface $that, array $value) : bool
{
return $that instanceof PrunableInterface
&& $that->willBeSatisfiedByEverythingBelow($value);
}
}

View File

@@ -0,0 +1,392 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Many thanks to webmozart by providing the original code in webmozart/glob
*
* @link https://github.com/webmozart/glob/blob/master/src/Glob.php
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use InvalidArgumentException;
use function array_slice;
use function count;
use function explode;
use function implode;
use function max;
use function min;
use function preg_match;
use function rtrim;
use function sprintf;
use function strlen;
use function strpos;
use function substr;
/**
* Glob specification class
*
* @psalm-immutable
*/
final class Glob extends CompositeSpecification
{
/** @var string */
private $regex;
/**
* The "static prefix" is the part of the glob up to the first wildcard "*".
* If the glob does not contain wildcards, the full glob is returned.
*
* @var string
*/
private $staticPrefix;
/**
* The "bounded prefix" is the part of the glob up to the first recursive wildcard "**".
* It is the longest prefix for which the number of directory segments in the partial match
* is known. If the glob does not contain the recursive wildcard "**", the full glob is returned.
*
* @var string
*/
private $boundedPrefix;
/**
* The "total prefix" is the part of the glob before the trailing catch-all wildcard sequence if the glob
* ends with one, otherwise null. It is needed for implementing the A-quantifier pruning hint.
*
* @var string|null
*/
private $totalPrefix;
public function __construct(string $glob)
{
$this->regex = self::toRegEx($glob);
$this->staticPrefix = self::getStaticPrefix($glob);
$this->boundedPrefix = self::getBoundedPrefix($glob);
$this->totalPrefix = self::getTotalPrefix($glob);
}
/**
* @inheritDoc
*/
public function isSatisfiedBy(array $value) : bool
{
//Flysystem paths are not absolute, so make it that way.
$path = '/' . $value['path'];
if (strpos($path, $this->staticPrefix) !== 0) {
return false;
}
return preg_match($this->regex, $path) === 1;
}
/**
* Returns the static prefix of a glob.
*
* The "static prefix" is the part of the glob up to the first wildcard "*".
* If the glob does not contain wildcards, the full glob is returned.
*
* @param string $glob The canonical glob. The glob should contain forward
* slashes as directory separators only. It must not
* contain any "." or ".." segments.
*
* @return string The static prefix of the glob.
*
* @psalm-pure
*/
private static function getStaticPrefix(string $glob) : string
{
self::assertValidGlob($glob);
$prefix = '';
$length = strlen($glob);
for ($i = 0; $i < $length; ++$i) {
$c = $glob[$i];
switch ($c) {
case '/':
$prefix .= '/';
if (self::isRecursiveWildcard($glob, $i)) {
break 2;
}
break;
case '*':
case '?':
case '{':
case '[':
break 2;
case '\\':
[$unescaped, $consumedChars] = self::scanBackslashSequence($glob, $i);
$prefix .= $unescaped;
$i += $consumedChars;
break;
default:
$prefix .= $c;
break;
}
}
return $prefix;
}
private static function getBoundedPrefix(string $glob) : string
{
self::assertValidGlob($glob);
$prefix = '';
$length = strlen($glob);
for ($i = 0; $i < $length; ++$i) {
$c = $glob[$i];
switch ($c) {
case '/':
$prefix .= '/';
if (self::isRecursiveWildcard($glob, $i)) {
break 2;
}
break;
case '\\':
[$unescaped, $consumedChars] = self::scanBackslashSequence($glob, $i);
$prefix .= $unescaped;
$i += $consumedChars;
break;
default:
$prefix .= $c;
break;
}
}
return $prefix;
}
private static function getTotalPrefix(string $glob) : ?string
{
self::assertValidGlob($glob);
$matches = [];
return preg_match('~(?<!\\\\)/\\*\\*(?:/\\*\\*?)+$~', $glob, $matches)
? substr($glob, 0, strlen($glob)-strlen($matches[0]))
: null;
}
/**
* @return mixed[]
*
* @psalm-return array{0: string, 1:int}
* @psalm-pure
*/
private static function scanBackslashSequence(string $glob, int $offset) : array
{
$startOffset = $offset;
$result = '';
switch ($c = $glob[$offset + 1] ?? '') {
case '*':
case '?':
case '{':
case '}':
case '[':
case ']':
case '-':
case '^':
case '$':
case '~':
case '\\':
$result .= $c;
++$offset;
break;
default:
$result .= '\\';
}
return [$result, $offset - $startOffset];
}
/**
* Asserts that glob is well formed
*
* @psalm-pure
*/
private static function assertValidGlob(string $glob) : void
{
if (strpos($glob, '/') !== 0 && strpos($glob, '://') === false) {
throw new InvalidArgumentException(sprintf(
'The glob "%s" is not absolute and not a URI.',
$glob
));
}
}
/**
* Checks if the current position the glob is start of a Recursive directory wildcard
*
* @psalm-pure
*/
private static function isRecursiveWildcard(string $glob, int $i) : bool
{
return isset($glob[$i + 3]) && $glob[$i + 1] . $glob[$i + 2] . $glob[$i + 3] === '**/';
}
/**
* Converts a glob to a regular expression.
*
* @param string $glob The canonical glob. The glob should contain forward
* slashes as directory separators only. It must not
* contain any "." or ".." segments.
*
* @return string The regular expression for matching the glob.
*
* @psalm-pure
*/
private static function toRegEx(string $glob) : string
{
$delimiter = '~';
$inSquare = false;
$curlyLevels = 0;
$regex = '';
$length = strlen($glob);
for ($i = 0; $i < $length; ++$i) {
$c = $glob[$i];
switch ($c) {
case '.':
case '(':
case ')':
case '|':
case '+':
case '^':
case '$':
case $delimiter:
$regex .= '\\' . $c;
break;
case '/':
if (self::isRecursiveWildcard($glob, $i)) {
$regex .= '/([^/]+/)*';
$i += 3;
} else {
$regex .= '/';
}
break;
case '*':
$regex .= '[^/]*';
break;
case '?':
$regex .= '.';
break;
case '{':
$regex .= '(';
++$curlyLevels;
break;
case '}':
if ($curlyLevels > 0) {
$regex .= ')';
--$curlyLevels;
} else {
$regex .= '}';
}
break;
case ',':
$regex .= $curlyLevels > 0 ? '|' : ',';
break;
case '[':
$regex .= '[';
$inSquare = true;
if (isset($glob[$i + 1]) && $glob[$i + 1] === '^') {
$regex .= '^';
++$i;
}
break;
case ']':
$regex .= $inSquare ? ']' : '\\]';
$inSquare = false;
break;
case '-':
$regex .= $inSquare ? '-' : '\\-';
break;
case '\\':
if (isset($glob[$i + 1])) {
switch ($glob[$i + 1]) {
case '*':
case '?':
case '{':
case '}':
case '[':
case ']':
case '-':
case '^':
case '$':
case '~':
case '\\':
$regex .= '\\' . $glob[$i + 1];
++$i;
break;
default:
$regex .= '\\\\';
}
}
break;
default:
$regex .= $c;
break;
}
}
if ($inSquare) {
throw new InvalidArgumentException(sprintf(
'Invalid glob: missing ] in %s',
$glob
));
}
if ($curlyLevels > 0) {
throw new InvalidArgumentException(sprintf(
'Invalid glob: missing } in %s',
$glob
));
}
return $delimiter . '^' . $regex . '$' . $delimiter;
}
/** @inheritDoc */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
$valueSegments = explode('/', '/' . $value['path']);
$boundedPrefixSegments = explode('/', rtrim($this->boundedPrefix, '/'));
$howManySegmentsToConsider = min(count($valueSegments), count($boundedPrefixSegments));
$boundedPrefixGlob = implode('/', array_slice($boundedPrefixSegments, 0, $howManySegmentsToConsider));
$valuePathPrefix = implode('/', array_slice($valueSegments, 1, max($howManySegmentsToConsider-1, 0)));
$prefixValue = $value;
$prefixValue['path'] = $valuePathPrefix;
if ($boundedPrefixGlob === '') {
$boundedPrefixGlob = '/';
}
$spec = new Glob($boundedPrefixGlob);
return $spec->isSatisfiedBy($prefixValue);
}
/** @inheritDoc */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
if ($this->totalPrefix === null) {
return false;
}
$spec = new Glob(rtrim($this->totalPrefix, '/') . '/**/*');
$terminatedValue = $value;
$terminatedValue['path'] = rtrim($terminatedValue['path'], '/') . '/x/x';
return $spec->isSatisfiedBy($terminatedValue);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use function in_array;
/**
* Files and directories meet the specification if they have the given extension
*
* @psalm-immutable
*/
class HasExtension extends CompositeSpecification
{
/** @var string[] */
private $extensions;
/**
* Receives the file extensions you want to find
*
* @param string[] $extensions
*/
public function __construct(array $extensions)
{
$this->extensions = $extensions;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return isset($value['extension']) && in_array($value['extension'], $this->extensions, false);
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Flyfinder\Path;
use function array_slice;
use function count;
use function explode;
use function implode;
use function in_array;
use function min;
use function preg_match;
use function str_replace;
/**
* Files *and directories* meet the specification if they are in the given path.
* Note this behavior is different than in Finder, in that directories *can* meet the spec,
* whereas Finder would never return a directory as "found".
*
* @psalm-immutable
*/
class InPath extends CompositeSpecification
{
/** @var Path */
private $path;
/**
* Initializes the InPath specification
*/
public function __construct(Path $path)
{
$this->path = $path;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
if (in_array($this->path, ['', '.', './'], false)) {
/*
* since flysystem stuff is always relative to the filesystem object's root,
* a spec of "current" dir should always be a match anything being considered
*/
return true;
}
$path = (string) $this->path;
$validChars = '[a-zA-Z0-9\\\/\.\<\>\,\|\:\(\)\&\;\#]';
/*
* a FILE spec would have to match on 'path',
* e.g. value path 'src/Cilex/Provider/MonologServiceProvider.php' should match FILE spec of same path...
* this should also hit a perfect DIR=DIR_SPEC match,
* e.g. value path 'src/Cilex/Provider' should match DIR spec of 'src/Cilex/Provider'
*/
if (isset($value['path'])) {
$pattern = '(^(?!\/)'
. str_replace(['?', '*'], [$validChars . '{1}', $validChars . '*'], $path)
. '(?:/' . $validChars . '*)?$)';
if (preg_match($pattern, $value['path'])) {
return true;
}
}
/* a DIR spec that wasn't an exact match should be able to match on dirname,
* e.g. value dirname 'src' of path 'src/Cilex' should match DIR spec of 'src'
*/
if (isset($value['dirname'])) {
$pattern = '(^(?!\/)'
. str_replace(['?', '*'], [$validChars . '{1}', $validChars . '*'], $path . '/')
. $validChars . '*)';
if (preg_match($pattern, $value['dirname'] . '/')) {
return true;
}
}
return false;
}
/** @inheritDoc */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
$pathSegments = explode('/', (string) $this->path);
$valueSegments = explode('/', $value['path']);
$pathPrefixSegments = array_slice($pathSegments, 0, min(count($pathSegments), count($valueSegments)));
$spec = new InPath(new Path(implode('/', $pathPrefixSegments)));
return $spec->isSatisfiedBy($value);
}
/** @inheritDoc */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return $this->isSatisfiedBy($value);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use function substr;
/**
* Files or directories meet the specification if they are hidden
*
* @psalm-immutable
*/
class IsHidden extends CompositeSpecification
{
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return isset($value['basename']) && substr($value['basename'], 0, 1) === '.';
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* @psalm-immutable
*/
final class NotSpecification extends CompositeSpecification
{
/** @var SpecificationInterface */
private $wrapped;
/**
* Initializes the NotSpecification object
*/
public function __construct(SpecificationInterface $wrapped)
{
$this->wrapped = $wrapped;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return !$this->wrapped->isSatisfiedBy($value);
}
/** @inheritDoc */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
return !self::thatWillBeSatisfiedByEverythingBelow($this->wrapped, $value);
}
/** @inheritDoc */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return !self::thatCanBeSatisfiedBySomethingBelow($this->wrapped, $value);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* @psalm-immutable
*/
final class OrSpecification extends CompositeSpecification
{
/** @var SpecificationInterface */
private $one;
/** @var SpecificationInterface */
private $other;
/**
* Initializes the OrSpecification object
*/
public function __construct(SpecificationInterface $one, SpecificationInterface $other)
{
$this->one = $one;
$this->other = $other;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return $this->one->isSatisfiedBy($value) || $this->other->isSatisfiedBy($value);
}
/** @inheritDoc */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
return self::thatCanBeSatisfiedBySomethingBelow($this->one, $value)
|| self::thatCanBeSatisfiedBySomethingBelow($this->other, $value);
}
/** @inheritDoc */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return self::thatWillBeSatisfiedByEverythingBelow($this->one, $value)
|| self::thatWillBeSatisfiedByEverythingBelow($this->other, $value);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Flyfinder\Specification;
/**
* Interface PrunableInterface
*
* @psalm-immutable
*/
interface PrunableInterface
{
/**
* Checks if anything under the directory path in value can possibly satisfy the specification.
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
*/
public function canBeSatisfiedBySomethingBelow(array $value) : bool;
/**
* Returns true if it is known or can be deduced that everything under the directory path in value
* will certainly satisfy the specification.
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
*/
public function willBeSatisfiedByEverythingBelow(array $value) : bool;
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* Interface for FlyFinder specifications
*
* @psalm-immutable
*/
interface SpecificationInterface
{
/**
* Checks if the value meets the specification
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
*/
public function isSatisfiedBy(array $value) : bool;
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
use PHPUnit\Framework\TestCase;
/**
* Integration test against examples/01-find-hidden-files.php
*
* @coversNothing
*/
class FindHiddenFilesTest extends TestCase
{
public function testFindingHiddenFiles() : void
{
$result = [];
include __DIR__ . '/../../examples/01-find-hidden-files.php';
$this->assertCount(1, $result);
$this->assertSame('.test.txt', $result[0]['basename']);
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
use PHPUnit\Framework\TestCase;
/**
* Integration test against examples/02-find-on-multiple-criteria.php
*
* @coversNothing
*/
class FindOnMultipleCriteriaTest extends TestCase
{
public function testFindingFilesOnMultipleCriteria() : void
{
$result = [];
include __DIR__ . '/../../examples/02-find-on-multiple-criteria.php';
$this->assertCount(2, $result);
$this->assertSame('found.txt', $result[0]['basename']);
$this->assertSame('example.txt', $result[1]['basename']);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
use PHPUnit\Framework\TestCase;
/**
* Integration test against examples/03-sample-phpdoc-layout.php
*
* @coversNothing
*/
class FindOnSamplePhpdocLayoutTest extends TestCase
{
public function testFindingOnSamplePhpdocLayout() : void
{
$result = [];
include __DIR__ . '/../../examples/03-sample-phpdoc-layout.php';
$this->assertCount(4, $result);
$this->assertSame('JmsSerializerServiceProvider.php', $result[0]['basename']);
$this->assertSame('MonologServiceProvider.php', $result[1]['basename']);
$this->assertSame('Application.php', $result[2]['basename']);
$this->assertSame('Bootstrap.php', $result[3]['basename']);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
use PHPUnit\Framework\TestCase;
/**
* Integration test against examples/04-sample-phpdoc-layout-using-glob.php
*
* @coversNothing
*/
class FindOnSamplePhpdocLayoutUsingGlobTest extends TestCase
{
public function testFindingOnSamplePhpdocLayout() : void
{
$result = [];
include __DIR__ . '/../../examples/04-sample-phpdoc-layout-using-glob.php';
$this->assertCount(4, $result);
$this->assertSame('JmsSerializerServiceProvider.php', $result[0]['basename']);
$this->assertSame('MonologServiceProvider.php', $result[1]['basename']);
$this->assertSame('Application.php', $result[2]['basename']);
$this->assertSame('Bootstrap.php', $result[3]['basename']);
}
}

View File

@@ -0,0 +1,468 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
use Flyfinder\Specification\Glob;
use Flyfinder\Specification\HasExtension;
use Flyfinder\Specification\InPath;
use Flyfinder\Specification\IsHidden;
use Generator;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemInterface;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use function array_map;
use function array_values;
use function iterator_to_array;
use function pathinfo;
use function sort;
use function substr;
use function trim;
/**
* Test case for Finder
*
* @coversDefaultClass Flyfinder\Finder
*/
class FinderTest extends TestCase
{
/** @var Finder */
private $fixture;
/**
* Initializes the fixture for this test.
*/
public function setUp() : void
{
$this->fixture = new Finder();
}
public function tearDown() : void
{
m::close();
}
/**
* @covers ::getMethod
*/
public function testGetMethod() : void
{
$this->assertSame('find', $this->fixture->getMethod());
}
public function testIfNotHiddenLetsSubpathsThrough() : void
{
$files = [ 'foo/bar/.hidden/baz/not-hidden.txt' ];
$this->fixture->setFilesystem($this->mockFileSystem($files));
$notHidden = (new IsHidden())->notSpecification();
$this->assertEquals(
$files,
$this->generatorToFileList($this->fixture->handle($notHidden))
);
}
public function testIfDoubleNotHiddenLetsSubpathsThrough() : void
{
$files = [ '.foo/.bar/not-hidden/.baz/.hidden.txt' ];
$this->fixture->setFilesystem($this->mockFileSystem($files));
$notHidden = (new IsHidden())->notSpecification()->notSpecification();
$this->assertEquals(
$files,
$this->generatorToFileList($this->fixture->handle($notHidden))
);
}
public function testIfNeitherHiddenNorExtLetsSubpathsThrough() : void
{
$files = [ 'foo/bar/.hidden/baz.ext/neither-hidden-nor.ext.zzz' ];
$this->fixture->setFilesystem($this->mockFileSystem($files));
$neitherHiddenNorExt =
(new IsHidden())->notSpecification()
->andSpecification((new HasExtension(['ext']))->notSpecification());
$this->assertEquals(
$files,
$this->generatorToFileList($this->fixture->handle($neitherHiddenNorExt))
);
$neitherHiddenNorExtDeMorgan = (new IsHidden())->orSpecification(new HasExtension(['ext']))->notSpecification();
$this->assertEquals(
$files,
$this->generatorToFileList($this->fixture->handle($neitherHiddenNorExtDeMorgan))
);
}
public function testIfNegatedOrCullsExactMatches() : void
{
$files = [
'foo/bar/baz/whatever.txt',
'foo/gen/pics/bottle.jpg',
'foo/lou/time.txt',
];
$this->fixture->setFilesystem($this->mockFileSystem($files, ['foo/bar', 'foo/gen']));
$negatedOr =
(new InPath(new Path('foo/gen')))
->orSpecification(new InPath(new Path('foo/bar')))
->notSpecification();
$this->assertEquals(
['foo/lou/time.txt'],
$this->generatorToFileList($this->fixture->handle($negatedOr))
);
$negatedOrDeMorgan =
(new InPath(new Path('foo/gen')))->notSpecification()
->andSpecification((new InPath(new Path('foo/bar')))->notSpecification());
$this->assertEquals(
['foo/lou/time.txt'],
$this->generatorToFileList($this->fixture->handle($negatedOrDeMorgan))
);
}
public function testIfNegatedAndCullsExactMatches() : void
{
$files = [
'foo/bar/baz/whatever.txt',
'foo/gen/pics/bottle.jpg',
'foo/lou/time.txt',
];
$expected = [
'foo/gen/pics/bottle.jpg',
'foo/lou/time.txt',
];
$this->fixture->setFilesystem($this->mockFileSystem($files, ['foo/bar']));
$negatedAnd =
(new InPath(new Path('foo/*')))
->andSpecification(new InPath(new Path('*/bar')))
->notSpecification();
$this->assertEquals(
$expected,
$this->generatorToFileList($this->fixture->handle($negatedAnd))
);
$negatedAndDeMorgan =
(new InPath(new Path('foo/*')))->notSpecification()
->orSpecification((new InPath(new Path('*/bar')))->notSpecification());
$this->assertEquals(
$expected,
$this->generatorToFileList($this->fixture->handle($negatedAndDeMorgan))
);
}
/**
* @covers ::handle
* @covers ::setFilesystem
* @covers ::<private>
*/
public function testIfCorrectFilesAreBeingYielded() : void
{
$isHidden = m::mock(IsHidden::class);
$filesystem = m::mock(Filesystem::class);
$listContents1 = [
0 => [
'type' => 'dir',
'path' => '.hiddendir',
'dirname' => '',
'basename' => '.hiddendir',
'filename' => '.hiddendir',
],
1 => [
'type' => 'file',
'path' => 'test.txt',
'basename' => 'test.txt',
],
];
$listContents2 = [
0 => [
'type' => 'file',
'path' => '.hiddendir/.test.txt',
'dirname' => '.hiddendir',
'basename' => '.test.txt',
'filename' => '.test',
'extension' => 'txt',
],
];
$filesystem->shouldReceive('listContents')
->with('')
->andReturn($listContents1);
$filesystem->shouldReceive('listContents')
->with('.hiddendir')
->andReturn($listContents2);
$isHidden->shouldReceive('isSatisfiedBy')
->with($listContents1[0])
->andReturn(true);
$isHidden->shouldReceive('canBeSatisfiedBySomethingBelow')
->with($listContents1[0])
->andReturn(true);
$isHidden->shouldReceive('isSatisfiedBy')
->with($listContents1[1])
->andReturn(false);
$isHidden->shouldReceive('isSatisfiedBy')
->with($listContents2[0])
->andReturn(true);
$this->fixture->setFilesystem($filesystem);
$generator = $this->fixture->handle($isHidden);
$result = [];
foreach ($generator as $value) {
$result[] = $value;
}
$expected = [
0 => [
'type' => 'file',
'path' => '.hiddendir/.test.txt',
'dirname' => '.hiddendir',
'basename' => '.test.txt',
'filename' => '.test',
'extension' => 'txt',
],
];
$this->assertSame($expected, $result);
}
public function testSubtreePruningOptimization() : void
{
$filesystem = $this->mockFileSystem(
[
'foo/bar/baz/file.txt',
'foo/bar/baz/file2.txt',
'foo/bar/baz/excluded/excluded.txt',
'foo/bar/baz/excluded/culled/culled.txt',
'foo/bar/baz/excluded/important/reincluded.txt',
'foo/bar/file3.txt',
'foo/lou/someSubdir/file4.txt',
'foo/irrelevant1/',
'irrelevant2/irrelevant3/irrelevantFile.txt',
],
[
'foo/irrelevant1',
'irrelevant2',
'foo/bar/baz/excluded/culled',
]
);
$inFooBar = new InPath(new Path('foo/bar'));
$inFooLou = new InPath(new Path('foo/lou'));
$inExcl = new InPath(new Path('foo/bar/baz/excl*'));
$inReincl = new InPath(new Path('foo/bar/baz/*/important'));
$spec =
$inFooBar
->orSpecification($inFooLou)
->andSpecification($inExcl->notSpecification())
->orSpecification($inReincl);
$finder = $this->fixture;
$finder->setFilesystem($filesystem);
$generator = $finder->handle($spec);
$expected = [
'foo/bar/baz/file.txt',
'foo/bar/baz/file2.txt',
'foo/bar/file3.txt',
'foo/bar/baz/excluded/important/reincluded.txt',
'foo/lou/someSubdir/file4.txt',
];
sort($expected);
$this->assertEquals($expected, $this->generatorToFileList($generator));
}
public function testGlobSubtreePruning() : void
{
$filesystem = $this->mockFileSystem(
[
'foo/bar/baz/file.txt',
'foo/bar/baz/file2.txt',
'foo/bar/baz/excluded/excluded.txt',
'foo/bar/baz/excluded/culled/culled.txt',
'foo/bar/baz/excluded/important/reincluded.txt',
'foo/bar/file3.txt',
'foo/lou/someSubdir/file4.txt',
'foo/irrelevant1/',
'irrelevant2/irrelevant3/irrelevantFile.txt',
],
[
'foo/irrelevant1',
'irrelevant2',
'foo/bar/baz/excluded/culled',
]
);
$txtInFooBar = new Glob('/foo/bar/**/*.txt');
$inFooLou = new Glob('/foo/lou/**/*');
$inExcl = new Glob('/foo/bar/baz/excl*/**/*');
$inReincl = new Glob('/foo/bar/baz/*/important/**/*');
$spec = $txtInFooBar
->orSpecification($inFooLou)
->andSpecification($inExcl->notSpecification())
->orSpecification($inReincl);
$finder = $this->fixture;
$finder->setFilesystem($filesystem);
$generator = $finder->handle($spec);
$expected = [
'foo/bar/baz/file.txt',
'foo/bar/baz/file2.txt',
'foo/bar/file3.txt',
'foo/bar/baz/excluded/important/reincluded.txt',
'foo/lou/someSubdir/file4.txt',
];
sort($expected);
$this->assertEquals($expected, $this->generatorToFileList($generator));
}
/**
* @return string[]
*/
protected function generatorToFileList(Generator $generator) : array
{
$actual = array_values(array_map(static function ($v) {
return $v['path'];
}, iterator_to_array($generator)));
sort($actual);
return $actual;
}
/**
* @param string[] $pathList
*
* @return mixed[]
*/
protected function mockFileTree(array $pathList) : array
{
$result = [
'.' => [
'type' => 'dir',
'path' => '',
'dirname' => '.',
'basename' => '.',
'filename' => '.',
'contents' => [],
],
];
foreach ($pathList as $path) {
$isFile = substr($path, -1) !== '/';
$child = null;
while (true) {
$info = pathinfo($path);
if ($isFile) {
$isFile = false;
$result[$path] = [
'type' => 'file',
'path' => $path,
'dirname' => $info['dirname'],
'basename' => $info['basename'],
'filename' => $info['filename'],
'extension' => $info['extension'],
];
} else {
$existed = true;
if (!isset($result[$path])) {
$existed = false;
$result[$path] = [
'type' => 'dir',
'path' => $path,
'basename' => $info['basename'],
'filename' => $info['filename'],
'contents' => [],
];
}
if ($child!==null) {
$result[$path]['contents'][] = $child;
}
if ($existed) {
break;
}
}
$child = $info['basename'];
$path = $info['dirname'];
}
}
return $result;
}
/**
* @param mixed[] $fileTreeMock
*
* @return mixed[]
*/
protected function mockListContents(array $fileTreeMock, string $path) : array
{
$path = trim($path, '/');
if (substr($path . ' ', 0, 2)==='./') {
$path = substr($path, 2);
}
if ($path==='') {
$path = '.';
}
if (!isset($fileTreeMock[$path]) || $fileTreeMock[$path]['type'] === 'file') {
return [];
}
$result = [];
foreach ($fileTreeMock[$path]['contents'] as $basename) {
$childPath = ($path==='.' ? '' : $path . '/') . $basename;
if (!isset($fileTreeMock[$childPath])) {
continue;
}
$result[$basename] = $fileTreeMock[$childPath];
}
return $result;
}
/**
* @param string[] $paths
* @param string[] $pathsThatShouldNotBeListed
*/
protected function mockFileSystem(array $paths, array $pathsThatShouldNotBeListed = []) : FilesystemInterface
{
$fsData = $this->mockFileTree($paths);
$filesystem = m::mock(Filesystem::class);
$filesystem->shouldReceive('listContents')
->zeroOrMoreTimes()
->andReturnUsing(function (string $path) use ($fsData, $pathsThatShouldNotBeListed) : array {
$this->assertNotContains($path, $pathsThatShouldNotBeListed);
return array_values($this->mockListContents($fsData, $path));
});
return $filesystem;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
use PHPUnit\Framework\TestCase;
/**
* Test case for Path
*
* @coversDefaultClass Flyfinder\Path
*/
class PathTest extends TestCase
{
/**
* @covers ::__construct
* @covers ::__toString
*/
public function testToString() : void
{
$path = new Path('/my/Path');
$this->assertSame('/my/Path', (string) $path);
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Mockery as m;
use PHPUnit\Framework\TestCase;
/**
* Test case for AndSpecification
*
* @coversDefaultClass \Flyfinder\Specification\AndSpecification
*/
class AndSpecificationTest extends TestCase
{
/** @var m\MockInterface|HasExtension */
private $hasExtension;
/** @var m\MockInterface|IsHidden */
private $isHidden;
/** @var AndSpecification */
private $fixture;
/**
* Initializes the fixture for this test.
*/
public function setUp() : void
{
$this->hasExtension = m::mock(HasExtension::class);
$this->isHidden = m::mock(IsHidden::class);
$this->fixture = new AndSpecification($this->hasExtension, $this->isHidden);
}
public function tearDown() : void
{
m::close();
}
/**
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsSatisfied() : void
{
$this->hasExtension->shouldReceive('isSatisfiedBy')->once()->andReturn(true);
$this->isHidden->shouldReceive('isSatisfiedBy')->once()->andReturn(true);
$this->assertTrue($this->fixture->isSatisfiedBy(['test']));
}
/**
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsNotSatisfied() : void
{
$this->hasExtension->shouldReceive('isSatisfiedBy')->once()->andReturn(true);
$this->isHidden->shouldReceive('isSatisfiedBy')->once()->andReturn(false);
$this->assertFalse($this->fixture->isSatisfiedBy(['test']));
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Mockery as m;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
* Test case for CompositeSpecification
*
* @coversDefaultClass Flyfinder\Specification\CompositeSpecification
*/
class CompositeSpecificationTest extends TestCase
{
/** @var m\MockInterface|HasExtension */
private $hasExtension;
/** @var CompositeSpecification|MockObject */
private $fixture;
/**
* Initializes the fixture for this test.
*/
public function setUp() : void
{
$this->hasExtension = m::mock(HasExtension::class);
$this->fixture = $this->getMockForAbstractClass(CompositeSpecification::class);
}
public function tearDown() : void
{
m::close();
}
/**
* @uses \Flyfinder\Specification\AndSpecification
*
* @covers ::andSpecification
*/
public function testAndSpecification() : void
{
$this->assertInstanceOf(
AndSpecification::class,
$this->fixture->andSpecification($this->hasExtension)
);
}
/**
* @uses \Flyfinder\Specification\OrSpecification
*
* @covers ::orSpecification
*/
public function testOrSpecification() : void
{
$this->assertInstanceOf(
OrSpecification::class,
$this->fixture->orSpecification($this->hasExtension)
);
}
/**
* @uses \Flyfinder\Specification\NotSpecification
*
* @covers ::notSpecification
*/
public function testNotSpecification() : void
{
$this->assertInstanceOf(
NotSpecification::class,
$this->fixture->notSpecification()
);
}
}

View File

@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Flyfinder\Specification;
use Generator;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use function is_array;
use function sprintf;
/**
* @coversDefaultClass \Flyfinder\Specification\Glob
* @covers ::<private>
* @covers ::isSatisfiedBy
* @covers ::__construct
*/
final class GlobTest extends TestCase
{
/**
* @param mixed[] $file
*
* @dataProvider matchingPatternFileProvider
* @dataProvider matchingPatternFileWithEscapeCharProvider
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $file
*/
public function testGlobIsMatching(string $pattern, array $file) : void
{
$glob = new Glob($pattern);
$this->assertTrue(
$glob->isSatisfiedBy($file),
sprintf('Failed: %s to match %s', $pattern, $file['path'])
);
}
/**
* @param mixed[] $file
*
* @dataProvider notMatchingPatternFileProvider
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $file
*/
public function testGlobIsNotMatching(string $pattern, array $file) : void
{
$glob = new Glob($pattern);
$this->assertFalse(
$glob->isSatisfiedBy($file),
sprintf('Failed: %s to match %s', $pattern, $file['path'])
);
}
/**
* @dataProvider invalidPatternProvider
*/
public function testInvalidGlobThrows(string $pattern) : void
{
$this->expectException(InvalidArgumentException::class);
new Glob($pattern);
}
/**
* @covers ::canBeSatisfiedBySomethingBelow
*/
public function testCanBeSatisfiedBySomethingBelow() : void
{
$glob = new Glob('/**/*');
$this->assertTrue($glob->canBeSatisfiedBySomethingBelow(['path' => 'src']));
}
public function invalidPatternProvider() : Generator
{
$invalidPatterns = [
'[aaa',
'{aaa',
'{a,{b}',
'aaaa', //path must be absolute
];
foreach ($invalidPatterns as $pattern) {
yield $pattern => [$pattern];
}
}
public function matchingPatternFileProvider() : Generator
{
$input = [
'/*.php' => 'test.php',
'/src/*' => 'src/test.php',
'/src/**/*.php' => 'src/subdir/test.php',
'/src/**/*' => 'src/subdir/second/test.php',
'/src/{subdir,other}/*' => [
'src/subdir/test.php',
'src/other/test.php',
],
'/src/subdir/test-[a-c].php' => [
'src/subdir/test-a.php',
'src/subdir/test-b.php',
'src/subdir/test-c.php',
],
'/src/subdir/test-[^a-c].php' => 'src/subdir/test-d.php',
'/src/subdir/test-?.php' => [
'src/subdir/test-a.php',
'src/subdir/test-b.php',
'src/subdir/test-c.php',
'src/subdir/test-~.php',
],
'/src/subdir/test-}.php' => 'src/subdir/test-}.php',
];
yield from $this->toTestData($input);
}
public function matchingPatternFileWithEscapeCharProvider() : Generator
{
$escapeChars = [
'*',
'?',
'{',
'}',
'[',
']',
'-',
'^',
'$',
'~',
'\\',
'\\\\',
];
foreach ($escapeChars as $char) {
$file = sprintf('/src/test\\%s.php', $char);
yield $file => [
$file,
['path' => sprintf('src/test%s.php', $char)],
];
}
}
public function notMatchingPatternFileProvider() : Generator
{
$input = [
'/*.php' => 'test.css',
'/src/*' => 'src/subdir/test.php',
'/src/**/*.php' => 'src/subdir/test.css',
'/src/subdir/test-[a-c].php' => 'src/subdir/test-d.php',
'/src/subdir/test-[^a-c].php' => [
'src/subdir/test-a.php',
'src/subdir/test-b.php',
'src/subdir/test-c.php',
],
'/src' => 'test/file.php',
];
yield from $this->toTestData($input);
}
/**
* @param mixed[] $input
*/
private function toTestData(array $input) : Generator
{
foreach ($input as $glob => $path) {
if (!is_array($path)) {
$path = [$path];
}
foreach ($path as $key => $item) {
yield ($key !== 0 ? $key . ' - ' : '') . $glob => [
$glob,
['path' => $item],
];
}
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Mockery as m;
use PHPUnit\Framework\TestCase;
/**
* Test case for HasExtension
*
* @coversDefaultClass Flyfinder\Specification\HasExtension
*/
class HasExtensionTest extends TestCase
{
/** @var HasExtension */
private $fixture;
/**
* Initializes the fixture for this test.
*/
public function setUp() : void
{
$this->fixture = new HasExtension(['txt']);
}
public function tearDown() : void
{
m::close();
}
/**
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsSatisfied() : void
{
$this->assertTrue($this->fixture->isSatisfiedBy(['extension' => 'txt']));
}
/**
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsNotSatisfied() : void
{
$this->assertFalse($this->fixture->isSatisfiedBy(['extension' => 'php']));
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Flyfinder\Path;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use function dirname;
/**
* Test case for InPath
*
* @coversDefaultClass \Flyfinder\Specification\InPath
* @covers ::<private>
*/
class InPathTest extends TestCase
{
/** @var InPath */
private $fixture;
public function tearDown() : void
{
m::close();
}
/**
* @uses \Flyfinder\Path
*
* @covers ::__construct
* @covers ::isSatisfiedBy
* @dataProvider validDirnames
*/
public function testExactMatch() : void
{
$absolutePath = 'absolute/path/to/file.txt';
$spec = new InPath(new Path($absolutePath));
$this->assertTrue($spec->isSatisfiedBy([
'type' => 'file',
'path' => $absolutePath,
'dirname' => $absolutePath,
'filename' => 'file',
'extension' => 'txt',
'basename' => 'file.txt',
]));
}
private function useWildcardPath() : void
{
$this->fixture = new InPath(new Path('*dden?ir/n'));
}
/**
* @uses \Flyfinder\Path
*
* @covers ::__construct
* @covers ::isSatisfiedBy
* @dataProvider validDirnames
*/
public function testIfSpecificationIsSatisfied(string $dirname) : void
{
$this->useWildcardPath();
$this->assertTrue($this->fixture->isSatisfiedBy(['dirname' => $dirname]));
}
/**
* @uses \Flyfinder\Path
*
* @covers ::__construct
* @covers ::isSatisfiedBy
* @dataProvider validDirnames
*/
public function testWithSingleDotSpec(string $dirname) : void
{
$spec = new InPath(new Path('.'));
$this->assertTrue($spec->isSatisfiedBy(['dirname' => $dirname]));
}
/**
* @uses \Flyfinder\Path
*
* @covers ::__construct
* @covers ::isSatisfiedBy
* @dataProvider validDirnames
*/
public function testWithCurrentDirSpec(string $dirname) : void
{
$spec = new InPath(new Path('./'));
$this->assertTrue($spec->isSatisfiedBy(['dirname' => $dirname]));
}
/**
* Data provider for testIfSpecificationIsSatisfied. Contains a few valid directory names
*
* @return string[][]
*/
public function validDirnames() : array
{
return [
['.hiddendir/n'],
['.hiddendir/n/'],
['.hiddendir/n/somedir'],
['.hiddendir/n/somedir.txt'],
['ddenxir/n'],
];
}
/**
* @uses \Flyfinder\Path
*
* @covers ::__construct
* @covers ::isSatisfiedBy
* @dataProvider invalidDirnames
*/
public function testIfSpecificationIsNotSatisfied(string $dirname) : void
{
$this->useWildcardPath();
$this->assertFalse($this->fixture->isSatisfiedBy(['dirname' => $dirname]));
}
/**
* Data provider for testIfSpecificationIsNotSatisfied. Contains a few invalid directory names
*
* @return string[][]
*/
public function invalidDirnames() : array
{
return [
['/hiddendir/n'],
['.hiddendir/normaldir'],
['.hiddendir.ext/n'],
['.hiddenxxir/n'],
['.hiddenir/n'],
];
}
/**
* @uses \Flyfinder\Path
*
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testNoFalsePositiveWithLongerDirName() : void
{
$prefixDir = 'absolute/path';
$absolutePath = 'absolute/pathMOAR/to/file.txt';
$spec = new InPath(new Path($prefixDir));
$this->assertFalse($spec->isSatisfiedBy([
'type' => 'file',
'path' => $absolutePath,
'dirname' => dirname($absolutePath),
'filename' => 'file',
'extension' => 'txt',
'basename' => 'file.txt',
]));
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Mockery as m;
use PHPUnit\Framework\TestCase;
/**
* Test case for IsHidden
*
* @coversDefaultClass Flyfinder\Specification\IsHidden
*/
class IsHiddenTest extends TestCase
{
/** @var IsHidden */
private $fixture;
/**
* Initializes the fixture for this test.
*/
public function setUp() : void
{
$this->fixture = new IsHidden();
}
public function tearDown() : void
{
m::close();
}
/**
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsSatisfied() : void
{
$this->assertTrue($this->fixture->isSatisfiedBy(['basename' => '.test']));
}
/**
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsNotSatisfied() : void
{
$this->assertFalse($this->fixture->isSatisfiedBy(['basename' => 'test']));
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Mockery as m;
use PHPUnit\Framework\TestCase;
/**
* Test case for NotSpecification
*
* @coversDefaultClass Flyfinder\Specification\NotSpecification
*/
class NotSpecificationTest extends TestCase
{
/** @var m\MockInterface|HasExtension */
private $hasExtension;
/** @var NotSpecification */
private $fixture;
/**
* Initializes the fixture for this test.
*/
public function setUp() : void
{
$this->hasExtension = m::mock(HasExtension::class);
$this->fixture = new NotSpecification($this->hasExtension);
}
public function tearDown() : void
{
m::close();
}
/**
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsSatisfied() : void
{
$this->hasExtension->shouldReceive('isSatisfiedBy')->once()->andReturn(false);
$this->assertTrue($this->fixture->isSatisfiedBy(['test']));
}
/**
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsNotSatisfied() : void
{
$this->hasExtension->shouldReceive('isSatisfiedBy')->once()->andReturn(true);
$this->assertFalse($this->fixture->isSatisfiedBy(['test']));
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Mockery as m;
use PHPUnit\Framework\TestCase;
/**
* Test case for OrSpecification
*
* @coversDefaultClass Flyfinder\Specification\OrSpecification
*/
class OrSpecificationTest extends TestCase
{
/** @var m\MockInterface|HasExtension */
private $hasExtension;
/** @var m\MockInterface|IsHidden */
private $isHidden;
/** @var OrSpecification */
private $fixture;
/**
* Initializes the fixture for this test.
*/
public function setUp() : void
{
$this->hasExtension = m::mock(HasExtension::class);
$this->isHidden = m::mock(IsHidden::class);
$this->fixture = new OrSpecification($this->hasExtension, $this->isHidden);
}
public function tearDown() : void
{
m::close();
}
/**
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsSatisfied() : void
{
$this->hasExtension->shouldReceive('isSatisfiedBy')->once()->andReturn(false);
$this->isHidden->shouldReceive('isSatisfiedBy')->once()->andReturn(true);
$this->assertTrue($this->fixture->isSatisfiedBy(['test']));
}
/**
* @covers ::__construct
* @covers ::isSatisfiedBy
*/
public function testIfSpecificationIsNotSatisfied() : void
{
$this->hasExtension->shouldReceive('isSatisfiedBy')->once()->andReturn(false);
$this->isHidden->shouldReceive('isSatisfiedBy')->once()->andReturn(false);
$this->assertFalse($this->fixture->isSatisfiedBy(['test']));
}
}

View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10

View File

@@ -0,0 +1,175 @@
on:
push:
branches:
- master
pull_request:
name: Qa workflow
env:
extensions: mbstring, intl, iconv, libxml, dom, json, simplexml, zlib, fileinfo
key: cache-v1 # can be any string, change to clear the extension cache.
defaultPHPVersion: '7.3'
jobs:
phpunit-with-coverage:
runs-on: ubuntu-latest
name: Unit tests
steps:
- uses: actions/checkout@v2
- name: Install graphviz
run: sudo apt-get update && sudo apt-get install -y graphviz
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.defaultPHPVersion }}
ini-values: memory_limit=2G, display_errors=On, error_reporting=-1
tools: phive
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-
- name: Install Composer dependencies
run: |
composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run PHPUnit
run: php vendor/bin/phpunit
phpunit:
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system:
- ubuntu-latest
php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1']
name: Unit tests for PHP version ${{ matrix.php-versions }} on ${{ matrix.operating-system }}
needs:
- phpunit-with-coverage
steps:
- uses: actions/checkout@v2
- name: Install graphviz
run: sudo apt-get update && sudo apt-get install -y graphviz
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extension-csv: mbstring, simplexml
ini-values: memory_limit=2G, display_errors=On, error_reporting=-1
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-
- name: Install Composer dependencies
run: |
composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run PHPUnit
run: php vendor/bin/phpunit
codestyle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: Code style check
uses: docker://phpdoc/phpcs-ga:latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: -d memory_limit=1024M
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.defaultPHPVersion }}
ini-values: memory_limit=2G, display_errors=On, error_reporting=-1
tools: pecl
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-
- name: Install Composer dependencies
run: |
composer install --no-progress --prefer-dist --optimize-autoloader
- name: PHPStan
uses: phpDocumentor/phpstan-ga@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: analyse src tests --configuration phpstan.neon
psalm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.3
ini-values: memory_limit=2G, display_errors=On, error_reporting=-1
tools: pecl, psalm
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-
- name: Install Composer dependencies
run: |
composer install --no-progress --prefer-dist --optimize-autoloader
- name: Psalm
run: vendor/bin/psalm.phar --output-format=github

View File

@@ -0,0 +1,18 @@
# IDE Shizzle; it is recommended to use a global .gitignore for this but since this is an OSS project we want to make
# it easy to contribute
.idea
/nbproject/private/
.buildpath
.project
.settings
# Build folder and vendor folder are generated code; no need to version this
build/
vendor/
temp/
tools/
*.phar
# By default the phpunit.xml.dist is provided; you can override this using a local config file
phpunit.xml
composer.lock

View File

@@ -0,0 +1,48 @@
before_commands:
- "composer install --no-dev --prefer-source"
checks:
php:
excluded_dependencies:
- phpstan/phpstan
tools:
external_code_coverage:
enabled: true
timeout: 300
filter:
excluded_paths: ["tests", "vendor"]
php_code_sniffer:
enabled: true
config:
standard: PSR2
filter:
paths: ["src/*", "tests/*"]
excluded_paths: []
php_cpd:
enabled: true
excluded_dirs: ["tests", "vendor"]
php_cs_fixer:
enabled: true
config:
level: all
filter:
paths: ["src/*", "tests/*"]
php_loc:
enabled: true
excluded_dirs: ["tests", "vendor"]
php_mess_detector:
enabled: true
config:
ruleset: phpmd.xml.dist
design_rules: { eval_expression: false }
filter:
paths: ["src/*"]
php_pdepend:
enabled: true
excluded_dirs: ["tests", "vendor"]
php_analyzer:
enabled: true
filter:
paths: ["src/*", "tests/*"]
sensiolabs_security_checker: true

21
vendor/phpdocumentor/graphviz/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Mike van Riel
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.

35
vendor/phpdocumentor/graphviz/Makefile vendored Normal file
View File

@@ -0,0 +1,35 @@
.PHONY: install-phive
install-phive:
mkdir tools; \
wget -O tools/phive.phar https://phar.io/releases/phive.phar; \
wget -O tools/phive.phar.asc https://phar.io/releases/phive.phar.asc; \
gpg --keyserver pool.sks-keyservers.net --recv-keys 0x9D8A98B29B2D5D79; \
gpg --verify tools/phive.phar.asc tools/phive.phar; \
chmod +x tools/phive.phar
.PHONY: setup
setup: install-phive
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phar-ga:latest php tools/phive.phar install --force-accept-unsigned
.PHONY: phpcbf
phpcbf:
docker run -it --rm -v${CURDIR}:/opt/project -w /opt/project phpdoc/phpcs-ga:latest phpcbf ${ARGS}
.PHONY: phpcs
phpcs:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phpcs-ga:latest -d memory_limit=1024M -s
.PHONY: phpstan
phpstan:
docker run -it --rm -v${CURDIR}:/opt/project -w /opt/project phpdoc/phpstan-ga:latest analyse src tests --configuration phpstan.neon ${ARGS}
.PHONY: psalm
psalm:
docker run -it --rm -v${CURDIR}:/data -w /data php:7.3 vendor/bin/psalm.phar
.PHONY: test
test:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.3 vendor/bin/phpunit
.PHONY: pre-commit-test
pre-commit-test: phpcs phpstan psalm test

27
vendor/phpdocumentor/graphviz/README.md vendored Normal file
View File

@@ -0,0 +1,27 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Travis Status](https://img.shields.io/travis/phpDocumentor/GraphViz.svg?label=Linux)](https://travis-ci.org/phpDocumentor/GraphViz)
[![Appveyor Status](https://img.shields.io/appveyor/ci/phpDocumentor/GraphViz.svg?label=Windows)](https://ci.appveyor.com/project/phpDocumentor/GraphViz/branch/master)
[![Coveralls Coverage](https://img.shields.io/coveralls/github/phpDocumentor/GraphViz.svg)](https://coveralls.io/github/phpDocumentor/GraphViz?branch=master)
[![Scrutinizer Code Coverage](https://img.shields.io/scrutinizer/coverage/g/phpDocumentor/GraphViz.svg)](https://scrutinizer-ci.com/g/phpDocumentor/GraphViz/?branch=master)
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/phpDocumentor/GraphViz.svg)](https://scrutinizer-ci.com/g/phpDocumentor/GraphViz/?branch=master)
[![Stable Version](https://img.shields.io/packagist/v/phpDocumentor/GraphViz.svg)](https://packagist.org/packages/phpDocumentor/GraphViz)
[![Unstable Version](https://img.shields.io/packagist/vpre/phpDocumentor/GraphViz.svg)](https://packagist.org/packages/phpDocumentor/GraphViz)
GraphViz
========
GraphViz is a library meant for generating .dot files for GraphViz with a
fluent interface.
### PHPStan extension
This library contains a number of magic methods to set attributes on `Node`, `Graph` and `Edge`
this will result in errors when using the library with checks by PHPStan. For your convenience this
library provides an phpStan extension so your code can be checked correctly by phpstan.
```
includes:
- vendor/phpdocumentor/graphviz/extension.neon
```

View File

@@ -0,0 +1,39 @@
{
"name": "phpdocumentor/graphviz",
"description": "Wrapper for Graphviz",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Mike van Riel",
"email": "mike.vanriel@naenius.com"
}
],
"require": {
"php": "^7.2 || ^8.0"
},
"autoload": {
"psr-4": {
"phpDocumentor\\GraphViz\\": "src/phpDocumentor/GraphViz",
"phpDocumentor\\GraphViz\\PHPStan\\": "./src/phpDocumentor/PHPStan"
}
},
"autoload-dev": {
"psr-4": {
"phpDocumentor\\GraphViz\\Test\\": "./tests/phpDocumentor/GraphViz/Test",
"phpDocumentor\\GraphViz\\PHPStan\\": "./tests/phpDocumentor/PHPStan"
}
},
"require-dev": {
"phpunit/phpunit": "^8.2 || ^9.2",
"mockery/mockery": "^1.2",
"phpstan/phpstan": "^0.12",
"ext-simplexml": "*",
"psalm/phar": "^4.15"
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
}
}

View File

@@ -0,0 +1,9 @@
services:
-
class: phpDocumentor\GraphViz\PHPStan\MethodReflectionExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: phpDocumentor\GraphViz\PHPStan\GraphNodeReflectionExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension

View File

@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<ruleset name="phpDocumentor">
<description>The coding standard for phpDocumentor.</description>
<file>src</file>
<file>tests</file>
<arg value="p"/>
<rule ref="PSR2" />
<rule ref="Doctrine">
<exclude name="SlevomatCodingStandard.TypeHints.UselessConstantTypeHint.UselessDocComment" />
</rule>
<rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps">
<exclude-pattern>tests/phpDocumentor/GraphViz/Test/GraphTest\.php</exclude-pattern>
</rule>
<rule ref="Generic.Formatting.SpaceAfterNot">
<properties>
<property name="spacing" value="0" />
</properties>
</rule>
</ruleset>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
name="ProxyManager rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"
>
<rule ref="rulesets/codesize.xml"/>
<rule ref="rulesets/unusedcode.xml"/>
<rule ref="rulesets/design.xml">
<!-- eval is needed to generate runtime classes -->
<exclude name="EvalExpression"/>
</rule>
<rule ref="rulesets/naming.xml">
<exclude name="LongVariable"/>
</rule>
<rule ref="rulesets/naming.xml/LongVariable">
<properties>
<property name="minimum">40</property>
</properties>
</rule>
</ruleset>

View File

@@ -0,0 +1,12 @@
includes:
- /composer/vendor/phpstan/phpstan-mockery/extension.neon
- ./extension.neon
parameters:
level: max
ignoreErrors:
#We have some runtime protection that needs to be tested. Ignore these errors
- '#Call to an undefined method phpDocumentor\\GraphViz\\Edge::someNonExcistingMethod\(\)\.#'
- '#Call to an undefined method phpDocumentor\\GraphViz\\Graph::MyMethod\(\)\.#'
- '#Call to an undefined method phpDocumentor\\GraphViz\\Graph::getNotExisting\(\)\.#'
- '#Call to an undefined method phpDocumentor\\GraphViz\\Node::someNonExistingMethod\(\)\.#'

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="true"
>
<testsuites>
<testsuite name="Graphviz suite">
<directory>./tests/phpDocumentor/GraphViz</directory>
</testsuite>
<testsuite name="PHPStan suite">
<directory>./tests/phpDocumentor/PHPStan</directory>
</testsuite>
</testsuites>
<logging>
<log type="coverage-clover" target="build/logs/clover.xml" />
<log type="junit" target="build/logs/junit.xml" />
</logging>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

26
vendor/phpdocumentor/graphviz/psalm.xml vendored Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0"?>
<psalm
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorLevel="1"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<MissingDependency errorLevel="info" />
<MixedArgumentTypeCoercion>
<errorLevel type="suppress">
<file name="src/phpDocumentor/GraphViz/Graph.php" />
</errorLevel>
</MixedArgumentTypeCoercion>
</issueHandlers>
</psalm>

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz;
use function addslashes;
use function preg_replace;
use function strstr;
/**
* Class representing a single GraphViz attribute.
*
* @link http://phpdoc.org
*/
class Attribute
{
/** @var string The name of this attribute */
protected $key = '';
/** @var string The value of this attribute */
protected $value = '';
/**
* Creating a new attribute.
*
* @param string $key Id for the new attribute.
* @param string $value Value for this attribute,
*/
public function __construct(string $key, string $value)
{
$this->key = $key;
$this->value = $value;
}
/**
* Sets the key for this attribute.
*
* @param string $key The new name of this attribute.
*/
public function setKey(string $key): self
{
$this->key = $key;
return $this;
}
/**
* Returns the name for this attribute.
*/
public function getKey(): string
{
return $this->key;
}
/**
* Sets the value for this attribute.
*
* @param string $value The new value.
*/
public function setValue(string $value): self
{
$this->value = $value;
return $this;
}
/**
* Returns the value for this attribute.
*/
public function getValue(): string
{
return $this->value;
}
/**
* Returns the attribute definition as is requested by GraphViz.
*/
public function __toString(): string
{
$key = $this->getKey();
if ($key === 'url') {
$key = 'URL';
}
$value = $this->getValue();
if ($this->isValueContainingSpecials()) {
$value = '"' . $this->encodeSpecials() . '"';
} elseif (!$this->isValueInHtml()) {
$value = '"' . addslashes($value) . '"';
}
return $key . '=' . $value;
}
/**
* Returns whether the value contains HTML.
*/
public function isValueInHtml(): bool
{
$value = $this->getValue();
return isset($value[0]) && ($value[0] === '<');
}
/**
* Checks whether the value contains any any special characters needing escaping.
*/
public function isValueContainingSpecials(): bool
{
return strstr($this->getValue(), '\\') !== false;
}
/**
* Encode special characters so the escape sequences aren't removed
*
* @see http://www.graphviz.org/doc/info/attrs.html#k:escString
*/
protected function encodeSpecials(): string
{
$value = $this->getValue();
$regex = '(\'|"|\\x00|\\\\(?![\\\\NGETHLnlr]))';
return (string) preg_replace($regex, '\\\\$0', $value);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz;
use function sprintf;
class AttributeNotFound extends Exception
{
public function __construct(string $name)
{
parent::__construct(sprintf('Attribute with name "%s" was not found.', $name));
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz;
use function array_key_exists;
trait Attributes
{
/** @var Attribute[] */
protected $attributes = [];
public function setAttribute(string $name, string $value): self
{
$this->attributes[$name] = new Attribute($name, $value);
return $this;
}
/**
* @throws AttributeNotFound
*/
public function getAttribute(string $name): Attribute
{
if (!array_key_exists($name, $this->attributes)) {
throw new AttributeNotFound($name);
}
return $this->attributes[$name];
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz;
use function addslashes;
use function implode;
use function strtolower;
use function substr;
/**
* Class representing an edge (arrow, line).
*
* @link http://phpdoc.org
*/
class Edge
{
use Attributes;
/** @var Node Node from where to link */
private $from;
/** @var Node Node where to to link */
private $to;
/**
* Creates a new Edge / Link between the given nodes.
*
* @param Node $from Starting node to create an Edge from.
* @param Node $to Destination node where to create and
* edge to.
*/
public function __construct(Node $from, Node $to)
{
$this->from = $from;
$this->to = $to;
}
/**
* Factory method used to assist with fluent interface handling.
*
* See the examples for more details.
*
* @param Node $from Starting node to create an Edge from.
* @param Node $to Destination node where to create and
* edge to.
*/
public static function create(Node $from, Node $to): self
{
return new self($from, $to);
}
/**
* Returns the source Node for this Edge.
*/
public function getFrom(): Node
{
return $this->from;
}
/**
* Returns the destination Node for this Edge.
*/
public function getTo(): Node
{
return $this->to;
}
/**
* Magic method to provide a getter/setter to add attributes on the edge.
*
* Using this method we make sure that we support any attribute without too
* much hassle. If the name for this method does not start with get or set
* we return null.
*
* Set methods return this graph (fluent interface) whilst get methods
* return the attribute value.
*
* @param string $name name of the invoked method, expect it to be
* setX or getX.
* @param mixed[] $arguments Arguments for the setter, only 1 is expected: value
*
* @return Attribute|Edge|null
*
* @throws AttributeNotFound
*/
public function __call(string $name, array $arguments)
{
$key = strtolower(substr($name, 3));
if (strtolower(substr($name, 0, 3)) === 'set') {
return $this->setAttribute($key, (string) $arguments[0]);
}
if (strtolower(substr($name, 0, 3)) === 'get') {
return $this->getAttribute($key);
}
return null;
}
/**
* Returns the edge definition as is requested by GraphViz.
*/
public function __toString(): string
{
$attributes = [];
foreach ($this->attributes as $value) {
$attributes[] = (string) $value;
}
$attributes = implode("\n", $attributes);
$fromName = addslashes($this->getFrom()->getName());
$toName = addslashes($this->getTo()->getName());
return <<<DOT
"${fromName}" -> "${toName}" [
${attributes}
]
DOT;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz;
/**
* Class representing an exception in this GraphViz component.
*/
class Exception extends \Exception
{
}

View File

@@ -0,0 +1,397 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz;
use InvalidArgumentException;
use function array_merge;
use function escapeshellarg;
use function exec;
use function file_put_contents;
use function implode;
use function in_array;
use function realpath;
use function strtolower;
use function substr;
use function sys_get_temp_dir;
use function tempnam;
use function unlink;
use const DIRECTORY_SEPARATOR;
use const PHP_EOL;
/**
* Class representing a graph; this may be a main graph but also a subgraph.
*
* In case of a subgraph:
* When the name of the subgraph is prefixed with _cluster_ then the contents
* of this graph will be grouped and a border will be added. Otherwise it is
* used as logical container to place defaults in.
*
* @method Graph setRankSep(string $rankSep)
* @method Graph setCenter(string $center)
* @method Graph setRank(string $rank)
* @method Graph setRankDir(string $rankDir)
* @method Graph setSplines(string $splines)
* @method Graph setConcentrate(string $concentrate)
*/
class Graph
{
use Attributes;
/** @var string Name of this graph */
protected $name = 'G';
/** @var string Type of this graph; may be digraph, graph or subgraph */
protected $type = 'digraph';
/** @var bool If the graph is strict then multiple edges are not allowed between the same pairs of nodes */
protected $strict = false;
/** @var Graph[] A list of subgraphs for this Graph */
protected $graphs = [];
/** @var Node[] A list of nodes for this Graph */
protected $nodes = [];
/** @var Edge[] A list of edges / arrows for this Graph */
protected $edges = [];
/** @var string The path to execute dot from */
protected $path = '';
/**
* Factory method to instantiate a Graph so that you can use fluent coding
* to chain everything.
*
* @param string $name The name for this graph.
* @param bool $directional Whether this is a directed or undirected graph.
*
* @return Graph
*/
public static function create(string $name = 'G', bool $directional = true): self
{
$graph = new self();
$graph
->setName($name)
->setType($directional ? 'digraph' : 'graph');
return $graph;
}
/**
* Sets the path for the execution. Only needed if it is not in the PATH env.
*
* @param string $path The path to execute dot from
*/
public function setPath(string $path): self
{
$realpath = realpath($path);
if ($path && $path === $realpath) {
$this->path = $path . DIRECTORY_SEPARATOR;
}
return $this;
}
/**
* Sets the name for this graph.
*
* If this is a subgraph you can prefix the name with _cluster_ to group all
* contained nodes and add a border.
*
* @param string $name The new name for this graph.
*/
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* Returns the name for this Graph.
*/
public function getName(): string
{
return $this->name;
}
/**
* Sets the type for this graph.
*
* @param string $type Must be either "digraph", "graph" or "subgraph".
*
* @throws InvalidArgumentException If $type is not "digraph", "graph" or
* "subgraph".
*/
public function setType(string $type): self
{
if (!in_array($type, ['digraph', 'graph', 'subgraph'], true)) {
throw new InvalidArgumentException(
'The type for a graph must be either "digraph", "graph" or '
. '"subgraph"'
);
}
$this->type = $type;
return $this;
}
/**
* Returns the type of this Graph.
*/
public function getType(): string
{
return $this->type;
}
/**
* Set if the Graph should be strict. If the graph is strict then
* multiple edges are not allowed between the same pairs of nodes
*/
public function setStrict(bool $isStrict): self
{
$this->strict = $isStrict;
return $this;
}
public function isStrict(): bool
{
return $this->strict;
}
/**
* Magic method to provide a getter/setter to add attributes on the Graph.
*
* Using this method we make sure that we support any attribute without
* too much hassle. If the name for this method does not start with get
* or set we return null.
*
* Set methods return this graph (fluent interface) whilst get methods
* return the attribute value.
*
* @param string $name Name of the method including get/set
* @param mixed[] $arguments The arguments, should be 1: the value
*
* @return Attribute|Graph|null
*
* @throws AttributeNotFound
*/
public function __call(string $name, array $arguments)
{
$key = strtolower(substr($name, 3));
if (strtolower(substr($name, 0, 3)) === 'set') {
return $this->setAttribute($key, (string) $arguments[0]);
}
if (strtolower(substr($name, 0, 3)) === 'get') {
return $this->getAttribute($key);
}
return null;
}
/**
* Adds a subgraph to this graph; automatically changes the type to subgraph.
*
* Please note that an index is maintained using the name of the subgraph.
* Thus if you have 2 subgraphs with the same name that the first will be
* overwritten by the latter.
*
* @see Graph::create()
*
* @param Graph $graph The graph to add onto this graph as
* subgraph.
*/
public function addGraph(self $graph): self
{
$graph->setType('subgraph');
$this->graphs[$graph->getName()] = $graph;
return $this;
}
/**
* Checks whether a graph with a certain name already exists.
*
* @param string $name Name of the graph to find.
*/
public function hasGraph(string $name): bool
{
return isset($this->graphs[$name]);
}
/**
* Returns the subgraph with a given name.
*
* @param string $name Name of the requested graph.
*/
public function getGraph(string $name): self
{
return $this->graphs[$name];
}
/**
* Sets a node in the $nodes array; uses the name of the node as index.
*
* Nodes can be retrieved by retrieving the property with the same name.
* Thus 'node1' can be retrieved by invoking: $graph->node1
*
* @see Node::create()
*
* @param Node $node The node to set onto this Graph.
*/
public function setNode(Node $node): self
{
$this->nodes[$node->getName()] = $node;
return $this;
}
/**
* Finds a node in this graph or any of its subgraphs.
*
* @param string $name Name of the node to find.
*/
public function findNode(string $name): ?Node
{
if (isset($this->nodes[$name])) {
return $this->nodes[$name];
}
foreach ($this->graphs as $graph) {
$node = $graph->findNode($name);
if ($node) {
return $node;
}
}
return null;
}
/**
* Sets a node using a custom name.
*
* @see Graph::setNode()
*
* @param string $name Name of the node.
* @param Node $value Node to set on the given name.
*/
public function __set(string $name, Node $value): void
{
$this->nodes[$name] = $value;
}
/**
* Returns the requested node by its name.
*
* @see Graph::setNode()
*
* @param string $name The name of the node to retrieve.
*/
public function __get(string $name): ?Node
{
return $this->nodes[$name] ?? null;
}
/**
* Links two nodes to eachother and registers the Edge onto this graph.
*
* @see Edge::create()
*
* @param Edge $edge The link between two classes.
*/
public function link(Edge $edge): self
{
$this->edges[] = $edge;
return $this;
}
/**
* Exports this graph to a generated image.
*
* This is the only method that actually requires GraphViz.
*
* @link http://www.graphviz.org/content/output-formats
* @uses GraphViz/dot
*
* @param string $type The type to export to; see the link above for a
* list of supported types.
* @param string $filename The path to write to.
*
* @throws Exception If an error occurred in GraphViz.
*/
public function export(string $type, string $filename): self
{
$type = escapeshellarg($type);
$filename = escapeshellarg($filename);
// write the dot file to a temporary file
$tmpfile = (string) tempnam(sys_get_temp_dir(), 'gvz');
file_put_contents($tmpfile, (string) $this);
// escape the temp file for use as argument
$tmpfileArg = escapeshellarg($tmpfile);
// create the dot output
$output = [];
$code = 0;
exec($this->path . "dot -T${type} -o${filename} < ${tmpfileArg} 2>&1", $output, $code);
unlink($tmpfile);
if ($code !== 0) {
throw new Exception(
'An error occurred while creating the graph; GraphViz returned: '
. implode(PHP_EOL, $output)
);
}
return $this;
}
/**
* Generates a DOT file for use with GraphViz.
*
* GraphViz is not used in this method; it is safe to call it even without
* GraphViz installed.
*/
public function __toString(): string
{
$elements = array_merge(
$this->graphs,
$this->attributes,
$this->edges,
$this->nodes
);
$attributes = [];
foreach ($elements as $value) {
$attributes[] = (string) $value;
}
$attributes = implode(PHP_EOL, $attributes);
$strict = ($this->isStrict() ? 'strict ' : '');
return <<<DOT
{$strict}{$this->getType()} "{$this->getName()}" {
${attributes}
}
DOT;
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz;
use function addslashes;
use function implode;
use function strtolower;
use function substr;
/**
* Class representing a node / element in a graph.
*
* @link http://phpdoc.org
*
* @method void setLabel(string $name) Sets the label for this node.
*/
class Node
{
use Attributes;
/** @var string Name for this node */
protected $name = '';
/**
* Creates a new node with name and optional label.
*
* @param string $name Name of the new node.
* @param string|null $label Optional label text.
*/
public function __construct(string $name, ?string $label = null)
{
$this->setName($name);
if ($label === null) {
return;
}
$this->setLabel($label);
}
/**
* Factory method used to assist with fluent interface handling.
*
* See the examples for more details.
*
* @param string $name Name of the new node.
* @param string|null $label Optional label text.
*/
public static function create(string $name, ?string $label = null): self
{
return new self($name, $label);
}
/**
* Sets the name for this node.
*
* Not to confuse with the label.
*
* @param string $name Name for this node.
*/
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* Returns the name for this node.
*/
public function getName(): string
{
return $this->name;
}
/**
* Magic method to provide a getter/setter to add attributes on the Node.
*
* Using this method we make sure that we support any attribute without
* too much hassle. If the name for this method does not start with get or
* set we return null.
*
* Set methods return this graph (fluent interface) whilst get methods
* return the attribute value.
*
* @param string $name Method name; either getX or setX is expected.
* @param mixed[] $arguments List of arguments; only 1 is expected for setX.
*
* @return Attribute|Node|null
*
* @throws AttributeNotFound
*/
public function __call(string $name, array $arguments)
{
$key = strtolower(substr($name, 3));
if (strtolower(substr($name, 0, 3)) === 'set') {
return $this->setAttribute($key, (string) $arguments[0]);
}
if (strtolower(substr($name, 0, 3)) === 'get') {
return $this->getAttribute($key);
}
return null;
}
/**
* Returns the node definition as is requested by GraphViz.
*/
public function __toString(): string
{
$attributes = [];
foreach ($this->attributes as $value) {
$attributes[] = (string) $value;
}
$attributes = implode("\n", $attributes);
$name = addslashes($this->getName());
return <<<DOT
"{$name}" [
${attributes}
]
DOT;
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz\PHPStan;
use phpDocumentor\GraphViz\Attribute;
use phpDocumentor\GraphViz\AttributeNotFound;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
final class AttributeGetterMethodReflection implements MethodReflection
{
/** @var ClassReflection */
private $classReflection;
/** @var string */
private $name;
public function __construct(ClassReflection $classReflection, string $name)
{
$this->classReflection = $classReflection;
$this->name = $name;
}
public function getDeclaringClass(): ClassReflection
{
return $this->classReflection;
}
public function isStatic(): bool
{
return false;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function getName(): string
{
return $this->name;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/**
* @return ParametersAcceptor[]
*/
public function getVariants(): array
{
return [
new FunctionVariant(
TemplateTypeMap::createEmpty(),
null,
[],
false,
new ObjectType(Attribute::class)
),
];
}
public function getDocComment(): ?string
{
return null;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): ?Type
{
return new ObjectType(AttributeNotFound::class);
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz\PHPStan;
use phpDocumentor\GraphViz\AttributeNotFound;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
final class AttributeSetterMethodReflection implements MethodReflection
{
/** @var ClassReflection */
private $classReflection;
/** @var string */
private $name;
/** @var Type */
private $attributeType;
public function __construct(ClassReflection $classReflection, string $name, Type $attributeType)
{
$this->classReflection = $classReflection;
$this->name = $name;
$this->attributeType = $attributeType;
}
public function getDeclaringClass(): ClassReflection
{
return $this->classReflection;
}
public function isStatic(): bool
{
return false;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function getName(): string
{
return $this->name;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/**
* @return ParametersAcceptor[]
*/
public function getVariants(): array
{
return [
new FunctionVariant(
TemplateTypeMap::createEmpty(),
TemplateTypeMap::createEmpty(),
[
new DummyParameter('value', $this->attributeType, false, null, false, null),
],
false,
new ObjectType($this->classReflection->getName())
),
];
}
public function getDocComment(): ?string
{
return null;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): ?Type
{
return new ObjectType(AttributeNotFound::class);
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createYes();
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz\PHPStan;
use phpDocumentor\GraphViz\Graph;
use phpDocumentor\GraphViz\Node;
use PHPStan\Reflection\Annotations\AnnotationPropertyReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Type\ObjectType;
final class GraphNodeReflectionExtension implements PropertiesClassReflectionExtension
{
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
return $classReflection->getName() === Graph::class;
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
return new AnnotationPropertyReflection(
$classReflection,
new ObjectType(Node::class),
true,
true
);
}
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz\PHPStan;
use InvalidArgumentException;
use phpDocumentor\GraphViz\Edge;
use phpDocumentor\GraphViz\Graph;
use phpDocumentor\GraphViz\Node;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Type\BooleanType;
use PHPStan\Type\FloatType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use RuntimeException;
use SimpleXMLElement;
use function array_key_exists;
use function array_map;
use function file_get_contents;
use function in_array;
use function simplexml_load_string;
use function sprintf;
use function str_replace;
use function stripos;
use function strtolower;
use function substr;
final class MethodReflectionExtension implements MethodsClassReflectionExtension
{
private const SUPPORTED_CLASSES = [
Node::class => 'node',
Graph::class => 'graph',
Edge::class => 'edge',
];
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if (!array_key_exists($classReflection->getName(), self::SUPPORTED_CLASSES)) {
return false;
}
$methods = $this->getMethodsFromSpec(self::SUPPORTED_CLASSES[$classReflection->getName()]);
$expectedAttribute = $this->getAttributeFromMethodName($methodName);
return in_array($expectedAttribute, $methods, true);
}
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
if (stripos($methodName, 'get') === 0) {
return new AttributeGetterMethodReflection($classReflection, $methodName);
}
$attributeName = $this->getAttributeFromMethodName($methodName);
return new AttributeSetterMethodReflection(
$classReflection,
$methodName,
$this->getAttributeInputType($attributeName)
);
}
/**
* @return string[]
*/
private function getMethodsFromSpec(string $className): array
{
$simpleXml = $this->getAttributesXmlDoc();
$elements = $simpleXml->xpath(sprintf("xsd:complexType[@name='%s']/xsd:attribute", $className));
if ($elements === false) {
throw new InvalidArgumentException(
sprintf('Class "%s" does not exist in Graphviz spec', $className)
);
}
return array_map(
static function (SimpleXMLElement $attribute): string {
return strtolower((string) $attribute['ref']);
},
$elements
);
}
private function getAttributeInputType(string $ref): Type
{
$simpleXml = $this->getAttributesXmlDoc();
$attributes = $simpleXml->xpath(sprintf("xsd:attribute[@name='%s']", $ref));
if (empty($attributes)) {
return new StringType();
}
$type = $attributes[0]['type'];
$type = str_replace('xsd:', '', (string) $type);
switch ($type) {
case 'boolean':
return new BooleanType();
case 'decimal':
return new FloatType();
case 'string':
default:
return new StringType();
}
}
private function getAttributesXmlDoc(): SimpleXMLElement
{
$fileContent = file_get_contents(__DIR__ . '/assets/attributes.xml');
if ($fileContent === false) {
throw new RuntimeException('Cannot read attributes spec');
}
$xml = simplexml_load_string($fileContent);
if ($xml === false) {
throw new RuntimeException('Cannot read attributes spec');
}
return $xml;
}
private function getAttributeFromMethodName(string $methodName): string
{
return strtolower(substr($methodName, 3));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz\Test;
use phpDocumentor\GraphViz\Attribute;
use PHPUnit\Framework\TestCase;
/**
* Test for the the class representing a GraphViz attribute.
*/
class AttributeTest extends TestCase
{
/** @var Attribute */
protected $fixture = null;
/**
* Initializes the fixture for this test.
*/
protected function setUp(): void
{
$this->fixture = new Attribute('a', '1');
}
/**
* Tests the construct method
*
* @covers \phpDocumentor\GraphViz\Attribute::__construct
* @returnn void
*/
public function testConstruct(): void
{
$fixture = new Attribute('MyKey', 'MyValue');
$this->assertInstanceOf(
Attribute::class,
$fixture
);
$this->assertSame('MyKey', $fixture->getKey());
$this->assertSame('MyValue', $fixture->getValue());
}
/**
* Tests the getting and setting of the key.
*
* @covers \phpDocumentor\GraphViz\Attribute::getKey
* @covers \phpDocumentor\GraphViz\Attribute::setKey
*/
public function testKey(): void
{
$this->assertSame(
$this->fixture->getKey(),
'a',
'Expecting the key to match the initial state'
);
$this->assertSame(
$this->fixture,
$this->fixture->setKey('b'),
'Expecting a fluent interface'
);
$this->assertSame(
$this->fixture->getKey(),
'b',
'Expecting the key to contain the new value'
);
}
/**
* Tests the getting and setting of the value.
*
* @covers \phpDocumentor\GraphViz\Attribute::getValue
* @covers \phpDocumentor\GraphViz\Attribute::setValue
*/
public function testValue(): void
{
$this->assertSame(
$this->fixture->getValue(),
'1',
'Expecting the value to match the initial state'
);
$this->assertSame(
$this->fixture,
$this->fixture->setValue('2'),
'Expecting a fluent interface'
);
$this->assertSame(
$this->fixture->getValue(),
'2',
'Expecting the value to contain the new value'
);
}
/**
* Tests whether a string starting with a < is recognized as HTML.
*
* @covers \phpDocumentor\GraphViz\Attribute::isValueInHtml
*/
public function testIsValueInHtml(): void
{
$this->fixture->setValue('a');
$this->assertFalse(
$this->fixture->isValueInHtml(),
'Expected value to not be a HTML code'
);
$this->fixture->setValue('<a>test</a>');
$this->assertTrue(
$this->fixture->isValueInHtml(),
'Expected value to be recognized as a HTML code'
);
}
/**
* Tests whether the toString provides a valid GraphViz attribute string.
*
* @covers \phpDocumentor\GraphViz\Attribute::__toString
*/
public function testToString(): void
{
$this->fixture = new Attribute('a', 'b');
$this->assertSame(
'a="b"',
(string) $this->fixture,
'Strings should be surrounded with quotes'
);
$this->fixture->setValue('a"a');
$this->assertSame(
'a="a\"a"',
(string) $this->fixture,
'Strings should be surrounded with quotes'
);
$this->fixture->setKey('url');
$this->assertSame(
'URL="a\"a"',
(string) $this->fixture,
'The key named URL must be uppercased'
);
$this->fixture->setValue('<a>test</a>');
$this->assertSame(
'URL=<a>test</a>',
(string) $this->fixture,
'HTML strings should not be surrounded with quotes'
);
}
/**
* Tests whether the toString provides a valid GraphViz attribute string.
*
* @covers \phpDocumentor\GraphViz\Attribute::__toString
* @covers \phpDocumentor\GraphViz\Attribute::encodeSpecials
*/
public function testToStringWithSpecials(): void
{
$this->fixture = new Attribute('a', 'b');
$this->fixture->setValue('a\la');
$this->assertSame(
'a="a\la"',
(string) $this->fixture,
'Specials should not be escaped'
);
$this->fixture->setValue('a\l"a');
$this->assertSame(
'a="a\l\"a"',
(string) $this->fixture,
'Specials should not be escaped, but quotes should'
);
$this->fixture->setValue('a\\\\l"a');
$this->assertSame(
'a="a\\\\l\"a"',
(string) $this->fixture,
'Double backslashes should stay the same'
);
}
/**
* Tests whether the isValueContainingSpecials function
*
* @covers \phpDocumentor\GraphViz\Attribute::isValueContainingSpecials
*/
public function testIsValueContainingSpecials(): void
{
$this->fixture->setValue('+ name : string\l+ home_country : string\l');
$this->assertTrue($this->fixture->isValueContainingSpecials());
$this->fixture->setValue('+ ship(): boolean');
$this->assertFalse($this->fixture->isValueContainingSpecials());
}
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\GraphViz\Test;
use Mockery as m;
use phpDocumentor\GraphViz\AttributeNotFound;
use phpDocumentor\GraphViz\Edge;
use phpDocumentor\GraphViz\Node;
use PHPUnit\Framework\TestCase;
/**
* Test for the the class representing a GraphViz edge (vertex).
*/
class EdgeTest extends TestCase
{
/**
* Tears down the fixture, for example, closes a network connection.
* This method is called after a test is executed.
*/
protected function tearDown(): void
{
m::close();
}
/**
* Tests the construct method
*
* @covers \phpDocumentor\GraphViz\Edge::__construct
*/
public function testConstruct(): void
{
$fromNode = m::mock(Node::class);
$toNode = m::mock(Node::class);
$fixture = new Edge($fromNode, $toNode);
$this->assertInstanceOf(
Edge::class,
$fixture
);
$this->assertSame(
$fromNode,
$fixture->getFrom()
);
$this->assertSame(
$toNode,
$fixture->getTo()
);
}
/**
* Tests the create method
*
* @covers \phpDocumentor\GraphViz\Edge::create
*/
public function testCreate(): void
{
$this->assertInstanceOf(
Edge::class,
Edge::create(new Node('from'), new Node('to'))
);
}
/**
* Tests whether the getFrom method returns the same node as passed
* in the create method
*
* @covers \phpDocumentor\GraphViz\Edge::getFrom
*/
public function testGetFrom(): void
{
$from = new Node('from');
$edge = Edge::create($from, new Node('to'));
$this->assertSame($from, $edge->getFrom());
}
/**
* Tests the getTo method returns the same node as passed
* in the create method
*
* @covers \phpDocumentor\GraphViz\Edge::getTo
*/
public function testGetTo(): void
{
$to = new Node('to');
$edge = Edge::create(new Node('from'), $to);
$this->assertSame($to, $edge->getTo());
}
/**
* Tests the magic __call method, to work as described, return the object
* instance for a setX method, return the value for an getX method, and null
* for the remaining method calls
*
* @covers \phpDocumentor\GraphViz\Edge::__call
* @covers \phpDocumentor\GraphViz\Edge::setAttribute
* @covers \phpDocumentor\GraphViz\Edge::getAttribute
*/
public function testCall(): void
{
$label = 'my label';
$fixture = new Edge(new Node('from'), new Node('to'));
$this->assertInstanceOf(Edge::class, $fixture->setLabel($label));
$this->assertSame($label, $fixture->getLabel()->getValue());
$this->assertNull($fixture->someNonExcistingMethod());
}
/**
* @covers \phpDocumentor\GraphViz\Edge::getAttribute
* @covers \phpDocumentor\GraphViz\AttributeNotFound::__construct
*/
public function testGetNonExistingAttributeThrowsAttributeNotFound(): void
{
$fixture = new Edge(new Node('from'), new Node('to'));
$this->expectException(AttributeNotFound::class);
$this->expectExceptionMessage('Attribute with name "label" was not found');
$fixture->getLabel();
}
/**
* Tests whether the magic __toString method returns a well formatted string
* as specified in the DOT standard
*
* @covers \phpDocumentor\GraphViz\Edge::__toString
*/
public function testToString(): void
{
$fixture = new Edge(new Node('from'), new Node('to'));
$fixture->setLabel('MyLabel');
$fixture->setWeight(45);
$dot = <<<DOT
"from" -> "to" [
label="MyLabel"
weight="45"
]
DOT;
$this->assertSame($dot, (string) $fixture);
}
}

Some files were not shown because too many files have changed in this diff Show More