8000 [Config] Fix resource tracking with new GlobResource by nicolas-grekas · Pull Request #22621 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Config] Fix resource tracking with new GlobResource #22621

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,17 @@
*/
class FileLocatorFileNotFoundException extends \InvalidArgumentException
{
private $paths;

public function __construct($message = '', $code = 0, $previous = null, array $paths = array())
{
parent::__construct($message, $code, $previous);

$this->paths = $paths;
}

public function getPaths()
{
return $this->paths;
}
}
8 changes: 5 additions & 3 deletions src/Symfony/Component/Config/FileLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function locate($name, $currentPath = null, $first = true)

if ($this->isAbsolutePath($name)) {
if (!file_exists($name)) {
throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist.', $name));
throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist.', $name), 0, null, array($name));
}

return $name;
Expand All @@ -56,19 +56,21 @@ public function locate($name, $currentPath = null, $first = true)
}

$paths = array_unique($paths);
$filepaths = array();
$filepaths = $notfound = array();

foreach ($paths as $path) {
if (@file_exists($file = $path.DIRECTORY_SEPARATOR.$name)) {
if (true === $first) {
return $file;
}
$filepaths[] = $file;
} else {
$notfound[] = $file;
}
}

if (!$filepaths) {
throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist (in: %s).', $name, implode(', ', $paths)));
throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist (in: %s).', $name, implode(', ', $paths)), 0, null, $notfound);
}

return $filepaths;
Expand Down
80 changes: 21 additions & 59 deletions src/Symfony/Component/Config/Loader/FileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
use Symfony\Component\Config\Exception\FileLoaderLoadException;
use Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\Glob;
use Symfony\Component\Config\Resource\FileExistenceResource;
use Symfony\Component\Config\Resource\GlobResource;

