Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dbfac50

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

File tree

13 files changed

+229
-7
lines changed

13 files changed

+229
-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: 117 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+
protected $currentDir;
2631

2732
/**
2833
* @param ContainerBuilder $container A ContainerBuilder instance
@@ -34,4 +39,116 @@ 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, $psr4Glob, $directory)
53+
{
54+
$directory = $this->locator->locate($directory, $this->currentDir, true);
55+
$directory = realpath($directory) ?: $directory;
56+
if (!$classes = $this->findClasses($psr4Glob, $directory)) {
57+
throw new InvalidArgumentException(sprintf('No classes matching "%s" were found in directory "%s".', $psr4Glob, $directory));
58+
}
59+
60+
foreach ($classes as $class) {
61+
$this->container->setDefinition($class, clone $prototype);
62+
}
63+
}
64+
65+
private function findClasses($psr4Glob, $directory)
66+
{
67+
$missingClassException = new \ReflectionException();
68+
$throwingAutoloader = function () use ($missingClassException) { throw $missingClassException; };
69+
spl_autoload_register($throwingAutoloader);
70+
$classes = array();
71+
72+
try {
73+
if (!preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++/', $psr4Glob, $match)) {
74+
throw new InvalidArgumentException(sprintf('Namespace glob does not start with a valid PSR-4 prefix: %s.', $psr4Glob));
75+
}
76+
77+
$psr4Prefix = $match[0];
78+
$classRegexp = self::patternToRegexp($psr4Glob);
79+
$excludeTest = false === strpos($psr4Glob, 'Test', strlen($psr4Prefix));
80+
81+
$flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS;
82+
$finder = new \RecursiveDirectoryIterator($directory, $flags);
83+
$extRegexp = defined('HHVM_VERSION') ? '/\\.(?:php|hh)$/' : '/\\.php$/';
84+
$prefixLen = 1 + strlen($directory);
85+
86+
// track directories only for new & removed files
87+
$this->container->addResource(new DirectoryResource(realpath($directory) ?: $directory, '/^$/'));
88+
89+
foreach (new \RecursiveIteratorIterator($finder) as $path => $info) {
90+
if (preg_match($extRegexp, $path, $m) && $info->isFile() && $info->isReadable()) {
91+
$class = str_replace('/', '\\', substr($path, $prefixLen, -strlen($m[0])));
92+
93+
if ($excludeTest && false !== strpos($class, 'Test')) {
94+
continue;
95+
}
96+
if (!preg_match($classRegexp, $class = $psr4Prefix.$class)) {
97+
continue;
98+
}
99+
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)) {
100+
continue;
101+
}
102+
if (class_exists($class, false)) {
103+
$classes[] = $class;
104+
continue;
105+
}
106+
if (!function_exists('opcache_is_script_cached') || !opcache_is_script_cached($info->getRealPath() ?: $path)) {
107+
// look for the namespace in the file
108+
$namespace = 'namespace '.substr($class, 0, strrpos($class, '\\')).';';
109+
foreach ($info->openFile() as $line) {
110+
if (false !== stripos($line, $namespace)) {
111+
$namespace = true;
112+
break;
113+
}
114+
}
115+
if (true !== $namespace) {
116+
$this->container->addResource(new FileResource($path));
117+
continue;
118+
}
119+
}
120+
try {
121+
if (class_exists($class)) {
122+
$classes[] = $class;
123+
}
124+
} catch (\ReflectionException $e) {
125+
if ($missingClassException !== $e) {
126+
throw $e;
127+
}
128+
$this->container->addResource(new FileResource($path));
129+
}
130+
}
131+
}
132+
133+
return $classes;
134+
} finally {
135+
spl_autoload_unregister($throwingAutoloader);
136+
}
137+
}
138+
139+
private static function patternToRegexp($pattern)
140+
{
141+
// Escape user input
142+
$regex = preg_quote(ltrim($pattern, '\\'));
143+
144+
// Wildcards * and **
145+
$regex = strtr($regex, array('\\*\\*' => '.*?', '\\*' => '[^\\\\]*?'));
146+
147+
// If this class does not end by a slash, anchor the end
148+
if (substr($regex, -1) !== '\\') {
149+
$regex .= '$';
150+
}
151+
152+
return '{^'.$regex.'}';
153+
}
37154
}

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

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

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

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

0 commit comments

Comments
 (0)