From 03470b788e2951e9d7edd3156fc0db3d1e1cc339 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 14 Jan 2017 12:46:46 +0100 Subject: [PATCH] [DI] Add "psr4" service attribute for PSR4-based discovery and registration --- .../Component/Config/Loader/FileLoader.php | 2 +- .../DependencyInjection/CHANGELOG.md | 1 + .../DependencyInjection/Loader/FileLoader.php | 109 ++++++++++++++++++ .../Loader/XmlFileLoader.php | 9 +- .../Loader/YamlFileLoader.php | 61 ++++++++-- .../schema/dic/services/services-1.0.xsd | 24 ++++ .../Tests/Fixtures/Prototype/Foo.php | 7 ++ .../Fixtures/Prototype/MissingParent.php | 7 ++ .../Tests/Fixtures/Prototype/Sub/Bar.php | 7 ++ .../Tests/Fixtures/xml/services_prototype.xml | 6 + .../Fixtures/yaml/services_prototype.yml | 3 + .../Tests/Loader/XmlFileLoaderTest.php | 23 ++++ .../Tests/Loader/YamlFileLoaderTest.php | 23 ++++ .../DependencyInjection/composer.json | 6 +- src/Symfony/Component/Finder/Glob.php | 2 +- 15 files changed, 273 insertions(+), 17 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/MissingParent.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Sub/Bar.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype.xml create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml diff --git a/src/Symfony/Component/Config/Loader/FileLoader.php b/src/Symfony/Component/Config/Loader/FileLoader.php index cdc4329d5215f..f0896a3b7b53e 100644 --- a/src/Symfony/Component/Config/Loader/FileLoader.php +++ b/src/Symfony/Component/Config/Loader/FileLoader.php @@ -32,7 +32,7 @@ abstract class FileLoader extends Loader */ protected $locator; - private $currentDir; + protected $currentDir; /** * Constructor. diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index e1aff752e1535..0461e74bee3bd 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 3.3.0 ----- + * [EXPERIMENTAL] added prototype services for PSR4-based discovery and registration * added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info * deprecated `ContainerBuilder::getClassResource()`, use `ContainerBuilder::getReflectionClass()` or `ContainerBuilder::addObjectResource()` instead * added `ContainerBuilder::fileExists()` for checking and tracking file or directory existence diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 90cd6bcfafa4d..c09ed5d2bdba3 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -12,8 +12,13 @@ namespace Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader; use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\Glob; /** * FileLoader is the abstract class used by all built-in loaders that are file based. @@ -34,4 +39,108 @@ public function __construct(ContainerBuilder $container, FileLocatorInterface $l parent::__construct($locator); } + + /** + * Registers a set of classes as services using PSR-4 for discovery. + * + * @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 + * + * @experimental in version 3.3 + */ + public function registerClasses(Definition $prototype, $namespace, $resource) + { + if ('\\' !== substr($namespace, -1)) { + throw new InvalidArgumentException(sprintf('Namespace prefix must end with a "\\": %s.', $namespace)); + } + if (!preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++$/', $namespace)) { + throw new InvalidArgumentException(sprintf('Namespace is not a valid PSR-4 prefix: %s.', $namespace)); + } + + $classes = $this->findClasses($namespace, $resource); + // prepare for deep cloning + $prototype = serialize($prototype); + + foreach ($classes as $class) { + $this->container->setDefinition($class, unserialize($prototype)); + } + } + + private function findClasses($namespace, $resource) + { + $classes = array(); + $extRegexp = defined('HHVM_VERSION') ? '/\\.(?:php|hh)$/' : '/\\.php$/'; + + foreach ($this->glob($resource, true, $prefixLen) as $path => $info) { + if (!preg_match($extRegexp, $path, $m) || !$info->isFile() || !$info->isReadable()) { + continue; + } + $class = $namespace.ltrim(str_replace('/', '\\', substr($path, $prefixLen, -strlen($m[0]))), '\\'); + + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $class)) { + continue; + } + if (!$r = $this->container->getReflectionClass($class, true)) { + continue; + } + if (!$r->isInterface() && !$r->isTrait()) { + $classes[] = $class; + } + } + + return $classes; + } + + private function glob($resource, $recursive, &$prefixLen = null) + { + if (strlen($resource) === $i = strcspn($resource, '*?{[')) { + $resourcePrefix = $resource; + $resource = ''; + } elseif (0 === $i) { + $resourcePrefix = '.'; + $resource = '/'.$resource; + } else { + $resourcePrefix = dirname(substr($resource, 0, 1 + $i)); + $resource = substr($resource, strlen($resourcePrefix)); + } + + $resourcePrefix = $this->locator->locate($resourcePrefix, $this->currentDir, true); + $resourcePrefix = realpath($resourcePrefix) ?: $resourcePrefix; + $prefixLen = strlen($resourcePrefix); + + // track directories only for new & removed files + $this->container->fileExists($resourcePrefix, '/^$/'); + + if (false === strpos($resource, '/**/') && (defined('GLOB_BRACE') || false === strpos($resource, '{'))) { + foreach (glob($resourcePrefix.$resource, defined('GLOB_BRACE') ? GLOB_BRACE : 0) as $path) { + if ($recursive && is_dir($path)) { + $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS; + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, $flags)) as $path => $info) { + yield $path => $info; + } + } else { + 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.', $resource)); + } + + $finder = new Finder(); + $regex = Glob::toRegex($resource); + if ($recursive) { + $regex = substr_replace($regex, '(/|$)', -2, 1); + } + + foreach ($finder->followLinks()->in($resourcePrefix) as $path => $info) { + if (preg_match($regex, substr($path, $prefixLen))) { + yield $path => $info; + } + } + } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index afc4b95baf586..a3c5f69f1456f 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -121,14 +121,19 @@ private function parseDefinitions(\DOMDocument $xml, $file) $xpath = new \DOMXPath($xml); $xpath->registerNamespace('container', self::NS); - if (false === $services = $xpath->query('//container:services/container:service')) { + if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) { return; } + $this->setCurrentDir(dirname($file)); $defaults = $this->getServiceDefaults($xml, $file); foreach ($services as $service) { if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) { - $this->container->setDefinition((string) $service->getAttribute('id'), $definition); + if ('prototype' === $service->tagName) { + $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource')); + } else { + $this->container->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 fb7216e730252..f9e755990eae9 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -36,7 +36,7 @@ */ class YamlFileLoader extends FileLoader { - private static $keywords = array( + private static $serviceKeywords = array( 'alias' => 'alias', 'parent' => 'parent', 'class' => 'class', @@ -62,6 +62,32 @@ class YamlFileLoader extends FileLoader 'autowiring_types' => 'autowiring_types', ); + private static $prototypeKeywords = array( + 'resource' => 'resource', + 'parent' => 'parent', + 'shared' => 'shared', + 'lazy' => 'lazy', + 'public' => 'public', + 'abstract' => 'abstract', + 'deprecated' => 'deprecated', + 'factory' => 'factory', + 'arguments' => 'arguments', + 'properties' => 'properties', + 'getters' => 'getters', + 'configurator' => 'configurator', + 'calls' => 'calls', + 'tags' => 'tags', + 'inherit_tags' => 'inherit_tags', + 'autowire' => 'autowire', + ); + + private static $defaultsKeywords = array( + 'public' => 'public', + 'tags' => 'tags', + 'inherit_tags' => 'inherit_tags', + 'autowire' => 'autowire', + ); + private $yamlParser; /** @@ -98,6 +124,7 @@ public function load($resource, $type = null) $this->loadFromExtensions($content); // services + $this->setCurrentDir(dirname($path)); $this->parseDefinitions($content, $resource); } @@ -188,12 +215,11 @@ private function parseDefaults(array &$content, $file) return array(); } - $defaultKeys = array('public', 'tags', 'inherit_tags', 'autowire'); unset($content['services']['_defaults']); foreach ($defaults as $key => $default) { - if (!in_array($key, $defaultKeys)) { - throw new InvalidArgumentException(sprintf('The configuration key "%s" cannot be used to define a default value in "%s". Allowed keys are "%s".', $key, $file, implode('", "', $defaultKeys))); + if (!isset(self::$defaultsKeywords[$key])) { + throw new InvalidArgumentException(sprintf('The configuration key "%s" cannot be used to define a default value in "%s". Allowed keys are "%s".', $key, $file, implode('", "', self::$defaultsKeywords))); } } if (!isset($defaults['tags'])) { @@ -443,7 +469,14 @@ private function parseDefinition($id, $service, $file, array $defaults) } } - $this->container->setDefinition($id, $definition); + if (array_key_exists('resource', $service)) { + 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']); + } else { + $this->container->setDefinition($id, $definition); + } } /** @@ -660,13 +693,19 @@ private function loadFromExtensions(array $content) */ private static function checkDefinition($id, array $definition, $file) { + if ($throw = isset($definition['resource'])) { + $keywords = static::$prototypeKeywords; + } else { + $keywords = static::$serviceKeywords; + } + foreach ($definition as $key => $value) { - if (!isset(static::$keywords[$key])) { - @trigger_error(sprintf('The configuration key "%s" is unsupported for service definition "%s" in "%s". Allowed configuration keys are "%s". The YamlFileLoader object will raise an exception instead in Symfony 4.0 when detecting an unsupported service configuration key.', $key, $id, $file, implode('", "', static::$keywords)), E_USER_DEPRECATED); - // @deprecated Uncomment the following statement in Symfony 4.0 - // and also update the corresponding unit test to make it expect - // an InvalidArgumentException exception. - //throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported for service definition "%s" in "%s". Allowed configuration keys are "%s".', $key, $id, $file, implode('", "', static::$keywords))); + if (!isset($keywords[$key])) { + if ($throw) { + throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported for definition "%s" in "%s". Allowed configuration keys are "%s".', $key, $id, $file, implode('", "', $keywords))); + } + + @trigger_error(sprintf('The configuration key "%s" is unsupported for service definition "%s" in "%s". Allowed configuration keys are "%s". The YamlFileLoader object will raise an exception instead in Symfony 4.0 when detecting an unsupported service configuration key.', $key, $id, $file, implode('", "', $keywords)), E_USER_DEPRECATED); } } } 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 2a90dae6cf8d4..fd5bb7b5d1da4 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 @@ -54,6 +54,7 @@ + @@ -136,6 +137,29 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php new file mode 100644 index 0000000000000..1e4f283c8f16e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.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 new file mode 100644 index 0000000000000..7113c9d957505 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml @@ -0,0 +1,3 @@ +services: + Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\: + resource: ../Prototype diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index b8673a54d94c7..623768ba4df68 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -20,7 +20,10 @@ 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\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype; use Symfony\Component\ExpressionLanguage\Expression; class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase @@ -608,6 +611,26 @@ public function testClassFromId() $this->assertEquals(CaseSensitiveClass::class, $container->getDefinition(CaseSensitiveClass::class)->getClass()); } + public function testPrototype() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_prototype.xml'); + + $ids = array_keys($container->getDefinitions()); + sort($ids); + $this->assertSame(array(Prototype\Foo::class, Prototype\Sub\Bar::class), $ids); + + $resources = $container->getResources(); + + $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)); + $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); + } + /** * @group legacy * @expectedDeprecation Using the attribute "class" is deprecated for the service "bar" which is defined as an alias %s. diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 91686d90b781b..de417b59cc56d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -20,7 +20,10 @@ 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\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype; use Symfony\Component\ExpressionLanguage\Expression; class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase @@ -372,6 +375,26 @@ public function testClassFromId() $this->assertEquals(CaseSensitiveClass::class, $container->getDefinition(CaseSensitiveClass::class)->getClass()); } + public function testPrototype() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_prototype.yml'); + + $ids = array_keys($container->getDefinitions()); + sort($ids); + $this->assertSame(array(Prototype\Foo::class, Prototype\Sub\Bar::class), $ids); + + $resources = $container->getResources(); + + $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)); + $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); + } + public function testDefaults() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index f029accc314c4..3a2916635ffb8 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -27,12 +27,14 @@ "suggest": { "symfony/yaml": "", "symfony/config": "", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", "symfony/expression-language": "For using expressions in service container configuration", "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them" }, "conflict": { - "symfony/yaml": "<3.3", - "symfony/config": "<3.3" + "symfony/config": "<3.3", + "symfony/finder": "<3.3", + "symfony/yaml": "<3.3" }, "provide": { "psr/container-implementation": "1.0" diff --git a/src/Symfony/Component/Finder/Glob.php b/src/Symfony/Component/Finder/Glob.php index 8a439411fbb6c..1fd508f2581f8 100644 --- a/src/Symfony/Component/Finder/Glob.php +++ b/src/Symfony/Component/Finder/Glob.php @@ -61,7 +61,7 @@ public static function toRegex($glob, $strictLeadingDot = true, $strictWildcardS $firstByte = '/' === $car; if ($firstByte && $strictWildcardSlash && isset($glob[$i + 3]) && '**/' === $glob[$i + 1].$glob[$i + 2].$glob[$i + 3]) { - $car = $strictLeadingDot ? '/((?=[^\.])[^/]+/)*' : '/([^/]+/)*'; + $car = $strictLeadingDot ? '/(?:(?=[^\.])[^/]++/)*' : '/(?:[^/]++/)*'; $i += 3; if ('/' === $delimiter) { $car = str_replace('/', '\\/', $car); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy