[DI] Add "psr4" service attribute for PSR4-based discovery and registration
This commit is contained in:
parent
5a388042bb
commit
03470b788e
@ -32,7 +32,7 @@ abstract class FileLoader extends Loader
|
||||
*/
|
||||
protected $locator;
|
||||
|
||||
private $currentDir;
|
||||
protected $currentDir;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
@ -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
|
||||
|
@ -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 @@ abstract class FileLoader extends BaseFileLoader
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,14 +121,19 @@ class XmlFileLoader extends FileLoader
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ use Symfony\Component\ExpressionLanguage\Expression;
|
||||
*/
|
||||
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 @@ class YamlFileLoader extends FileLoader
|
||||
$this->loadFromExtensions($content);
|
||||
|
||||
// services
|
||||
$this->setCurrentDir(dirname($path));
|
||||
$this->parseDefinitions($content, $resource);
|
||||
}
|
||||
|
||||
@ -188,12 +215,11 @@ class YamlFileLoader extends FileLoader
|
||||
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 @@ class YamlFileLoader extends FileLoader
|
||||
}
|
||||
}
|
||||
|
||||
$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 @@ class YamlFileLoader extends FileLoader
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@
|
||||
</xsd:annotation>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="service" type="service" minOccurs="1" />
|
||||
<xsd:element name="prototype" type="prototype" minOccurs="0" />
|
||||
<xsd:element name="defaults" type="defaults" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
@ -136,6 +137,29 @@
|
||||
<xsd:attribute name="inherit-tags" type="boolean" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="prototype">
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
|
||||
<xsd:element name="configurator" type="callable" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="factory" type="callable" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="deprecated" type="xsd:string" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
|
||||
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
|
||||
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
|
||||
<xsd:element name="getter" type="getter" minOccurs="0" maxOccurs="unbounded" />
|
||||
<xsd:element name="autowire" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
||||
</xsd:choice>
|
||||
<xsd:attribute name="namespace" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="resource" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="shared" type="boolean" />
|
||||
<xsd:attribute name="public" type="boolean" />
|
||||
<xsd:attribute name="lazy" type="boolean" />
|
||||
<xsd:attribute name="abstract" type="boolean" />
|
||||
<xsd:attribute name="parent" type="xsd:string" />
|
||||
<xsd:attribute name="autowire" type="boolean" />
|
||||
<xsd:attribute name="inherit-tags" type="boolean" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="tag">
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
<xsd:anyAttribute namespace="##any" processContents="lax" />
|
||||
|
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
|
||||
|
||||
class Foo
|
||||
{
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
|
||||
|
||||
class MissingParent extends NotExistingParent
|
||||
{
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub;
|
||||
|
||||
class Bar
|
||||
{
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
<services>
|
||||
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*" />
|
||||
</services>
|
||||
</container>
|
@ -0,0 +1,3 @@
|
||||
services:
|
||||
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\:
|
||||
resource: ../Prototype
|
@ -20,7 +20,10 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
|
||||
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 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
|
||||
$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.
|
||||
|
@ -20,7 +20,10 @@ use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
|
||||
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 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
|
||||
$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();
|
||||
|
@ -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"
|
||||
|
@ -61,7 +61,7 @@ class Glob
|
||||
$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);
|
||||
|
Reference in New Issue
Block a user