Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 75d66d8

Browse filesBrowse files
[DI] Add "psr4" service attribute for PSR4-based discovery and registration
1 parent c43b85e commit 75d66d8
Copy full SHA for 75d66d8

File tree

Expand file treeCollapse file tree

15 files changed

+271
-15
lines changed
Filter options
Expand file treeCollapse file tree

15 files changed

+271
-15
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Config/Loader/FileLoader.php
+1-1Lines changed: 1 addition & 1 deletion
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
+109Lines changed: 109 additions & 0 deletions
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 $resources The directory to look for classes, glob-patterns allowed
49+
*
50+
* @experimental in version 3.3
51+
*/
52+
public function registerClasses(Definition $prototype, $namespace, $resources)
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, $resources);
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, $resources)
71+
{
72+
$classes = array();
73+
$extRegexp = defined('HHVM_VERSION') ? '/\\.(?:php|hh)$/' : '/\\.php$/';
74+
75+
foreach ($this->glob($resources, 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($resources, $recursive, &$prefixLen = null)
96+
{
97+
if (strlen($resources) === $i = strcspn($resources, '*?{[')) {
98+
$resourcesPrefix = $resources;
99+
$resources = '';
100+
} elseif (0 === $i) {
101+
$resourcesPrefix = '.';
102+
$resources = '/'.$resources;
103+
} else {
104+
$resourcesPrefix = dirname(substr($resources, 0, 1 + $i));
105+
$resources = substr($resources, 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($resources, '/**/') && (defined('GLOB_BRACE') || false === strpos($resources, '{'))) {
116+
foreach (glob($resourcesPrefix.$resources, 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.', $resources));
132+
}
133+
134+
$finder = new Finder();
135+
$regex = Glob::toRegex($resources);
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+7-2Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,19 @@ private function parseDefinitions(\DOMDocument $xml, $file)
121121
$xpath = new \DOMXPath($xml);
122122
$xpath->registerNamespace('container', self::NS);
123123

124-
if (false === $services = $xpath->query('//container:services/container:service')) {
124+
if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) {
125125
return;
126126
}
127+
$this->setCurrentDir(dirname($file));
127128

128129
$defaults = $this->getServiceDefaults($xml, $file);
129130
foreach ($services as $service) {
130131
if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
131-
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);
132+
if ('prototype' === $service->tagName) {
133+
$this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resources'));
134+
} else {
135+
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);
136+
}
132137
}
133138
}
134139
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+50-11Lines changed: 50 additions & 11 deletions
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+
'resources' => 'resources',
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,14 @@ private function parseDefinition($id, $service, $file, array $defaults)
442468
}
443469
}
444470

445-
$this->container->setDefinition($id, $definition);
471+
if (array_key_exists('resources', $service)) {
472+
if (!is_string($service['resources'])) {
473+
throw new InvalidArgumentException(sprintf('A "resources" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file));
474+
}
475+
$this->registerClasses($definition, $id, $service['resources']);
476+
} else {
477+
$this->container->setDefinition($id, $definition);
478+
}
446479
}
447480

448481
/**
@@ -659,13 +692,19 @@ private function loadFromExtensions(array $content)
659692
*/
660693
private static function checkDefinition($id, array $definition, $file)
661694
{
695+
if ($throw = isset($definition['resources'])) {
696+
$keywords = static::$prototypeKeywords;
697+
} else {
698+
$keywords = static::$serviceKeywords;
699+
}
700+
662701
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);
665-
// @deprecated Uncomment the following statement in Symfony 4.0
666-
// and also update the corresponding unit test to make it expect
667-
// 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)));
702+
if (!isset($keywords[$key])) {
703+
if ($throw) {
704+
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)));
705+
}
706+
707+
@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);
669708
}
670709
}
671710
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
+24Lines changed: 24 additions & 0 deletions
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="resources" 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" />
+7Lines changed: 7 additions & 0 deletions
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+
}
+7Lines changed: 7 additions & 0 deletions
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+
}
+7Lines changed: 7 additions & 0 deletions
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+
}
+6Lines changed: 6 additions & 0 deletions
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\" resources="../Prototype/*" />
5+
</services>
6+
</container>
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
services:
2+
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\:
3+
resources: ../Prototype

‎src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
+23Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
2121
use Symfony\Component\Config\Loader\LoaderResolver;
2222
use Symfony\Component\Config\FileLocator;
23+
use Symfony\Component\Config\Resource\DirectoryResource;
24+
use Symfony\Component\Config\Resource\FileResource;
2325
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
26+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
2427
use Symfony\Component\ExpressionLanguage\Expression;
2528

2629
class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
@@ -608,6 +611,26 @@ public function testClassFromId()
608611
$this->assertEquals(CaseSensitiveClass::class, $container->getDefinition(CaseSensitiveClass::class)->getClass());
609612
}
610613

614+
public function testPrototype()
615+
{
616+
$container = new ContainerBuilder();
617+
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
618+
$loader->load('services_prototype.xml');
619+
620+
$ids = array_keys($container->getDefinitions());
621+
sort($ids);
622+
$this->assertSame(array(Prototype\Foo::class, Prototype\Sub\Bar::class), $ids);
623+
624+
$resources = $container->getResources();
625+
626+
$fixturesDir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR;
627+
$this->assertEquals(new FileResource($fixturesDir.'xml'.DIRECTORY_SEPARATOR.'services_prototype.xml'), $resources[0]);
628+
$this->assertEquals(new DirectoryResource($fixturesDir.'Prototype', '/^$/'), $resources[1]);
629+
$resources = array_map('strval', $resources);
630+
$this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo', $resources);
631+
$this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar', $resources);
632+
}
633+
611634
/**
612635
* @group legacy
613636
* @expectedDeprecation Using the attribute "class" is deprecated for the service "bar" which is defined as an alias %s.

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.