Skip to content

Commit eb69028

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

File tree

15 files changed

+231
-8
lines changed

15 files changed

+231
-8
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 "psr4" service attribute 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

+102
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,101 @@ public function __construct(ContainerBuilder $container, FileLocatorInterface $l
3439

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

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

+20-6
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,16 @@ private function parseDefinitions(\DOMDocument $xml, $file)
124124
if (false === $services = $xpath->query('//container:services/container:service')) {
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 ($service->hasAttribute('psr4')) {
133+
$this->registerClasses($definition, (string) $service->getAttribute('id'), (string) $service->getAttribute('psr4'));
134+
} else {
135+
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);
136+
}
132137
}
133138
}
134139
}
@@ -386,13 +391,19 @@ private function processAnonymousServices(\DOMDocument $xml, $file)
386391
$xpath->registerNamespace('container', self::NS);
387392

388393
// anonymous services as arguments/properties
389-
if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:getter[@type="service"][not(@id)]')) {
394+
if (false !== $nodes = $xpath->query('//container:argument[@type="service"]|//container:property[@type="service"]|//container:getter[@type="service"]')) {
390395
foreach ($nodes as $node) {
391-
// give it a unique name
392-
$id = sprintf('%d_%s', ++$count, hash('sha256', $file));
393-
$node->setAttribute('id', $id);
396+
if (!$services = $this->getChildren($node, 'service')) {
397+
continue;
398+
}
399+
if ($services[0]->hasAttribute('psr4')) {
400+
throw new InvalidArgumentException(sprintf('The "psr4" attribute cannot be used with inline services in %s.', $file));
401+
}
402+
if (!$node->hasAttribute('id')) {
403+
// give it a unique name
404+
$id = sprintf('%d_%s', ++$count, hash('sha256', $file));
405+
$node->setAttribute('id', $id);
394406

395-
if ($services = $this->getChildren($node, 'service')) {
396407
$definitions[$id] = array($services[0], $file, false);
397408
$services[0]->setAttribute('id', $id);
398409

@@ -406,6 +417,9 @@ private function processAnonymousServices(\DOMDocument $xml, $file)
406417
// anonymous services "in the wild"
407418
if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) {
408419
foreach ($nodes as $node) {
420+
if ($node->hasAttribute('psr4')) {
421+
throw new InvalidArgumentException(sprintf('The "psr4" attribute cannot be used with anonymous services in %s.', $file));
422+
}
409423
// give it a unique name
410424
$id = sprintf('%d_%s', ++$count, hash('sha256', $file));
411425
$node->setAttribute('id', $id);

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
class YamlFileLoader extends FileLoader
3737
{
3838
private static $keywords = array(
39+
'psr4' => 'psr4',
3940
'alias' => 'alias',
4041
'parent' => 'parent',
4142
'class' => 'class',
@@ -97,6 +98,7 @@ public function load($resource, $type = null)
9798
$this->loadFromExtensions($content);
9899

99100
// services
101+
$this->setCurrentDir(dirname($path));
100102
$this->parseDefinitions($content, $resource);
101103
}
102104

@@ -432,7 +434,11 @@ private function parseDefinition($id, $service, $file, array $defaults)
432434
}
433435
}
434436

435-
$this->container->setDefinition($id, $definition);
437+
if (isset($service['psr4'])) {
438+
$this->registerClasses($definition, $id, $service['psr4']);
439+
} else {
440+
$this->container->setDefinition($id, $definition);
441+
}
436442
}
437443

438444
/**

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

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
<xsd:element name="autowire" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
122122
</xsd:choice>
123123
<xsd:attribute name="id" type="xsd:string" />
124+
<xsd:attribute name="psr4" type="xsd:string" />
124125
<xsd:attribute name="class" type="xsd:string" />
125126
<xsd:attribute name="shared" type="boolean" />
126127
<xsd:attribute name="public" type="boolean" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4;
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\Psr4;
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\Psr4\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+
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4\" psr4="../Psr4/*" />
5+
</services>
6+
</container>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
<service id="foo">
5+
<argument type="service">
6+
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4\" psr4="../Psr4/*" />
7+
</argument>
8+
</service>
9+
</services>
10+
</container>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
services:
2+
Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4\:
3+
psr4: ../Psr4

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

+34
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\Psr4;
2427
use Symfony\Component\ExpressionLanguage\Expression;
2528

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

614+
public function testPsr4()
615+
{
616+
$container = new ContainerBuilder();
617+
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
618+
$loader->load('services_psr4.xml');
619+
620+
$ids = array_keys($container->getDefinitions());
621+
sort($ids);
622+
$this->assertSame(array(Psr4\Foo::class, Psr4\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_psr4.xml'), $resources[0]);
628+
$this->assertEquals(new DirectoryResource($fixturesDir.'Psr4', '/^$/'), $resources[1]);
629+
$resources = array_map('strval', $resources);
630+
$this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4\Foo', $resources);
631+
$this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4\Sub\Bar', $resources);
632+
}
633+
634+
/**
635+
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
636+
* @expectedExceptionMessage The "psr4" attribute cannot be used with inline services
637+
*/
638+
public function testPsr4Inline()
639+
{
640+
$container = new ContainerBuilder();
641+
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
642+
$loader->load('services_psr4_inline.xml');
643+
}
644+
611645
/**
612646
* @group legacy
613647
* @expectedDeprecation Using the attribute "class" is deprecated for the service "bar" which is defined as an alias %s.

src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php

+23
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
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\Psr4;
2427
use Symfony\Component\ExpressionLanguage\Expression;
2528

2629
class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
@@ -372,6 +375,26 @@ public function testClassFromId()
372375
$this->assertEquals(CaseSensitiveClass::class, $container->getDefinition(CaseSensitiveClass::class)->getClass());
373376
}
374377

378+
public function testPsr4()
379+
{
380+
$container = new ContainerBuilder();
381+
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
382+
$loader->load('services_psr4.yml');
383+
384+
$ids = array_keys($container->getDefinitions());
385+
sort($ids);
386+
$this->assertSame(array(Psr4\Foo::class, Psr4\Sub\Bar::class), $ids);
387+
388+
$resources = $container->getResources();
389+
390+
$fixturesDir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR;
391+
$this->assertEquals(new FileResource($fixturesDir.'yaml'.DIRECTORY_SEPARATOR.'services_psr4.yml'), $resources[0]);
392+
$this->assertEquals(new DirectoryResource($fixturesDir.'Psr4', '/^$/'), $resources[1]);
393+
$resources = array_map('strval', $resources);
394+
$this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4\Foo', $resources);
395+
$this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4\Sub\Bar', $resources);
396+
}
397+
375398
public function testDefaults()
376399
{
377400
$container = new ContainerBuilder();

src/Symfony/Component/DependencyInjection/composer.json

+2
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
"suggest": {
2727
"symfony/yaml": "",
2828
"symfony/config": "",
29+
"symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required",
2930
"symfony/expression-language": "For using expressions in service container configuration",
3031
"symfony/proxy-manager-bridge": "Generate service proxies to lazy load them"
3132
},
3233
"conflict": {
3334
"symfony/config": "<3.3",
35+
"symfony/finder": "<3.3",
3436
"symfony/yaml": "<3.2"
3537
},
3638
"autoload": {

0 commit comments

Comments
 (0)