diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 1910ec66a25c6..40cf8f9ee4cae 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -47,8 +47,9 @@ public function __construct(ContainerBuilder $container, FileLocatorInterface $l * @param Definition $prototype A definition to use as template * @param string $namespace The namespace prefix of classes in the scanned directory * @param string $resource The directory to look for classes, glob-patterns allowed + * @param string $exclude A globed path of files to exclude */ - public function registerClasses(Definition $prototype, $namespace, $resource) + public function registerClasses(Definition $prototype, $namespace, $resource, $exclude = null) { if ('\\' !== substr($namespace, -1)) { throw new InvalidArgumentException(sprintf('Namespace prefix must end with a "\\": %s.', $namespace)); @@ -57,7 +58,7 @@ public function registerClasses(Definition $prototype, $namespace, $resource) throw new InvalidArgumentException(sprintf('Namespace is not a valid PSR-4 prefix: %s.', $namespace)); } - $classes = $this->findClasses($namespace, $resource); + $classes = $this->findClasses($namespace, $resource, $exclude); // prepare for deep cloning $prototype = serialize($prototype); @@ -84,9 +85,24 @@ protected function setDefinition($id, Definition $definition) } } - private function findClasses($namespace, $pattern) + private function findClasses($namespace, $pattern, $excludePattern) { $parameterBag = $this->container->getParameterBag(); + + $excludePaths = array(); + $excludePrefix = null; + if ($excludePattern) { + $excludePattern = $parameterBag->unescapeValue($parameterBag->resolveValue($excludePattern)); + foreach ($this->glob($excludePattern, true, $resource) as $path => $info) { + if (null === $excludePrefix) { + $excludePrefix = $resource->getPrefix(); + } + + // normalize Windows slashes + $excludePaths[str_replace('\\', '/', $path)] = true; + } + } + $pattern = $parameterBag->unescapeValue($parameterBag->resolveValue($pattern)); $classes = array(); $extRegexp = defined('HHVM_VERSION') ? '/\\.(?:php|hh)$/' : '/\\.php$/'; @@ -94,6 +110,14 @@ private function findClasses($namespace, $pattern) foreach ($this->glob($pattern, true, $resource) as $path => $info) { if (null === $prefixLen) { $prefixLen = strlen($resource->getPrefix()); + + if ($excludePrefix && strpos($excludePrefix, $resource->getPrefix()) !== 0) { + throw new InvalidArgumentException(sprintf('Invalid "exclude" pattern when importing classes for "%s": make sure your "exclude" pattern (%s) is a subset of the "resource" pattern (%s)', $namespace, $excludePattern, $pattern)); + } + } + + if (isset($excludePaths[str_replace('\\', '/', $path)])) { + continue; } if (!preg_match($extRegexp, $path, $m) || !$info->isReadable()) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 3ccbdbc79155e..79c8fbf9677ab 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -143,7 +143,7 @@ private function parseDefinitions(\DOMDocument $xml, $file, $defaults) foreach ($services as $service) { if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) { if ('prototype' === $service->tagName) { - $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource')); + $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), (string) $service->getAttribute('exclude')); } else { $this->setDefinition((string) $service->getAttribute('id'), $definition); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 3214f4e023cb6..c3e697c1b5207 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -61,6 +61,7 @@ class YamlFileLoader extends FileLoader private static $prototypeKeywords = array( 'resource' => 'resource', + 'exclude' => 'exclude', 'parent' => 'parent', 'shared' => 'shared', 'lazy' => 'lazy', @@ -528,7 +529,8 @@ private function parseDefinition($id, $service, $file, array $defaults) if (!is_string($service['resource'])) { throw new InvalidArgumentException(sprintf('A "resource" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file)); } - $this->registerClasses($definition, $id, $service['resource']); + $exclude = isset($service['exclude']) ? $service['exclude'] : null; + $this->registerClasses($definition, $id, $service['resource'], $exclude); } else { $this->setDefinition($id, $definition); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index 43ea65a2d8577..2de786bdb7cba 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -161,6 +161,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/OtherDir/AnotherSub/DeeperBaz.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/OtherDir/AnotherSub/DeeperBaz.php new file mode 100644 index 0000000000000..37f0e7ac45399 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/OtherDir/AnotherSub/DeeperBaz.php @@ -0,0 +1,7 @@ + - + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml index 7113c9d957505..fb47bcb7e7a52 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml @@ -1,3 +1,4 @@ services: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\: resource: ../Prototype + exclude: '../Prototype/{OtherDir}' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php index b0245c3556388..6544145c4e0e0 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php @@ -16,12 +16,15 @@ use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Loader\FileLoader; use Symfony\Component\DependencyInjection\Loader\IniFileLoader; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub\DeeperBaz; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Baz; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar; class FileLoaderTest extends TestCase @@ -90,6 +93,26 @@ public function testRegisterClasses() ); } + public function testRegisterClassesWithExclude() + { + $container = new ContainerBuilder(); + $container->setParameter('other_dir', 'OtherDir'); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + $loader->registerClasses( + new Definition(), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', + 'Prototype/*', + // load everything, except OtherDir/AnotherSub & Foo.php + 'Prototype/{%other_dir%/AnotherSub,Foo.php}' + ); + + $this->assertTrue($container->has(Bar::class)); + $this->assertTrue($container->has(Baz::class)); + $this->assertFalse($container->has(Foo::class)); + $this->assertFalse($container->has(DeeperBaz::class)); + } + /** * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException * @expectedExceptionMessageRegExp /Expected to find class "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\Prototype\\Bar" in file ".+" while importing services from resource "Prototype\/Sub\/\*", but it was not found\! Check the namespace prefix used with the resource/ @@ -102,6 +125,35 @@ public function testRegisterClassesWithBadPrefix() // the Sub is missing from namespace prefix $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', 'Prototype/Sub/*'); } + + /** + * @dataProvider getIncompatibleExcludeTests + */ + public function testRegisterClassesWithIncompatibleExclude($resourcePattern, $excludePattern) + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + try { + $loader->registerClasses( + new Definition(), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', + $resourcePattern, + $excludePattern + ); + } catch (InvalidArgumentException $e) { + $this->assertEquals( + sprintf('Invalid "exclude" pattern when importing classes for "Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\": make sure your "exclude" pattern (%s) is a subset of the "resource" pattern (%s)', $excludePattern, $resourcePattern), + $e->getMessage() + ); + } + } + + public function getIncompatibleExcludeTests() + { + yield array('Prototype/*', 'yaml/*', false); + yield array('Prototype/OtherDir/*', 'Prototype/*', false); + } } class TestFileLoader extends FileLoader