Skip to content

Commit 28420c2

Browse files
[DI] Add "psr4" service attribute for PSR4-based discovery and registration
1 parent c43b85e commit 28420c2

File tree

15 files changed

+281
-16
lines changed

15 files changed

+281
-16
lines changed

src/Symfony/Component/Config/Loader/FileLoader.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ abstract class FileLoader extends Loader
3232
*/
3333
protected $locator;
3434

35-
private $currentDir;
35+
protected $currentDir;
3636

3737
/**
3838
* Constructor.

src/Symfony/Component/DependencyInjection/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
3.3.0
55
-----
66

7+
* [EXPERIMENTAL] added prototype services for PSR4-based discovery and registration
78
* added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info
89
* deprecated `ContainerBuilder::getClassResource()`, use `ContainerBuilder::getReflectionClass()` or `ContainerBuilder::addObjectResource()` instead
910
* added `ContainerBuilder::fileExists()` for checking and tracking file or directory existence

src/Symfony/Component/DependencyInjection/Loader/FileLoader.php

+109
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@
1212
namespace Symfony\Component\DependencyInjection\Loader;
1313

1414
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Definition;
16+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
17+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1518
use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader;
1619
use Symfony\Component\Config\FileLocatorInterface;
20+
use Symfony\Component\Finder\Finder;
21+
use Symfony\Component\Finder\Glob;
1722

1823
/**
1924
* 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
3439

3540
parent::__construct($locator);
3641
}
42+
43+
/**
44+
* Registers a set of classes as services using PSR-4 for discovery.
45+
*
46+
* @param Definition $prototype A definition to use as template
47+
* @param string $namespace The namespace prefix of classes in the scanned directory
48+
* @param string $resourcesGlob The directory to look for classes, glob-patterns allowed
49+
*
50+
* @experimental in version 3.3
51+
*/
52+
public function registerClasses(Definition $prototype, $namespace, $resourcesGlob)
53+
{
54+
if ('\\' !== substr($namespace, -1)) {
55+
throw new InvalidArgumentException(sprintf('Namespace prefix must end with a "\\": %s.', $namespace));
56+
}
57+
if (!preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++$/', $namespace)) {
58+
throw new InvalidArgumentException(sprintf('Namespace is not a valid PSR-4 prefix: %s.', $namespace));
59+
}
60+
61+
$classes = $this->findClasses($namespace, $resourcesGlob);
62+
// prepare for deep cloning
63+
$prototype = serialize($prototype);
64+
65+
foreach ($classes as $class) {
66+
$this->container->setDefinition($class, unserialize($prototype));
67+
}
68+
}
69+
70+
private function findClasses($namespace, $resourcesGlob)
71+
{
72+
$classes = array();
73+
$extRegexp = defined('HHVM_VERSION') ? '/\\.(?:php|hh)$/' : '/\\.php$/';
74+
75+
foreach ($this->glob($resourcesGlob, true, $prefixLen) as $path => $info) {
76+
if (!preg_match($extRegexp, $path, $m) || !$info->isFile() || !$info->isReadable()) {
77+
continue;
78+
}
79+
$class = $namespace.ltrim(str_replace('/', '\\', substr($path, $prefixLen, -strlen($m[0]))), '\\');
80+
81+
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)) {
82+
continue;
83+
}
84+
if (!$r = $this->container->getReflectionClass($class, true)) {
85+
continue;
86+
}
87+
if (!$r->isInterface() && !$r->isTrait()) {
88+
$classes[] = $class;
89+
}
90+
}
91+
92+
return $classes;
93+
}
94+
95+
private function glob($resourcesGlob, $recursive, &$prefixLen = null)
96+
{
97+
if (strlen($resourcesGlob) === $i = strcspn($resourcesGlob, '*?{[')) {
98+
$resourcesPrefix = $resourcesGlob;
99+
$resourcesGlob = '';
100+
} elseif (0 === $i) {
101+
$resourcesPrefix = '.';
102+
$resourcesGlob = '/'.$resourcesGlob;
103+
} else {
104+
$resourcesPrefix = dirname(substr($resourcesGlob, 0, 1 + $i));
105+
$resourcesGlob = substr($resourcesGlob, strlen($resourcesPrefix));
106+
}
107+
108+
$resourcesPrefix = $this->locator->locate($resourcesPrefix, $this->currentDir, true);
109+
$resourcesPrefix = realpath($resourcesPrefix) ?: $resourcesPrefix;
110+
$prefixLen = strlen($resourcesPrefix);
111+
112+
// track directories only for new & removed files
113+
$this->container->fileExists($resourcesPrefix, '/^$/');
114+
115+
if (false === strpos($resourcesGlob, '/**/') && (defined('GLOB_BRACE') || false === strpos($resourcesGlob, '{'))) {
116+
foreach (glob($resourcesPrefix.$resourcesGlob, defined('GLOB_BRACE') ? GLOB_BRACE : 0) as $path) {
117+
if ($recursive && is_dir($path)) {
118+
$flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS;
119+
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, $flags)) as $path => $info) {
120+
yield $path => $info;
121+
}
122+
} else {
123+
yield $path => new \SplFileInfo($path);
124+
}
125+
}
126+
127+
return;
128+
}
129+
130+
if (!class_exists(Finder::class)) {
131+
throw new LogicException(sprintf('Extended glob pattern "%s" cannot be used as the Finder component is not installed.', $resourcesGlob));
132+
}
133+
134+
$finder = new Finder();
135+
$regex = Glob::toRegex($resourcesGlob);
136+
if ($recursive) {
137+
$regex = substr_replace($regex, '(/|$)', -2, 1);
138+
}
139+
140+
foreach ($finder->followLinks()->in($resourcesPrefix) as $path => $info) {
141+
if (preg_match($regex, substr($path, $prefixLen))) {
142+
yield $path => $info;
143+
}
144+
}
145+
}
37146
}

