diff --git a/src/Symfony/Component/Config/Exception/FileLocatorFileNotFoundException.php b/src/Symfony/Component/Config/Exception/FileLocatorFileNotFoundException.php index 4d4a56dfc66a8..af764eb4718d8 100644 --- a/src/Symfony/Component/Config/Exception/FileLocatorFileNotFoundException.php +++ b/src/Symfony/Component/Config/Exception/FileLocatorFileNotFoundException.php @@ -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; + } } diff --git a/src/Symfony/Component/Config/FileLocator.php b/src/Symfony/Component/Config/FileLocator.php index 6b686390bc296..16bb5d48d7383 100644 --- a/src/Symfony/Component/Config/FileLocator.php +++ b/src/Symfony/Component/Config/FileLocator.php @@ -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; @@ -56,7 +56,7 @@ 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)) { @@ -64,11 +64,13 @@ public function locate($name, $currentPath = null, $first = true) 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; diff --git a/src/Symfony/Component/Config/Loader/FileLoader.php b/src/Symfony/Component/Config/Loader/FileLoader.php index d563bc026951c..6119e5a5b1118 100644 --- a/src/Symfony/Component/Config/Loader/FileLoader.php +++ b/src/Symfony/Component/Config/Loader/FileLoader.php @@ -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. @@ -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) { + ++$ct; + $ret[] = $this->doImport($path, $type, $ignoreErrors, $sourceResource); + } } return $ct > 1 ? $ret : (isset($ret[0]) ? $ret[0] : null); @@ -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 { @@ -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, \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; } } diff --git a/src/Symfony/Component/Config/Resource/GlobResource.php b/src/Symfony/Component/Config/Resource/GlobResource.php new file mode 100644 index 0000000000000..67625201530f5 --- /dev/null +++ b/src/Symfony/Component/Config/Resource/GlobResource.php @@ -0,0 +1,153 @@ + + * + * 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 + */ +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; + } + + /** + * {@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); + } +} diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Resource/.hiddenFile b/src/Symfony/Component/Config/Tests/Fixtures/Resource/.hiddenFile new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php new file mode 100644 index 0000000000000..b84cc9d3ae3b9 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Resource; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Resource\GlobResource; + +class GlobResourceTest extends TestCase +{ + protected function tearDown() + { + $dir = dirname(__DIR__).'/Fixtures'; + @rmdir($dir.'/TmpGlob'); + @unlink($dir.'/TmpGlob'); + @unlink($dir.'/Resource/TmpGlob'); + touch($dir.'/Resource/.hiddenFile'); + } + + public function testIterator() + { + $dir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'; + $resource = new GlobResource($dir, '/Resource', true); + + $paths = iterator_to_array($resource); + + $file = $dir.'/Resource'.DIRECTORY_SEPARATOR.'ConditionalClass.php'; + $this->assertEquals(array($file => new \SplFileInfo($file)), $paths); + $this->assertInstanceOf('SplFileInfo', current($paths)); + $this->assertSame($dir, $resource->getPrefix()); + } + + public function testIsFreshNonRecursiveDetectsNewFile() + { + $dir = dirname(__DIR__).'/Fixtures'; + $resource = new GlobResource($dir, '/*', false); + + $this->assertTrue($resource->isFresh(0)); + + mkdir($dir.'/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + rmdir($dir.'/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + touch($dir.'/TmpGlob'); + $this->assertFalse($resource->isFresh(0)); + + unlink($dir.'/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + } + + public function testIsFreshNonRecursiveDetectsRemovedFile() + { + $dir = dirname(__DIR__).'/Fixtures'; + $resource = new GlobResource($dir, '/*', false); + + touch($dir.'/TmpGlob'); + touch($dir.'/.TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + unlink($dir.'/.TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + unlink($dir.'/TmpGlob'); + $this->assertFalse($resource->isFresh(0)); + } + + public function testIsFreshRecursiveDetectsRemovedFile() + { + $dir = dirname(__DIR__).'/Fixtures'; + $resource = new GlobResource($dir, '/*', true); + + touch($dir.'/Resource/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + unlink($dir.'/Resource/TmpGlob'); + $this->assertFalse($resource->isFresh(0)); + + touch($dir.'/Resource/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + unlink($dir.'/Resource/.hiddenFile'); + $this->assertTrue($resource->isFresh(0)); + } + + public function testIsFreshRecursiveDetectsNewFile() + { + $dir = dirname(__DIR__).'/Fixtures'; + $resource = new GlobResource($dir, '/*', true); + + $this->assertTrue($resource->isFresh(0)); + + touch($dir.'/Resource/TmpGlob'); + $this->assertFalse($resource->isFresh(0)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 929b8983cacc4..30c8614b3e6a7 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -33,6 +33,7 @@ use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Config\Resource\FileExistenceResource; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\GlobResource; use Symfony\Component\Config\Resource\ReflectionClassResource; use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface; @@ -249,6 +250,10 @@ public function addResource(ResourceInterface $resource) return $this; } + if ($resource instanceof GlobResource && $this->inVendors($resource->getPrefix())) { + return $this; + } + $this->resources[(string) $resource] = $resource; return $this; diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 4e3c37d87eb72..02ffd902254e0 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader; use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Resource\GlobResource; /** * FileLoader is the abstract class used by all built-in loaders that are file based. @@ -83,16 +84,16 @@ protected function setDefinition($id, Definition $definition) } } - private function findClasses($namespace, $resource) + private function findClasses($namespace, $pattern) { $parameterBag = $this->container->getParameterBag(); - $resource = $parameterBag->unescapeValue($parameterBag->resolveValue($resource)); + $pattern = $parameterBag->unescapeValue($parameterBag->resolveValue($pattern)); $classes = array(); $extRegexp = defined('HHVM_VERSION') ? '/\\.(?:php|hh)$/' : '/\\.php$/'; $prefixLen = null; - foreach ($this->glob($resource, true, $prefix) as $path => $info) { + foreach ($this->glob($pattern, true, $resource) as $path => $info) { if (null === $prefixLen) { - $prefixLen = strlen($prefix); + $prefixLen = strlen($resource->getPrefix()); } if (!preg_match($extRegexp, $path, $m) || !$info->isReadable()) { @@ -105,16 +106,20 @@ private function findClasses($namespace, $resource) } // check to make sure the expected class exists if (!$r = $this->container->getReflectionClass($class)) { - throw new InvalidArgumentException(sprintf('Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $resource)); + throw new InvalidArgumentException(sprintf('Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $pattern)); } if (!$r->isInterface() && !$r->isTrait()) { $classes[] = $class; } } - if (null !== $prefix) { - // track directories only for new & removed files - $this->container->fileExists($prefix, '/^$/'); + // track only for new & removed files + if ($resource instanceof GlobResource) { + $this->container->addResource($resource); + } else { + foreach ($resource as $path) { + $this->container->fileExists($path, false); + } } return $classes; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index db48db99c6032..a4ae5a0fb48fc 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -20,8 +20,8 @@ use Symfony\Component\DependencyInjection\Loader\IniFileLoader; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\FileLocator; -use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\GlobResource; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; @@ -608,7 +608,7 @@ public function testPrototype() $fixturesDir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR; $this->assertTrue(false !== array_search(new FileResource($fixturesDir.'xml'.DIRECTORY_SEPARATOR.'services_prototype.xml'), $resources)); - $this->assertTrue(false !== array_search(new DirectoryResource($fixturesDir.'Prototype', '/^$/'), $resources)); + $this->assertTrue(false !== array_search(new GlobResource($fixturesDir.'Prototype', '/*', true), $resources)); $resources = array_map('strval', $resources); $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo', $resources); $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar', $resources); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 6407112b407d6..7511e018978f8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -21,8 +21,8 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\FileLocator; -use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\GlobResource; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; @@ -382,7 +382,7 @@ public function testPrototype() $fixturesDir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR; $this->assertTrue(false !== array_search(new FileResource($fixturesDir.'yaml'.DIRECTORY_SEPARATOR.'services_prototype.yml'), $resources)); - $this->assertTrue(false !== array_search(new DirectoryResource($fixturesDir.'Prototype', '/^$/'), $resources)); + $this->assertTrue(false !== array_search(new GlobResource($fixturesDir.'Prototype', '', true), $resources)); $resources = array_map('strval', $resources); $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo', $resources); $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar', $resources); diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 3a2916635ffb8..7b0932881b464 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -32,7 +32,7 @@ "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them" }, "conflict": { - "symfony/config": "<3.3", + "symfony/config": "<=3.3-beta1", "symfony/finder": "<3.3", "symfony/yaml": "<3.3" },