8000 [DI] Add "psr4" service attribute for PSR4-based discovery and regist… · symfony/symfony@5389d98 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5389d98

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

File tree

12 files changed

+210
-7
lines changed

12 files changed

+210
-7
lines changed

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines 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+
* added "psr4" service attribute for PSR4-based discovery and registration
78
* deprecated case insensitivity of service identifiers
89
* added "iterator" argument type for lazy iteration over a set of values and services
910
* added "closure-proxy" argument type for turning services' methods into lazy callables

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
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;
1517
use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader;
1618
use Symfony\Component\Config\FileLocatorInterface;
19+
use Symfony\Component\Config\Resource\DirectoryResource;
20+
use Symfony\Component\Config\Resource\FileResource;
1721

1822
/**
1923
* FileLoader is the abstract class used by all built-in loaders that are file based.
@@ -23,6 +27,7 @@
2327
abstract class FileLoader extends BaseFileLoader
2428
{
2529
protected $container;
30+
private $currentDir;
2631

2732
/**
2833
* @param ContainerBuilder $container A ContainerBuilder instance
@@ -34,4 +39,108 @@ public function __construct(ContainerBuilder $container, FileLocatorInterface $l
3439

3540
parent::__construct($locator);
3641
}
42+
43+
/**
44+
* {@inheritdoc}
45+
*/
46+
public function setCurrentDir($dir)
47+
{
48+
$this->currentDir = $dir;
49+
parent::setCurrentDir($dir);
50+
}
51+
52+
public function registerClasses(Definition $prototype, $namespace, $directoryGlob)
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+
if (strlen($directoryGlob) === $i = strcspn($directoryGlob, '*?{[')) {
62+
$directoryPrefix = $directoryGlob;
63+
$directoryGlob = '';
64+
} else {
65+
$directoryPrefix = dirname(substr($directoryGlob, 0, 1 + $i));
66+
$directoryGlob = substr($directoryGlob, strlen($directoryPrefix));
67+
}
68+
69+
$directoryPrefix = $this->locator->locate($directoryPrefix, $this->currentDir, true);
70+
$directoryGlob = $directoryPrefix.$directoryGlob;
71+
72+
// track directories only for new & removed files
73+
$this->container->addResource(new DirectoryResource($directoryPrefix, '/^$/'));
74+
75+
if (!$classes = $this->findClasses($namespace, $directoryGlob, strlen($directoryPrefix))) {
76+
$this->container->getCompiler()->addLogMessage(sprintf('No classes found in "%s" for namespace "%s".', $directoryGlob, $namespace));
77+
}
78+
79+
foreach ($classes as $class) {
80+
$this->container->setDefinition($class, clone $prototype);
81+
}
82+
}
83+
84+
private function findClasses($namespace, $directoryGlob, $prefixLen)
85+
{
86+
$classes = array();
87+
$extRegexp = defined('HHVM_VERSION') ? '/\\.(?:php|hh)$/' : '/\\.php$/';
88+
$missingClassException = new \ReflectionException();
89+
$throwingAutoloader = function () use ($missingClassException) { throw $missingClassException; };
90+
spl_autoload_register($throwingAutoloader);
91+
92+
try {
93+
foreach (glob($directoryGlob, defined('GLOB_BRACE') ? GLOB_BRACE : 0) as $path) {
94+
if (is_dir($path)) {
95+
$flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS;
96+
$files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, $flags));
97+
} else {
98+
$files = array($path => new \SplFileInfo($path));
99+
}
100+
101+
foreach ($files as $path => $info) {
102+
if (!preg_match($extRegexp, $path, $m) || !$info->isFile() || !$info->isReadable()) {
103+
continue;
104+
}
105+
$class = $namespace.ltrim(str_replace('/', '\\', substr($path, $prefixLen, -strlen($m[0]))), '\\');
106+
107+
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)) {
108+
continue;
109+
}
110+
if (class_exists($class, false)) {
111+
$classes[] = $class;
112+
continue;
113+
}
114+
if (!function_exists('opcache_is_script_cached') || !opcache_is_script_cached($info->getRealPath() ?: $path)) {
115+
// look for the namespace in the file
116+
$ns = 'namespace '.substr($class, 0, strrpos($class, '\\')).';';
117+
foreach ($info->openFile() as $line) {
118+
if (false !== stripos($line, $ns)) {
119+
$ns = true;
120+
break;
121+
}
122+
}
123+
if (true !== $ns) {
124+
$this->container->addResource(new FileResource($path));
125+
continue;
126+
}
127+
}
128+
try {
129+
if (class_exists($class)) {
130+
$classes[] = $class;
131+
}
132+
} catch (\ReflectionException $e) {
133+
if ($missingClassException !== $e) {
134+
throw $e;
135+
}
136+
$this->container->addResource(new FileResource($path));
137+
}
138+
}
139+
}
140+
141+
return $classes;
142+
} finally {
143+
spl_autoload_unregister($throwingAutoloader);
144+
}
145+
}
37146
}

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,15 @@ 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
foreach ($services as $service) {
129130
if (null !== $definition = $this->parseDefinition($service, $file, $this->getServiceDefaults($xml, $file))) {
130-
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);
131+
if ($service->hasAttribute('psr4')) {
132+
$this->registerClasses($definition, (string) $service->getAttribute('id'), (string) $service->getAttribute('psr4'));
133+
} else {
134+
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);
135+
}
131136
}
132137
}
133138
}
@@ -384,13 +389,19 @@ private function processAnonymousServices(\DOMDocument $xml, $file)
384389
$xpath->registerNamespace('container', self::NS);
385390

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

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

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

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

