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

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 ccda044

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

File tree

12 files changed

+211
-7
lines changed

12 files changed

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

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