You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
7.5 KiB
237 lines
7.5 KiB
<?php |
|
|
|
/* |
|
* This file is part of the Symfony package. |
|
* |
|
* (c) Fabien Potencier <fabien@symfony.com> |
|
* |
|
* For the full copyright and license information, please view the LICENSE |
|
* file that was distributed with this source code. |
|
*/ |
|
|
|
namespace Symfony\Component\Config\Resource; |
|
|
|
use Symfony\Component\Finder\Finder; |
|
use Symfony\Component\Finder\Glob; |
|
|
|
/** |
|
* GlobResource represents a set of resources stored on the filesystem. |
|
* |
|
* Only existence/removal is tracked (not mtimes.) |
|
* |
|
* @author Nicolas Grekas <p@tchwork.com> |
|
* |
|
* @final |
|
* |
|
* @implements \IteratorAggregate<string, \SplFileInfo> |
|
*/ |
|
class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface |
|
{ |
|
private $prefix; |
|
private $pattern; |
|
private $recursive; |
|
private $hash; |
|
private $forExclusion; |
|
private $excludedPrefixes; |
|
private $globBrace; |
|
|
|
/** |
|
* @param string $prefix A directory prefix |
|
* @param string $pattern A glob pattern |
|
* @param bool $recursive Whether directories should be scanned recursively or not |
|
* |
|
* @throws \InvalidArgumentException |
|
*/ |
|
public function __construct(string $prefix, string $pattern, bool $recursive, bool $forExclusion = false, array $excludedPrefixes = []) |
|
{ |
|
ksort($excludedPrefixes); |
|
$this->prefix = realpath($prefix) ?: (file_exists($prefix) ? $prefix : false); |
|
$this->pattern = $pattern; |
|
$this->recursive = $recursive; |
|
$this->forExclusion = $forExclusion; |
|
$this->excludedPrefixes = $excludedPrefixes; |
|
$this->globBrace = \defined('GLOB_BRACE') ? \GLOB_BRACE : 0; |
|
|
|
if (false === $this->prefix) { |
|
throw new \InvalidArgumentException(sprintf('The path "%s" does not exist.', $prefix)); |
|
} |
|
} |
|
|
|
public function getPrefix(): string |
|
{ |
|
return $this->prefix; |
|
} |
|
|
|
public function __toString(): string |
|
{ |
|
return 'glob.'.$this->prefix.(int) $this->recursive.$this->pattern.(int) $this->forExclusion.implode("\0", $this->excludedPrefixes); |
|
} |
|
|
|
/** |
|
* {@inheritdoc} |
|
*/ |
|
public function isFresh(int $timestamp): bool |
|
{ |
|
$hash = $this->computeHash(); |
|
|
|
if (null === $this->hash) { |
|
$this->hash = $hash; |
|
} |
|
|
|
return $this->hash === $hash; |
|
} |
|
|
|
/** |
|
* @internal |
|
*/ |
|
public function __sleep(): array |
|
{ |
|
if (null === $this->hash) { |
|
$this->hash = $this->computeHash(); |
|
} |
|
|
|
return ['prefix', 'pattern', 'recursive', 'hash', 'forExclusion', 'excludedPrefixes']; |
|
} |
|
|
|
/** |
|
* @internal |
|
*/ |
|
public function __wakeup(): void |
|
{ |
|
$this->globBrace = \defined('GLOB_BRACE') ? \GLOB_BRACE : 0; |
|
} |
|
|
|
public function getIterator(): \Traversable |
|
{ |
|
if (!file_exists($this->prefix) || (!$this->recursive && '' === $this->pattern)) { |
|
return; |
|
} |
|
$prefix = str_replace('\\', '/', $this->prefix); |
|
$paths = null; |
|
|
|
if (!str_starts_with($this->prefix, 'phar://') && !str_contains($this->pattern, '/**/')) { |
|
if ($this->globBrace || !str_contains($this->pattern, '{')) { |
|
$paths = glob($this->prefix.$this->pattern, \GLOB_NOSORT | $this->globBrace); |
|
} elseif (!str_contains($this->pattern, '\\') || !preg_match('/\\\\[,{}]/', $this->pattern)) { |
|
foreach ($this->expandGlob($this->pattern) as $p) { |
|
$paths[] = glob($this->prefix.$p, \GLOB_NOSORT); |
|
} |
|
$paths = array_merge(...$paths); |
|
} |
|
} |
|
|
|
if (null !== $paths) { |
|
natsort($paths); |
|
foreach ($paths as $path) { |
|
if ($this->excludedPrefixes) { |
|
$normalizedPath = str_replace('\\', '/', $path); |
|
do { |
|
if (isset($this->excludedPrefixes[$dirPath = $normalizedPath])) { |
|
continue 2; |
|
} |
|
} while ($prefix !== $dirPath && $dirPath !== $normalizedPath = \dirname($dirPath)); |
|
} |
|
|
|
if (is_file($path)) { |
|
yield $path => new \SplFileInfo($path); |
|
} |
|
if (!is_dir($path)) { |
|
continue; |
|
} |
|
if ($this->forExclusion) { |
|
yield $path => new \SplFileInfo($path); |
|
continue; |
|
} |
|
if (!$this->recursive || isset($this->excludedPrefixes[str_replace('\\', '/', $path)])) { |
|
continue; |
|
} |
|
$files = iterator_to_array(new \RecursiveIteratorIterator( |
|
new \RecursiveCallbackFilterIterator( |
|
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), |
|
function (\SplFileInfo $file, $path) { |
|
return !isset($this->excludedPrefixes[str_replace('\\', '/', $path)]) && '.' !== $file->getBasename()[0]; |
|
} |
|
), |
|
\RecursiveIteratorIterator::LEAVES_ONLY |
|
)); |
|
uksort($files, 'strnatcmp'); |
|
|
|
foreach ($files as $path => $info) { |
|
if ($info->isFile()) { |
|
yield $path => $info; |
|
} |
|
} |
|
} |
|
|
|
return; |
|
} |
|
|
|
if (!class_exists(Finder::class)) { |
|
throw new \LogicException(sprintf('Extended glob pattern "%s" cannot be used as the Finder component is not installed.', $this->pattern)); |
|
} |
|
|
|
$finder = new Finder(); |
|
$regex = Glob::toRegex($this->pattern); |
|
if ($this->recursive) { |
|
$regex = substr_replace($regex, '(/|$)', -2, 1); |
|
} |
|
|
|
$prefixLen = \strlen($this->prefix); |
|
foreach ($finder->followLinks()->sortByName()->in($this->prefix) as $path => $info) { |
|
$normalizedPath = str_replace('\\', '/', $path); |
|
if (!preg_match($regex, substr($normalizedPath, $prefixLen)) || !$info->isFile()) { |
|
continue; |
|
} |
|
if ($this->excludedPrefixes) { |
|
do { |
|
if (isset($this->excludedPrefixes[$dirPath = $normalizedPath])) { |
|
continue 2; |
|
} |
|
} while ($prefix !== $dirPath && $dirPath !== $normalizedPath = \dirname($dirPath)); |
|
} |
|
|
|
yield $path => $info; |
|
} |
|
} |
|
|
|
private function computeHash(): string |
|
{ |
|
$hash = hash_init('md5'); |
|
|
|
foreach ($this->getIterator() as $path => $info) { |
|
hash_update($hash, $path."\n"); |
|
} |
|
|
|
return hash_final($hash); |
|
} |
|
|
|
private function expandGlob(string $pattern): array |
|
{ |
|
$segments = preg_split('/\{([^{}]*+)\}/', $pattern, -1, \PREG_SPLIT_DELIM_CAPTURE); |
|
$paths = [$segments[0]]; |
|
$patterns = []; |
|
|
|
for ($i = 1; $i < \count($segments); $i += 2) { |
|
$patterns = []; |
|
|
|
foreach (explode(',', $segments[$i]) as $s) { |
|
foreach ($paths as $p) { |
|
$patterns[] = $p.$s.$segments[1 + $i]; |
|
} |
|
} |
|
|
|
$paths = $patterns; |
|
} |
|
|
|
$j = 0; |
|
foreach ($patterns as $i => $p) { |
|
if (str_contains($p, '{')) { |
|
$p = $this->expandGlob($p); |
|
array_splice($paths, $i + $j, 1, $p); |
|
$j += \count($p) - 1; |
|
} |
|
} |
|
|
|
return $paths; |
|
} |
|
}
|
|
|