First commit

This commit is contained in:
Pierre Hubert
2016-11-19 12:08:12 +01:00
commit 990540b2b9
4706 changed files with 931207 additions and 0 deletions

View File

@ -0,0 +1,50 @@
<?php
namespace RocketTheme\Toolbox\ResourceLocator;
/**
* Implements recursive iterator over filesystem.
*
* @package RocketTheme\Toolbox\ResourceLocator
* @author RocketTheme
* @license MIT
*/
class RecursiveUniformResourceIterator extends UniformResourceIterator implements \SeekableIterator, \RecursiveIterator
{
protected $subPath;
public function getChildren()
{
$subPath = $this->getSubPathName();
return (new RecursiveUniformResourceIterator($this->getUrl(), $this->flags, $this->locator))->setSubPath($subPath);
}
public function hasChildren($allow_links = null)
{
$allow_links = (bool) ($allow_links !== null ? $allow_links : $this->flags & \FilesystemIterator::FOLLOW_SYMLINKS);
return $this->iterator && $this->isDir() && !$this->isDot() && ($allow_links || !$this->isLink());
}
public function getSubPath()
{
return $this->subPath;
}
public function getSubPathName()
{
return ($this->subPath ? $this->subPath . '/' : '') . $this->getFilename();
}
/**
* @param $path
* @return $this
* @internal
*/
public function setSubPath($path)
{
$this->subPath = $path;
return $this;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace RocketTheme\Toolbox\ResourceLocator;
/**
* Defines ResourceLocatorInterface.
*
* @package RocketTheme\Toolbox\ResourceLocator
* @author RocketTheme
* @license MIT
*/
interface ResourceLocatorInterface
{
/**
* Alias for findResource()
*
* @param $uri
* @return string|bool
*/
public function __invoke($uri);
/**
* Returns true if uri is resolvable by using locator.
*
* @param string $uri
* @return bool
*/
public function isStream($uri);
/**
* @param string $uri
* @param bool $absolute
* @param bool $first
* @return string|bool
*/
public function findResource($uri, $absolute = true, $first = false);
/**
* @param string $uri
* @param bool $absolute
* @param bool $all
* @return array
*/
public function findResources($uri, $absolute = true, $all = false);
}

View File

@ -0,0 +1,250 @@
<?php
namespace RocketTheme\Toolbox\ResourceLocator;
use FilesystemIterator;
/**
* Implements FilesystemIterator for uniform resource locator.
*
* @package RocketTheme\Toolbox\ResourceLocator
* @author RocketTheme
* @license MIT
*/
class UniformResourceIterator extends FilesystemIterator
{
/**
* @var FilesystemIterator
*/
protected $iterator;
/**
* @var array
*/
protected $found;
/**
* @var array
*/
protected $stack;
/**
* @var string
*/
protected $path;
/**
* @var int
*/
protected $flags;
/**
* @var UniformResourceLocator
*/
protected $locator;
public function __construct($path, $flags = null, UniformResourceLocator $locator = null)
{
if (!$locator) {
throw new \BadMethodCallException('Use $locator->getIterator() instead');
}
$this->path = $path;
$this->setFlags($flags);
$this->locator = $locator;
$this->rewind();
}
public function current()
{
if ($this->flags & static::CURRENT_AS_SELF) {
return $this;
}
return $this->iterator->current();
}
public function key()
{
return $this->iterator->key();
}
public function next()
{
do {
$found = $this->findNext();
} while ($found && !empty($this->found[$found]));
if ($found) {
// Mark the file as found.
$this->found[$found] = true;
}
}
public function valid()
{
return $this->iterator && $this->iterator->valid();
}
public function rewind()
{
$this->found = [];
$this->stack = $this->locator->findResources($this->path);
$this->next();
}
public function getUrl()
{
$path = $this->path . (substr($this->path, -1, 1) === '/' ? '' : '/');
return $path . $this->iterator->getFilename();
}
public function seek($position)
{
throw new \RuntimeException('Seek not implemented');
}
public function getATime()
{
return $this->iterator->getATime();
}
public function getBasename($suffix = null)
{
return $this->iterator->getBasename($suffix);
}
public function getCTime()
{
return $this->iterator->getCTime();
}
public function getExtension()
{
return $this->iterator->getExtension();
}
public function getFilename()
{
return $this->iterator->getFilename();
}
public function getGroup()
{
return $this->iterator->getGroup();
}
public function getInode()
{
return $this->iterator->getInode();
}
public function getMTime()
{
return $this->iterator->getMTime();
}
public function getOwner()
{
return $this->iterator->getOwner();
}
public function getPath()
{
return $this->iterator->getPath();
}
public function getPathname()
{
return $this->iterator->getPathname();
}
public function getPerms()
{
return $this->iterator->getPerms();
}
public function getSize()
{
return $this->iterator->getSize();
}
public function getType()
{
return $this->iterator->getType();
}
public function isDir()
{
return $this->iterator->isDir();
}
public function isDot()
{
return $this->iterator->isDot();
}
public function isExecutable()
{
return $this->iterator->isExecutable();
}
public function isFile()
{
return $this->iterator->isFile();
}
public function isLink()
{
return $this->iterator->isLink();
}
public function isReadable()
{
return $this->iterator->isReadable();
}
public function isWritable()
{
return $this->iterator->isWritable();
}
public function __toString()
{
return $this->iterator->__toString();
}
public function getFlags()
{
return $this->flags;
}
public function setFlags($flags = null)
{
$this->flags = $flags === null ? static::KEY_AS_PATHNAME | static::CURRENT_AS_SELF | static::SKIP_DOTS : $flags;
if ($this->iterator) {
$this->iterator->setFlags($this->flags);
}
}
protected function findNext()
{
if ($this->iterator) {
$this->iterator->next();
}
if (!$this->valid()) {
do {
// Move to the next iterator if it exists.
$path = array_shift($this->stack);
if (!isset($path)) {
return null;
}
$this->iterator = new \FilesystemIterator($path, $this->getFlags());
} while (!$this->iterator->valid());
}
return $this->getFilename();
}
}

View File

@ -0,0 +1,447 @@
<?php
namespace RocketTheme\Toolbox\ResourceLocator;
/**
* Implements Uniform Resource Location.
*
* @package RocketTheme\Toolbox\ResourceLocator
* @author RocketTheme
* @license MIT
*
* @link http://webmozarts.com/2013/06/19/the-power-of-uniform-resource-location-in-php/
*/
class UniformResourceLocator implements ResourceLocatorInterface
{
/**
* @var string Base URL for all the streams.
*/
public $base;
/**
* @var array
*/
protected $schemes = [];
/**
* @var array
*/
protected $cache = [];
public function __construct($base = null)
{
// Normalize base path.
$this->base = rtrim(str_replace('\\', '/', $base ?: getcwd()), '/');
}
/**
* Return iterator for the resource URI.
*
* @param string $uri
* @param int $flags See constants from FilesystemIterator class.
* @return UniformResourceIterator
*/
public function getIterator($uri, $flags = null)
{
return new UniformResourceIterator($uri, $flags, $this);
}
/**
* Return recursive iterator for the resource URI.
*
* @param string $uri
* @param int $flags See constants from FilesystemIterator class.
* @return RecursiveUniformResourceIterator
*/
public function getRecursiveIterator($uri, $flags = null)
{
return new RecursiveUniformResourceIterator($uri, $flags, $this);
}
/**
* Reset locator by removing all the schemes.
*
* @return $this
*/
public function reset()
{
$this->schemes = [];
$this->cache = [];
return $this;
}
/**
* Reset a locator scheme
*
* @param string $scheme The scheme to reset
*
* @return $this
*/
public function resetScheme($scheme)
{
$this->schemes[$scheme] = [];
$this->cache = [];
return $this;
}
/**
* Add new paths to the scheme.
*
* @param string $scheme
* @param string $prefix
* @param string|array $paths
* @param bool|string $override True to add path as override, string
* @param bool $force True to add paths even if them do not exist.
* @throws \BadMethodCallException
*/
public function addPath($scheme, $prefix, $paths, $override = false, $force = false)
{
$list = [];
foreach((array) $paths as $path) {
if (is_array($path)) {
// Support stream lookup in ['theme', 'path/to'] format.
if (count($path) != 2) {
throw new \BadMethodCallException('Invalid stream path given.');
}
$list[] = $path;
} elseif (strstr($path, '://')) {
// Support stream lookup in 'theme://path/to' format.
$stream = explode('://', $path, 2);
$stream[1] = trim($stream[1], '/');
$list[] = $stream;
} else {
// Normalize path.
$path = rtrim(str_replace('\\', '/', $path), '/');
if ($force || @file_exists("{$this->base}/{$path}") || @file_exists($path)) {
// Support for absolute and relative paths.
$list[] = $path;
}
}
}
if (isset($this->schemes[$scheme][$prefix])) {
$paths = $this->schemes[$scheme][$prefix];
if (!$override || $override == 1) {
$list = $override ? array_merge($paths, $list) : array_merge($list, $paths);
} else {
$location = array_search($override, $paths) ?: count($paths);
array_splice($paths, $location, 0, $list);
$list = $paths;
}
}
$this->schemes[$scheme][$prefix] = $list;
// Sort in reverse order to get longer prefixes to be matched first.
krsort($this->schemes[$scheme]);
$this->cache = [];
}
/**
* Return base directory.
*
* @return string
*/
public function getBase()
{
return $this->base;
}
/**
* Return true if scheme has been defined.
*
* @param string $name
* @return bool
*/
public function schemeExists($name)
{
return isset($this->schemes[$name]);
}
/**
* Return defined schemes.
*
* @return array
*/
public function getSchemes()
{
return array_keys($this->schemes);
}
/**
* Return all scheme lookup paths.
*
* @param string $scheme
* @return array
*/
public function getPaths($scheme = null)
{
return !$scheme ? $this->schemes : (isset($this->schemes[$scheme]) ? $this->schemes[$scheme] : []);
}
/**
* @param string $uri
* @return string|bool
* @throws \BadMethodCallException
*/
public function __invoke($uri)
{
if (!is_string($uri)) {
throw new \BadMethodCallException('Invalid parameter $uri.');
}
return $this->findCached($uri, false, true, false);
}
/**
* Returns true if uri is resolvable by using locator.
*
* @param string $uri
* @return bool
*/
public function isStream($uri)
{
try {
list ($scheme,) = $this->normalize($uri, true, true);
} catch (\Exception $e) {
return false;
}
return $this->schemeExists($scheme);
}
/**
* Returns the canonicalized URI on success. The resulting path will have no '/./' or '/../' components.
* Trailing delimiter `/` is kept.
*
* By default (if $throwException parameter is not set to true) returns false on failure.
*
* @param string $uri
* @param bool $throwException
* @param bool $splitStream
* @return string|array|bool
* @throws \BadMethodCallException
*/
public function normalize($uri, $throwException = false, $splitStream = false)
{
if (!is_string($uri)) {
if ($throwException) {
throw new \BadMethodCallException('Invalid parameter $uri.');
} else {
return false;
}
}
$uri = preg_replace('|\\\|u', '/', $uri);
$segments = explode('://', $uri, 2);
$path = array_pop($segments);
$scheme = array_pop($segments) ?: 'file';
if ($path) {
$path = preg_replace('|\\\|u', '/', $path);
$parts = explode('/', $path);
$list = [];
foreach ($parts as $i => $part) {
if ($part === '..') {
$part = array_pop($list);
if ($part === null || $part === '' || (!$list && strpos($part, ':'))) {
if ($throwException) {
throw new \BadMethodCallException('Invalid parameter $uri.');
} else {
return false;
}
}
} elseif (($i && $part === '') || $part === '.') {
continue;
} else {
$list[] = $part;
}
}
if (($l = end($parts)) === '' || $l === '.' || $l === '..') {
$list[] = '';
}
$path = implode('/', $list);
}
return $splitStream ? [$scheme, $path] : ($scheme !== 'file' ? "{$scheme}://{$path}" : $path);
}
/**
* Find highest priority instance from a resource.
*
* @param string $uri Input URI to be searched.
* @param bool $absolute Whether to return absolute path.
* @param bool $first Whether to return first path even if it doesn't exist.
* @throws \BadMethodCallException
* @return string|bool
*/
public function findResource($uri, $absolute = true, $first = false)
{
if (!is_string($uri)) {
throw new \BadMethodCallException('Invalid parameter $uri.');
}
return $this->findCached($uri, false, $absolute, $first);
}
/**
* Find all instances from a resource.
*
* @param string $uri Input URI to be searched.
* @param bool $absolute Whether to return absolute path.
* @param bool $all Whether to return all paths even if they don't exist.
* @throws \BadMethodCallException
* @return array
*/
public function findResources($uri, $absolute = true, $all = false)
{
if (!is_string($uri)) {
throw new \BadMethodCallException('Invalid parameter $uri.');
}
return $this->findCached($uri, true, $absolute, $all);
}
/**
* Find all instances from a list of resources.
*
* @param array $uris Input URIs to be searched.
* @param bool $absolute Whether to return absolute path.
* @param bool $all Whether to return all paths even if they don't exist.
* @throws \BadMethodCallException
* @return array
*/
public function mergeResources(array $uris, $absolute = true, $all = false)
{
$uris = array_unique($uris);
$list = [];
foreach ($uris as $uri) {
$list = array_merge($list, $this->findResources($uri, $absolute, $all));
}
return $list;
}
/**
* Pre-fill cache by a stream.
*
* @param string $uri
* @return $this
*/
public function fillCache($uri)
{
$cacheKey = $uri . '@cache';
if (!isset($this->cache[$cacheKey])) {
$this->cache[$cacheKey] = true;
$iterator = new \RecursiveIteratorIterator($this->getRecursiveIterator($uri), \RecursiveIteratorIterator::SELF_FIRST);
/** @var UniformResourceIterator $uri */
foreach ($iterator as $uri) {
$key = $uri->getUrl() . '@010';
$this->cache[$key] = $uri->getPathname();
}
}
return $this;
}
protected function findCached($uri, $array, $absolute, $all)
{
// Local caching: make sure that the function gets only called at once for each file.
$key = $uri .'@'. (int) $array . (int) $absolute . (int) $all;
if (!isset($this->cache[$key])) {
try {
list ($scheme, $file) = $this->normalize($uri, true, true);
if (!$file && $scheme === 'file') {
$file = $this->base;
}
$this->cache[$key] = $this->find($scheme, $file, $array, $absolute, $all);
} catch (\BadMethodCallException $e) {
$this->cache[$key] = $array ? [] : false;
}
}
return $this->cache[$key];
}
/**
* @param string $scheme
* @param string $file
* @param bool $array
* @param bool $absolute
* @param bool $all
*
* @throws \InvalidArgumentException
* @return array|string|bool
* @internal
*/
protected function find($scheme, $file, $array, $absolute, $all)
{
if (!isset($this->schemes[$scheme])) {
throw new \InvalidArgumentException("Invalid resource {$scheme}://");
}
$results = $array ? [] : false;
foreach ($this->schemes[$scheme] as $prefix => $paths) {
if ($prefix && strpos($file, $prefix) !== 0) {
continue;
}
// Remove prefix from filename.
$filename = '/' . trim(substr($file, strlen($prefix)), '\/');
foreach ($paths as $path) {
if (is_array($path)) {
// Handle scheme lookup.
$relPath = trim($path[1] . $filename, '/');
$found = $this->find($path[0], $relPath, $array, $absolute, $all);
if ($found) {
if (!$array) {
return $found;
}
$results = array_merge($results, $found);
}
} else {
// TODO: We could provide some extra information about the path to remove preg_match().
// Check absolute paths for both unix and windows
if (!$path || !preg_match('`^/|\w+:`', $path)) {
// Handle relative path lookup.
$relPath = trim($path . $filename, '/');
$fullPath = $this->base . '/' . $relPath;
} else {
// Handle absolute path lookup.
$fullPath = rtrim($path . $filename, '/');
if (!$absolute) {
throw new \RuntimeException("UniformResourceLocator: Absolute stream path with relative lookup not allowed ({$prefix})", 500);
}
}
if ($all || file_exists($fullPath)) {
$current = $absolute ? $fullPath : $relPath;
if (!$array) {
return $current;
}
$results[] = $current;
}
}
}
}
return $results;
}
}

View File

@ -0,0 +1,258 @@
<?php
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class UniformResourceLocatorTest extends PHPUnit_Framework_TestCase
{
/**
* @var UniformResourceLocator
*/
static protected $locator;
public static function setUpBeforeClass()
{
// Share locator in all tests.
self::$locator = new UniformResourceLocator(__DIR__ . '/data');
}
public function testGetBase()
{
$this->assertEquals(__DIR__ . '/data', self::$locator->getBase());
}
/**
* @param $scheme
* @param $path
* @param $lookup
*
* @dataProvider addPathProvider
*/
public function testAddPath($scheme, $path, $lookup)
{
$locator = self::$locator;
$this->assertFalse($locator->schemeExists($scheme));
$locator->addPath($scheme, $path, $lookup);
$this->assertTrue($locator->schemeExists($scheme));
}
public function addPathProvider() {
return [
['base', '', 'base'],
['local', '', 'local'],
['override', '', 'override'],
['all', '', ['override://all', 'local://all', 'base://all']],
];
}
/**
* @depends testAddPath
*/
public function testGetSchemes()
{
$this->assertEquals(
['base', 'local', 'override', 'all'],
self::$locator->getSchemes()
);
}
/**
* @depends testAddPath
* @dataProvider getPathsProvider
*/
public function testGetPaths($scheme, $expected)
{
$locator = self::$locator;
$this->assertEquals($expected, $locator->getPaths($scheme));
}
public function getPathsProvider() {
return [
['base', ['' => ['base']]],
['local', ['' => ['local']]],
['override', ['' => ['override']]],
['all', ['' => [['override', 'all'], ['local', 'all'], ['base', 'all']]]],
['fail', []]
];
}
/**
* @depends testAddPath
*/
public function testSchemeExists()
{
$locator = self::$locator;
// Partially tested in addPath() tests.
$this->assertFalse($locator->schemeExists('foo'));
$this->assertFalse($locator->schemeExists('file'));
}
/**
* @depends testAddPath
*/
public function testGetIterator()
{
$locator = self::$locator;
$this->assertInstanceOf(
'RocketTheme\Toolbox\ResourceLocator\UniformResourceIterator',
$locator->getIterator('all://')
);
$this->setExpectedException('InvalidArgumentException', 'Invalid resource fail://');
$locator->getIterator('fail://');
}
/**
* @depends testAddPath
*/
public function testGetRecursiveIterator()
{
$locator = self::$locator;
$this->assertInstanceOf(
'RocketTheme\Toolbox\ResourceLocator\RecursiveUniformResourceIterator',
$locator->getRecursiveIterator('all://')
);
$this->setExpectedException('InvalidArgumentException', 'Invalid resource fail://');
$locator->getRecursiveIterator('fail://');
}
/**
* @depends testAddPath
*/
public function testIsStream($uri)
{
$locator = self::$locator;
// Existing file.
$this->assertEquals(true, $locator->isStream('all://base.txt'));
// Non-existing file.
$this->assertEquals(true, $locator->isStream('all://bar.txt'));
// Unknown uri type.
$this->assertEquals(false, $locator->isStream('fail://base.txt'));
// Bad uri.
$this->assertEquals(false, $locator->isStream('fail://../base.txt'));
}
/**
* @dataProvider normalizeProvider
*/
public function testNormalize($uri, $path)
{
$locator = self::$locator;
$this->assertEquals($path, $locator->normalize($uri));
}
/**
* @depends testAddPath
* @dataProvider findResourcesProvider
*/
public function testFindResource($uri, $paths)
{
$locator = self::$locator;
$path = $paths ? reset($paths) : false;
$fullPath = !$path ? false : __DIR__ . "/data/{$path}";
$this->assertEquals($fullPath, $locator->findResource($uri));
$this->assertEquals($path, $locator->findResource($uri, false));
}
/**
* @depends testAddPath
* @dataProvider findResourcesProvider
*/
public function testFindResources($uri, $paths)
{
$locator = self::$locator;
$this->assertEquals($paths, $locator->findResources($uri, false));
}
/**
* @depends testFindResource
* @dataProvider findResourcesProvider
*/
public function testInvoke($uri, $paths)
{
$locator = self::$locator;
$path = $paths ? reset($paths) : false;
$fullPath = !$path ? false : __DIR__ . "/data/{$path}";
$this->assertEquals($fullPath, $locator($uri));
}
public function normalizeProvider() {
return [
['', ''],
['./', ''],
['././/./', ''],
['././/../', false],
['/', '/'],
['//', '/'],
['///', '/'],
['/././', '/'],
['foo', 'foo'],
['/foo', '/foo'],
['//foo', '/foo'],
['/foo/', '/foo/'],
['//foo//', '/foo/'],
['path/to/file.txt', 'path/to/file.txt'],
['path/to/../file.txt', 'path/file.txt'],
['path/to/../../file.txt', 'file.txt'],
['path/to/../../../file.txt', false],
['/path/to/file.txt', '/path/to/file.txt'],
['/path/to/../file.txt', '/path/file.txt'],
['/path/to/../../file.txt', '/file.txt'],
['/path/to/../../../file.txt', false],
['c:\\', 'c:/'],
['c:\\path\\to\file.txt', 'c:/path/to/file.txt'],
['c:\\path\\to\../file.txt', 'c:/path/file.txt'],
['c:\\path\\to\../../file.txt', 'c:/file.txt'],
['c:\\path\\to\../../../file.txt', false],
['stream://path/to/file.txt', 'stream://path/to/file.txt'],
['stream://path/to/../file.txt', 'stream://path/file.txt'],
['stream://path/to/../../file.txt', 'stream://file.txt'],
['stream://path/to/../../../file.txt', false],
];
}
public function findResourcesProvider() {
return [
['all://base.txt', ['base/all/base.txt']],
['all://base_all.txt', ['override/all/base_all.txt', 'local/all/base_all.txt', 'base/all/base_all.txt']],
['all://base_local.txt', ['local/all/base_local.txt', 'base/all/base_local.txt']],
['all://base_override.txt', ['override/all/base_override.txt', 'base/all/base_override.txt']],
['all://local.txt', ['local/all/local.txt']],
['all://local_override.txt', ['override/all/local_override.txt', 'local/all/local_override.txt']],
['all://override.txt', ['override/all/override.txt']],
['all://missing.txt', []],
['all://asdf/../base.txt', ['base/all/base.txt']],
];
}
/**
* @depends testAddPath
*/
public function testMergeResources()
{
$locator = self::$locator;
}
public function testReset()
{
$locator = self::$locator;
}
public function testResetScheme()
{
$locator = self::$locator;
}
}