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);