src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

+27-6
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,13 @@ public function load($resource, $type = null)
5656
// extensions
5757
$this->loadFromExtensions($xml);
5858

59+
$defaults = $this->getServiceDefaults($xml, $path);
60+
61+
// prototypes
62+
$this->parsePrototypes($xml, $path, $defaults);
63+
5964
// services
60-
$this->parseDefinitions($xml, $path);
65+
$this->parseDefinitions($xml, $path, $defaults);
6166
}
6267

6368
/**
@@ -110,13 +115,30 @@ private function parseImports(\DOMDocument $xml, $file)
110115
}
111116
}
112117

118+
/**
119+
* Parses multiple prototype definitions.
120+
*/
121+
private function parsePrototypes(\DOMDocument $xml, $file, array $defaults)
122+
{
123+
$xpath = new \DOMXPath($xml);
124+
$xpath->registerNamespace('container', self::NS);
125+
126+
if (false === $prototypes = $xpath->query('//container:services/container:prototype')) {
127+
return;
128+
}
129+
$this->setCurrentDir(dirname($file));
130+
131+
foreach ($prototypes as $prototype) {
132+
if (null !== $definition = $this->parseDefinition($prototype, $file, $defaults)) {
133+
$this->registerClasses($definition, (string) $prototype->getAttribute('namespace'), (string) $prototype->getAttribute('resource'));
134+
}
135+
}
136+
}
137+
113138
/**
114139
* Parses multiple definitions.
115-
*
116-
* @param \DOMDocument $xml
117-
* @param string $file
118140
*/
119-
private function parseDefinitions(\DOMDocument $xml, $file)
141+
private function parseDefinitions(\DOMDocument $xml, $file, array $defaults)
120142
{
121143
$xpath = new \DOMXPath($xml);
122144
$xpath->registerNamespace('container', self::NS);
@@ -125,7 +147,6 @@ private function parseDefinitions(\DOMDocument $xml, $file)
125147
return;
126148
}
127149

128-
$defaults = $this->getServiceDefaults($xml, $file);
129150
foreach ($services as $service) {
130151
if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
131152
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);

src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