Lines changed: 7 additions & 1 deletion
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',
@@ -96,6 +97,7 @@ public function load($resource, $type = null)
9697
$this->loadFromExtensions($content);
9798

9899
// services
100+
$this->setCurrentDir(dirname($path));
99101
$this->parseDefinitions($content, $resource);
100102
}
101103

@@ -410,7 +412,11 @@ private function parseDefinition($id, $service, $file, array $defaults)
410412
}
411413
}
412414

413-
$this->container->setDefinition($id, $definition);
415+
if (isset($service['psr4'])) {
416+
$this->registerClasses($definition, $id, $service['psr4']);
417+
} else {
418+
$this->container->setDefinition($id, $definition);
419+
}
414420
}
415421

416422
/**

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
<xsd:element name="autowire" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
121121
</xsd:choice>
122122
<xsd:attribute name="id" type="xsd:string" />
123+
<xsd:attribute name="psr4" type="xsd:string" />
123124
<xsd:attribute name="class" type="xsd:string" />
124125
<xsd:attribute name="shared" type="boolean" />
125126
<xsd:attribute name="public" type="boolean" />
Lines 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\Psr4;
4+
5+
class Foo
6+
{
7+
}
Lines 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\Psr4;
4+
5+
class MissingParent extends NotExistingParent
6+
{
7+
}
Lines 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\Psr4\Sub;
4+
5+
class Bar
6+
{
7+
}
Lines 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+
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\Psr4\" psr4="../Psr4/*" />
5+
</services>
6+
</container>
Lines 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\Psr4\:
3+
psr4: ../Psr4

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

Lines changed: 21 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\Psr4;
2427
use Symfony\Component\ExpressionLanguage\Expression;
2528

2629
class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
@@ -595,6 +598,24 @@ public function testClassFromId()
595598
$this->assertEquals(CaseSensitiveClass::class, $container->getDefinition(CaseSensitiveClass::class)->getClass());
596599
}
597600

601+
public function testPsr4()
602+
{
603+
$container = new ContainerBuilder();
604+
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
605+
$loader->load('services_psr4.xml');
606+
607+
$ids = array_keys($container->getDefinitions());
608+
sort($ids);
609+
$this->assertSame(array(Psr4\Foo::class, Psr4\Sub\Bar::class), $ids);
610+
611+
$resources = $container->getResources();
612+
613+
$fixturesDir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR;
614+
$this->assertEquals(new FileResource($fixturesDir.'xml'.DIRECTORY_SEPARATOR.'services_psr4.xml'), $resources[0]);
615+
$this->assertEquals(new DirectoryResource($fixturesDir.'Psr4', '/^$/'), $resources[1]);
616+
$this->assertEquals(new FileResource($fixturesDir.'Psr4'.DIRECTORY_SEPARATOR.'MissingParent.php'), $resources[2]);
617+
}
618+
598619
/**
599620
* @group legacy
600621
* @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

Lines changed: 21 additions & 0 deletions
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
@@ -367,6 +370,24 @@ public function testClassFromId()
367370
$this->assertEquals(CaseSensitiveClass::class, $container->getDefinition(CaseSensitiveClass::class)->getClass());
368371
}
369372

373+
public function testPsr4()
374+
{
375+
$container = new ContainerBuilder();
376+
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
377+
$loader->load('services_psr4.yml');
378+
379+
$ids = array_keys($container->getDefinitions());
380+
sort($ids);
381+
$this->assertSame(array(Psr4\Foo::class, Psr4\Sub\Bar::class), $ids);
382+
383+
$resources = $container->getResources();
384+
385+
$fixturesDir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR;
386+
$this->assertEquals(new FileResource($fixturesDir.'yaml'.DIRECTORY_SEPARATOR.'services_psr4.yml'), $resources[0]);
387+
$this->assertEquals(new DirectoryResource($fixturesDir.'Psr4', '/^$/'), $resources[1]);
388+
$this->assertEquals(new FileResource($fixturesDir.'Psr4'.DIRECTORY_SEPARATOR.'MissingParent.php'), $resources[2]);
389+
}
390+
370391
public function testDefaults()
371392
{
372393
$container = new ContainerBuilder();

0 commit comments

Comments
 (0)
0