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 0564cd8

Browse filesBrowse files
[DI] Add "psr4" service attribute for PSR4-based discovery and registration
1 parent 5a38804 commit 0564cd8
Copy full SHA for 0564cd8

File tree

15 files changed

+273
-17
lines changed
Filter options

15 files changed

+273
-17
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
@@ -36,7 +36,7 @@
3636
*/
3737
class YamlFileLoader extends FileLoader
3838
{
39-
private static $keywords = array(
39+
private static $serviceKeywords = array(
4040
'alias' => 'alias',
4141
'parent' => 'parent',
4242
'class' => 'class',
@@ -62,6 +62,32 @@ class YamlFileLoader extends FileLoader
6262
'autowiring_types' => 'autowiring_types',
6363
);
6464

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

6793
/**
@@ -98,6 +124,7 @@ public function load($resource, $type = null)
98124
$this->loadFromExtensions($content);
99125

100126
// services
127+
$this->setCurrentDir(dirname($path));
101128
$this->parseDefinitions($content, $resource);
102129
}
103130

@@ -188,12 +215,11 @@ private function parseDefaults(array &$content, $file)
188215
return array();
189216
}
190217

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

194220
foreach ($defaults as $key => $default) {
195-
if (!in_array($key, $defaultKeys)) {
196-
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)));
221+
if (!isset(self::$defaultsKeywords[$key])) {
222+
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)));
197223
}
198224
}
199225
if (!isset($defaults['tags'])) {
@@ -443,7 +469,14 @@ private function parseDefinition($id, $service, $file, array $defaults)
443469
}
444470
}
445471

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

449482
/**
@@ -660,13 +693,19 @@ private function loadFromExtensions(array $content)
660693
*/
661694
private static function checkDefinition($id, array $definition, $file)
662695
{
696+
if ($throw = isset($definition['resources'])) {
697+
$keywords = static::$prototypeKeywords;
698+
} else {
699+
$keywords = static::$serviceKeywords;
700+
}
701+
663702
foreach ($definition as $key => $value) {
664-
if (!isset(static::$keywords[$key])) {
665-
@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);
666-
// @deprecated Uncomment the following statement in Symfony 4.0
667-
// and also update the corresponding unit test to make it expect
668-
// an InvalidArgumentException exception.
669-
//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)));
703+
if (!isset($keywords[$key])) {
704+
if ($throw) {
705+
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)));
706+
}
707+
708+
@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);
670709
}
671710
}
672711
}

‎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.