/**
* FileLoader is the abstract class used by all built-in loaders that are file based.
Expand Down Expand Up @@ -85,9 +85,13 @@ public function import($resource, $type = null, $ignoreErrors = false, $sourceRe
{
$ret = array();
$ct = 0;
foreach ($this->glob($resource, false, $_, $ignoreErrors) as $resource => $info) {
++$ct;
if (!is_string($resource) || false === strpbrk($resource, '*?{[')) {
$ret[] = $this->doImport($resource, $type, $ignoreErrors, $sourceResource);
} else {
foreach ($this->glob($resource, false, $_, $ignoreErrors) as $path => $info) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fact that we ignore the resource here means that we may miss some cases for reloading the container when using a glob import.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's how things are now: the Config component doesn't know anything about the container so this cannot trivially be otherwise. I guess $sourceResource is here for that purpose. Not something this PR tries to improve.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if the import is done by a DI loader extending this abstract class, it should be given a chance to register the resource somewhere (the child class in the component knows what it wants to do with the resource)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and I know this is not what this PR tries to improve. But I detected it while reviewing it. And this looks like one of the cases that we still need to polish before the stable release including glob support.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be able to give it a try after this PR is merged?

++$ct;
$ret[] = $this->doImport($path, $type, $ignoreErrors, $sourceResource);
}
}

return $ct > 1 ? $ret : (isset($ret[0]) ? $ret[0] : null);
Expand All @@ -96,24 +100,17 @@ public function import($resource, $type = null, $ignoreErrors = false, $sourceRe
/**
* @internal
*/
protected function glob($resource, $recursive, &$prefix = null, $ignoreErrors = false)
protected function glob($pattern, $recursive, &$resource = null, $ignoreErrors = false)
{
if (strlen($resource) === $i = strcspn($resource, '*?{[')) {
if (!$recursive) {
$prefix = null;

yield $resource => new \SplFileInfo($resource);

return;
}
$prefix = $resource;
$resource = '';
if (strlen($pattern) === $i = strcspn($pattern, '*?{[')) {
$prefix = $pattern;
$pattern = '';
} elseif (0 === $i) {
$prefix = '.';
$resource = '/'.$resource;
$pattern = '/'.$pattern;
} else {
$prefix = dirname(substr($resource, 0, 1 + $i));
$resource = substr($resource, strlen($prefix));
$prefix = dirname(substr($pattern, 0, 1 + $i));
$pattern = substr($pattern, strlen($prefix));
}

try {
Expand All @@ -123,52 +120,17 @@ protected function glob($resource, $recursive, &$prefix = null, $ignoreErrors =
throw $e;
}

return;
}
$prefix = realpath($prefix) ?: $prefix;

if (false === strpos($resource, '/**/') && (defined('GLOB_BRACE') || false === strpos($resource, '{'))) {
foreach (glob($prefix.$resource, defined('GLOB_BRACE') ? GLOB_BRACE : 0) as $path) {
if ($recursive && is_dir($path)) {
$files = iterator_to_array(new \RecursiveIteratorIterator(
new \RecursiveCallbackFilterIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
function (\SplFileInfo $file) { return '.' !== $file->getBasename()[0]; }
),
\RecursiveIteratorIterator::LEAVES_ONLY
));
uasort($files, function (\SplFileInfo $a 9E88 , \SplFileInfo $b) {
return (string) $a > (string) $b ? 1 : -1;
});

foreach ($files as $path => $info) {
if ($info->isFile()) {
yield $path => $info;
}
}
} elseif (is_file($path)) {
yield $path => new \SplFileInfo($path);
}
$resource = array();
foreach ($e->getPaths() as $path) {
$resource[] = new FileExistenceResource($path);
}

return;
}
$resource = new GlobResource($prefix, $pattern, $recursive);

if (!class_exists(Finder::class)) {
throw new \LogicException(sprintf('Extended glob pattern "%s" cannot be used as the Finder component is not installed.', $resource));
}

$finder = new Finder();
$regex = Glob::toRegex($resource);
if ($recursive) {
$regex = substr_replace($regex, '(/|$)', -2, 1);
}

$prefixLen = strlen($prefix);
foreach ($finder->followLinks()->sortByName()->in($prefix) as $path => $info) {
if (preg_match($regex, substr($path, $prefixLen)) && $info->isFile()) {
yield $path => $info;
}
foreach ($resource as $path => $info) {
yield $path => $info;
}
}

Expand Down
153 changes: 153 additions & 0 deletions src/Symfony/Component/Config/Resource/GlobResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?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>
*/
class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface, \Serializable
{
private $prefix;
private $pattern;
private $recursive;
private $hash;

/**
* Constructor.
*
* @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($prefix, $pattern, $recursive)
{
$this->prefix = realpath($prefix) ?: (file_exists($prefix) ? $prefix : false);
$this->pattern = $pattern;
$this->recursive = $recursive;

if (false === $this->prefix) {
throw new \InvalidArgumentException(sprintf('The path "%s" does not exist.', $prefix));
}
}

public function getPrefix()
{
return $this->prefix;
}

/**
* {@inheritdoc}
*/
public function __toString()
{
return 'glob.'.$this->prefix.$this->pattern.(int) $this->recursive;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should add a delimiter between these, to avoid having similar identifiers for different prefixes and patterns (using a delimiter unlikely to appear in a glob)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, that's on purpose, two resources with the same concat for (prefix, pattern) should be considered the same, because they'll end up listing the same files.

}

/**
* {@inheritdoc}
*/
public function isFresh($timestamp)
{
$hash = $this->computeHash();

if (null === $this->hash) {
$this->hash = $hash;
}

return $this->hash === $hash;
}

public function serialize()
{
if (null === $this->hash) {
$this->hash = $this->computeHash();
}

return serialize(array($this->prefix, $this->pattern, $this->recursive, $this->hash));
}

public function unserialize($serialized)
{
list($this->prefix, $this->pattern, $this->recursive, $this->hash) = unserialize($serialized);
}

public function getIterator()
{
if (!file_exists($this->prefix) || (!$this->recursive && '' === $this->pattern)) {
return;
}

if (false === strpos($this->pattern, '/**/') && (defined('GLOB_BRACE') || false === strpos($this->pattern, '{'))) {
foreach (glob($this->prefix.$this->pattern, defined('GLOB_BRACE') ? GLOB_BRACE : 0) as $path) {
if ($this->recursive && is_dir($path)) {
$files = iterator_to_array(new \RecursiveIteratorIterator(
new \RecursiveCallbackFilterIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
function (\SplFileInfo $file) { return '.' !== $file->getBasename()[0]; }
),
\RecursiveIteratorIterator::LEAVES_ONLY
));
uasort($files, function (\SplFileInfo $a, \SplFileInfo $b) {
return (string) $a > (string) $b ? 1 : -1;
});

foreach ($files as $path => $info) {
if ($info->isFile()) {
yield $path => $info;
}
}
} elseif (is_file($path)) {
yield $path => new \SplFileInfo($path);
}
}

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) {
if (preg_match($regex, substr($path, $prefixLen)) && $info->isFile()) {
yield $path => $info;
}
}
}

private function computeHash()
{
$hash = hash_init('md5');

foreach ($this->getIterator() as $path => $info) {
hash_update($hash, $path."\n");
}

return hash_final($hash);
}
}
Empty file.
Loading
0