8000 [DI][Config] Add & use ReflectionClassResource · symfony/symfony@e251e19 · GitHub
[go: up one dir, main page]

Skip to content

Commit e251e19

Browse files
[DI][Config] Add & use ReflectionClassResource
1 parent b4ff1c8 commit e251e19

28 files changed

+589
-181
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
use Symfony\Component\DependencyInjection\ContainerBuilder;
1515
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1617
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\Translation\TranslatorInterface;
19+
use Symfony\Component\Translation\TranslatorBagInterface;
1720

1821
/**
1922
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
@@ -31,7 +34,9 @@ public function process(ContainerBuilder $container)
3134
$definition = $container->getDefinition((string) $translatorAlias);
3235
$class = $container->getParameterBag()->resolveValue($definition->getClass());
3336

34-
if (is_subclass_of($class, 'Symfony\Component\Translation\TranslatorInterface') && is_subclass_of($class, 'Symfony\Component\Translation\TranslatorBagInterface')) {
37+
if (!$r = $container->getReflectionClass($class)) {
38+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $translatorAlias));
39+
} elseif ($r->isSubclassOf(TranslatorInterface::class) && $r->isSubclassOf(TranslatorBagInterface::class)) {
3540
$container->getDefinition('translator.logging')->setDecoratedService('translator');
3641
$container->getDefinition('translation.warmer')->replaceArgument(0, new Reference('translator.logging.inner'));
3742
}

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/AddConsoleCommandPassTest.php

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,6 @@ public function visibilityProvider()
6161
);
6262
}
6363

64-
/**
65-
* @expectedException \InvalidArgumentException
66-
* @expectedExceptionMessage The service "my-command" tagged "console.command" must not be abstract.
67-
*/
68-
public function testProcessThrowAnExceptionIfTheServiceIsAbstract()
69-
{
70-
$container = new ContainerBuilder();
71-
$container->addCompilerPass(new AddConsoleCommandPass());
72-
73-
$definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\MyCommand');
74-
$definition->addTag('console.command');
75-
$definition->setAbstract(true);
76-
$container->setDefinition('my-command', $definition);
77-
78-
$container->compile();
79-
}
80-
8164
/**
8265
* @expectedException \InvalidArgumentException
8366
* @expectedExceptionMessage The service "my-command" tagged "console.command" must be a subclass of "Symfony\Component\Console\Command\Command".

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public function testProcess()
5454
->method('getParameterBag')
5555
->will($this->returnValue($parameterBag));
5656

57+
$container->expects($this->once())
58+
->method('getReflectionClass')
59+
->with('Symfony\Bundle\FrameworkBundle\Translation\Translator')
60+
->will($this->returnValue(new \ReflectionClass('Symfony\Bundle\FrameworkBundle\Translation\Translator')));
61+
5762
$pass = new LoggingTranslatorPass();
5863
$pass->process($container);
5964
}

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"symfony/dependency-injection": "~3.3",
2323
"symfony/config": "~3.3",
2424
"symfony/event-dispatcher": "~3.3",
25-
"symfony/http-foundation": "~3.1",
25+
"symfony/http-foundation": "~3.3",
2626
"symfony/http-kernel": "~3.3",
2727
"symfony/polyfill-mbstring": "~1.0",
2828
"symfony/filesystem": "~2.8|~3.0",

src/Symfony/Component/Config/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
CHANGELOG
22
=========
33

4+
3.3.0
5+
-----
6+
7+
* added `ReflectionClassResource` class
8+
* added second `$exists` constructor argument to `ClassExistenceResource`
9+
* made `ClassExistenceResource` work with interfaces and traits
10+
411
3.0.0
512
-----
613

src/Symfony/Component/Config/Resource/ClassExistenceResource.php

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,27 @@
2121
*/
2222
class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializable
2323
{
24+
const EXISTS_OK = 1;
25+
const EXISTS_KO = 0;
26+
const EXISTS_KO_WITH_THROWING_AUTOLOADER = -1;
27+
2428
private $resource;
2529
private $exists;
2630

31+
private static $checkingLevel = 0;
32+
private static $throwingAutoloader;
33+
private static $existsCache = array();
34+
2735
/**
28-
* @param string $resource The fully-qualified class name
36+
* @param string $resource The fully-qualified class name
37+
* @param int|null $exists One of the self::EXISTS_* const if the existency check has already been done
2938
*/
30-
public function __construct($resource)
39+
public function __construct($resource, $exists = null)
3140
{
3241
$this->resource = $resource;
33-
$this->exists = class_exists($resource);
42+
if (null !== $exists) {
43+
$this->exists = (int) $exists;
44+
}
3445
}
3546

3647
/**
@@ -54,14 +65,46 @@ public function getResource()
5465
*/
5566
public function isFresh($timestamp)
5667
{
57-
return class_exists($this->resource) === $this->exists;
68+
if (null !== $exists = &self::$existsCache[$this->resource]) {
69+
$exists = $exists || class_exists($this->resource, false) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
70+
} elseif (self::EXISTS_KO_WITH_THROWING_AUTOLOADER === $this->exists) {
71+
if (null === self::$throwingAutoloader) {
72+
$signalingException = new \ReflectionException();
73+
self::$throwingAutoloader = function () use ($signalingException) { throw $signalingException; };
74+
}
75+
if (!self::$checkingLevel++) {
76+
spl_autoload_register(self::$throwingAutoloader);
77+
}
78+
79+
try {
80+
$exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
81+
} catch (\ReflectionException $e) {
82+
$exists = false;
83+
} finally {
84+
if (!--self::$checkingLevel) {
85+
spl_autoload_unregister(self::$throwingAutoloader);
86+
}
87+
}
88+
} else {
89+
$exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
90+
}
91+
92+
if (null === $this->exists) {
93+
$this->exists = $exists ? self::EXISTS_OK : self::EXISTS_KO;
94+
}
95+
96+
return self::EXISTS_OK === $this->exists xor !$exists;
5897
}
5998

6099
/**
61100
* {@inheritdoc}
62101
*/
63102
public function serialize()
64103
{
104+
if (null === $this->exists) {
105+
$this->isFresh(0);
106+
}
107+
65108
return serialize(array($this->resource, $this->exists));
66109
}
67110

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Config\Resource;
13+
14+
/**
15+
* @author Nicolas Grekas <p@tchwork.com>
16+
*/
17+
class ReflectionClassResource implements SelfCheckingResourceInterface, \Serializable
18+
{
19+
private $files = array();
20+
private $className;
21+
private $classReflector;
22+
private $hash;
23+
24+
private static $mtimeCache = array();
25+
26+
public function __construct(\ReflectionClass $classReflector)
27+
{
28+
$this->className = $classReflector->name;
29+
$this->classReflector = $classReflector;
30+
}
31+
32+
public function isFresh($timestamp)
33+
{
34+
if (null === $this->hash) {
35+
$this->hash = $this->computeHash();
36+
$this->loadFiles($this->classReflector);
37+
}
38+
39+
foreach ($this->files as $file => $v) {
40+
if (!file_exists($file)) {
41+
return false;
42+
}
43+
44+
if (null === $mtime = &self::$mtimeCache[$file]) {
45+
$mtime = @filemtime($file);
46+
}
47+
if ($mtime > $timestamp) {
48+
if ($this->hash !== $this->computeHash()) {
49+
return false;
50+
}
51+
52+
// ensure the next checks won't require a hash computation again,
53+
// use a local mtime cache so that the current process still sees
54+
// the original mtime and validates the hash for child classes
55+
@touch($file, $timestamp);
56+
57+
return true;
58+
}
59+
}
60+
61+
return true;
62+
}
63+
64+
public function __toString()
65+
{
66+
return 'reflection.'.$this->className;
67+
}
68+
69+
public function serialize()
70+
{
71+
if (null === $this->hash) {
72+
$this->hash = $this->computeHash();
73+
$this->loadFiles($this->classReflector);
74+
}
75+
76+
return serialize(array($this->files, $this->className, $this->hash));
77+
}
78+
79+
public function unserialize($serialized)
80+
{
81+
list($this->files, $this->className, $this->hash) = unserialize($serialized);
82+
}
83+
84+
private function loadFiles(\ReflectionClass $class)
85+
{
86+
foreach ($class->getInterfaces() as $v) {
87+
$this->loadFiles($v);
88+
}
89+
do {
90+
$file = $class->getFileName();
91+
if (false !== $file && file_exists($file)) {
92+
$this->files[$file] = null;
93+
}
94+
foreach ($class->getTraits() as $v) {
95+
$this->loadFiles($v);
96+
}
97+
} while ($class = $class->getParentClass());
98+
}
99+
100+
private function computeHash()
101+
{
102+
if (null === $this->classReflector) {
103+
try {
104+
$this->classReflector = new \ReflectionClass($this->className);
105+
} catch (\ReflectionException $e) {
106+
// the class does not exist anymore
107+
return false;
108+
}
109+
}
110+
$hash = hash_init('md5');
111+
112+
foreach ($this->generateSignature($this->classReflector) as $info) {
113+
hash_update($hash, $info);
114+
}
115+
116+
return hash_final($hash);
117+
}
118+
119+
private function generateSignature(\ReflectionClass $class)
120+
{
121+
yield $class->getDocComment().$class->getModifiers();
122+
123+
if ($class->isTrait()) {
124+
yield print_r(class_uses($class->name), true);
125+
} else {
126+
yield print_r(class_parents($class->name), true);
127+
yield print_r(class_implements($class->name), true);
128+
yield print_r($class->getConstants(), true);
129+
}
130+
131+
if (!$class->isInterface()) {
132+
$defaults = $class->getDefaultProperties();
133+
134+
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $p) {
135+
yield $p->getDocComment().$p;
136+
yield print_r($defaults[$p->name], true);
137+
}
138+
}
139+
140+
if (defined('HHVM_VERSION')) {
141+
foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) {
142+
// workaround HHVM bug with variadics, see https://github.com/facebook/hhvm/issues/5762
143+
yield preg_replace('/^ @@.*/m', '', new ReflectionMethodHhvmWrapper($m->class, $m->name));
144+
}
145+
} else {
146+
foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) {
147+
yield preg_replace('/^ @@.*/m', '', $m);
148+
149+
$defaults = array();
150+
foreach ($m->getParameters() as $p) {
151+
$defaults[$p->name] = $p->isDefaultValueAvailable() ? $p->getDefaultValue() : null;
152+
}
153+
yield print_r($defaults, true);
154+
}
155+
}
156+
}
157+
}
158+
159+
/**
160+
* @internal
161+
*/
162+
class ReflectionMethodHhvmWrapper extends \ReflectionMethod
163+
{
164+
public function getParameters()
165+
{
166+
$params = array();
167+
168+
foreach (parent::getParameters() as $i => $p) {
169+
$params[] = new ReflectionParameterHhvmWrapper(array($this->class, $this->name), $i);
170+
}
171+
172+
return $params;
173+
}
174+
}
175+
176+
/**
177+
* @internal
178+
*/
179+
class ReflectionParameterHhvmWrapper extends \ReflectionParameter
180+
{
181+
public function getDefaultValue()
182+
{
183+
return array($this->isVariadic(), $this->isDefaultValueAvailable() ? parent::getDefaultValue() : null);
184+
}
185+
}

src/Symfony/Component/Config/Tests/Resource/ClassExistenceResourceTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,20 @@ public function testIsFreshWhenClassExists()
5151

5252
$this->assertTrue($res->isFresh(time()));
5353
}
54+
55+
public function testExistsKo()
56+
{
57+
spl_autoload_register(function ($class) use (&$loadedClass) { $loadedClass = $class; });
58+
59+
$res = new ClassExistenceResource('MissingFooClass');
60+
$this->assertTrue($res->isFresh(0));
61+
62+
$this->assertSame('MissingFooClass', $loadedClass);
63+
64+
$loadedClass = 123;
65+
66+
$res = new ClassExistenceResource('MissingFooClass', ClassExistenceResource::EXISTS_KO);
67+
68+
$this->assertSame(123, $loadedClass);
69+
}
5470
}

0 commit comments

Comments
 (0)
0