+40-8
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
*/
3636
class YamlFileLoader extends FileLoader
3737
{
38-
private static $keywords = array(
38+
private static $serviceKeywords = array(
3939
'alias' => 'alias',
4040
'parent' => 'parent',
4141
'class' => 'class',
@@ -61,6 +61,32 @@ class YamlFileLoader extends FileLoader
6161
'autowiring_types' => 'autowiring_types',
6262
);
6363

64+
private static $prototypeKeywords = array(
65+
'resource' => 'resource',
66+
'parent' => 'parent',
67+
'shared' => 'shared',
68+
'lazy' => 'lazy',
69+
'public' => 'public',
70+
'abstract' => 'abstract',
71+
'deprecated' => 'deprecated',
72+
'factory' => 'factory',
73+
'arguments' => 'arguments',
74+
'properties' => 'properties',
75+
'getters' => 'getters',
76+
'configurator' => 'configurator',
77+
'calls' => 'calls',
78+
'tags' => 'tags',
79+
'inherit_tags' => 'inherit_tags',
80+
'autowire' => 'autowire',
81+
);
82+
83+
private static $defaultsKeywords = array(
84+
'public' => 'public',
85+
'tags' => 'tags',
86+
'inherit_tags' => 'inherit_tags',
87+
'autowire' => 'autowire',
88+
);
89+
6490
private $yamlParser;
6591

6692
/**
@@ -97,6 +123,7 @@ public function load($resource, $type = null)
97123
$this->loadFromExtensions($content);
98124

99125
// services
126+
$this->setCurrentDir(dirname($path));
100127
$this->parseDefinitions($content, $resource);
101128
}
102129

@@ -187,12 +214,11 @@ private function parseDefaults(array &$content, $file)
187214
return array();
188215
}
189216

190-
$defaultKeys = array('public', 'tags', 'inherit_tags', 'autowire');
191217
unset($content['services']['_defaults']);
192218

193219
foreach ($defaults as $key => $default) {
194-
if (!in_array($key, $defaultKeys)) {
195-
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)));
220+
if (!isset(self::$defaultsKeywords[$key])) {
221+
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)));
196222
}
197223
}
198224
if (!isset($defaults['tags'])) {
@@ -442,7 +468,11 @@ private function parseDefinition($id, $service, $file, array $defaults)
442468
}
443469
}
444470

445-
$this->container->setDefinition($id, $definition);
471+
if (isset($service['resource'])) {
472+
$this->registerClasses($definition, $id, $service['resource']);
473+
} else {
474+
$this->container->setDefinition($id, $definition);
475+
}
446476
}
447477

448478
/**
@@ -659,13 +689,15 @@ private function loadFromExtensions(array $content)
659689
*/
660690
private static function checkDefinition($id, array $definition, $file)
661691
{
692+
$keywords = isset($definition['resource']) ? static::$prototypeKeywords : static::$serviceKeywords;
693+
662694
foreach ($definition as $key => $value) {
663-
if (!isset(static::$keywords[$key])) {
664-
@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);
695+
if (!isset($keywords[$key])) {
696+
@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);
665697
// @deprecated Uncomment the following statement in Symfony 4.0
666698
// and also update the corresponding unit test to make it expect
667699
// an InvalidArgumentException exception.
668-
//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)));
700+
//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('", "', $keywords)));
669701
}
670702
}
671703
}

src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd

+24
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
</xsd:annotation>
5555
<xsd:choice maxOccurs="unbounded">
5656
<xsd:element name="service" type="service" minOccurs="1" />
57+
<xsd:element name="prototype" type="prototype" minOccurs="0" />
5758
<xsd:element name="defaults" type="defaults" minOccurs="0" maxOccurs="1" />
5859
</xsd:choice>
5960
</xsd:complexType>
@@ -136,6 +137,29 @@
136137
<xsd:attribute name="inherit-tags" type="boolean" />
137138
</xsd:complexType>
138139

140+
<xsd:complexType name="prototype">
141+
<xsd:choice maxOccurs="unbounded">
142+
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
143+
<xsd:element name="configurator" type="callable" minOccurs="0" maxOccurs="1" />
144+
<xsd:element name="factory" type="callable" minOccurs="0" maxOccurs="1" />
145+
<xsd:element name="deprecated" type="xsd:string" minOccurs="0" maxOccurs="1" />
146+
<xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
147+
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
148+
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
149+
<xsd:element name="getter" type="getter" minOccurs="0" maxOccurs="unbounded" />
150+
<xsd:element name="autowire" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
151+
</xsd:choice>
152+
<xsd:attribute name="namespace" type="xsd:string" use="required" />
153+
<xsd:attribute name="resource" type="xsd:string" use="required" />
154+
<xsd:attribute name="shared" type="boolean" />
155+
<xsd:attribute name="public" type="boolean" />
156+
<xsd:attribute name="lazy" type="boolean" />
157+
<xsd:attribute name="abstract" type="boolean" />
158+
<xsd:attribute name="parent" type="xsd:string" />
159+
<xsd:attribute name="autowire" type="boolean" />
160+
<xsd:attribute name="inherit-tags" type="boolean" />
161+
</xsd:complexType>
162+
139163
<xsd:complexType name="tag">
140164
<xsd:attribute name="name" type="xsd:string" use="required" />
141165
<xsd:anyAttribute namespace="##any" processContents="lax" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
4+
5+
class Foo
6+
{
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
4+
5+
class MissingParent extends NotExistingParent
6+
{
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub;
4+
5+
class Bar
6+
{
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<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">
3+
<services>
4+
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*" />
5+
</services>
6+
</container>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
services:
2+
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\:
3+
resource: ../Prototype

0 commit comments

Comments
 (0)