Permalink
Browse files

feature #21419 [DI][Config] Add & use ReflectionClassResource (nicola…

…s-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI][Config] Add & use ReflectionClassResource

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #21079
| License       | MIT
| Doc PR        | -

With new changes comming to 3.3, we need a more generic reflection tracking logic than the one already managed by the autowiring subsystem.

This PR adds a new ReflectionClassResource in the Config component, and a new ContainerBuilder::getReflectionClass() method in the DI one (for fetching+tracking reflection-related info).

ReflectionClassResource tracks changes to any public or protected properties/method.

PR updated and ready, best viewed [ignoring whitespaces](https://github.com/symfony/symfony/pull/21419/files?w=1).

changelog:

* added `ReflectionClassResource` class
* added second `$exists` constructor argument to `ClassExistenceResource` - with a special mode that prevents fatal errors from happening when some parent class is broken (logic generalized from AutowiringPass)
* made `ClassExistenceResource` also work with interfaces and traits
* added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info
* deprecated `ContainerBuilder::getClassResource()`, use `ContainerBuilder::getReflectionClass()` or `ContainerBuilder::addObjectResource()` instead

Commits
-------

37e4493 [DI][Config] Add & use ReflectionClassResource
  • Loading branch information...
2 parents 87273d9 + 37e4493 commit 03b7cf72dcb0896b39e38e509b6a720b814658d5 @fabpot fabpot committed Feb 2, 2017
Showing with 616 additions and 176 deletions.
  1. +7 −1 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php
  2. +5 −0 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php
  3. +1 −1 src/Symfony/Bundle/FrameworkBundle/composer.json
  4. +7 −0 src/Symfony/Component/Config/CHANGELOG.md
  5. +50 −7 src/Symfony/Component/Config/Resource/ClassExistenceResource.php
  6. +171 −0 src/Symfony/Component/Config/Resource/ReflectionClassResource.php
  7. +20 −0 src/Symfony/Component/Config/Tests/Resource/ClassExistenceResourceTest.php
  8. +140 −0 src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php
  9. +9 −2 src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php
  10. +4 −1 src/Symfony/Component/Console/composer.json
  11. +2 −0 src/Symfony/Component/DependencyInjection/CHANGELOG.md
  12. +14 −61 src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php
  13. +4 −1 src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php
  14. +5 −0 src/Symfony/Component/DependencyInjection/Config/AutowireServiceResource.php
  15. +110 −46 src/Symfony/Component/DependencyInjection/ContainerBuilder.php
  16. +4 −24 src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
  17. +6 −11 src/Symfony/Component/DependencyInjection/Extension/Extension.php
  18. +1 −0 src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php
  19. +3 −0 src/Symfony/Component/DependencyInjection/Tests/Config/AutowireServiceResourceTest.php
  20. +32 −7 src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
  21. +3 −3 src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
  22. +2 −1 src/Symfony/Component/DependencyInjection/composer.json
  23. +1 −0 src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php
  24. +6 −7 src/Symfony/Component/HttpKernel/DependencyInjection/FragmentRendererPass.php
  25. +6 −1 src/Symfony/Component/HttpKernel/Tests/DependencyInjection/FragmentRendererPassTest.php
  26. +3 −2 src/Symfony/Component/HttpKernel/composer.json
@@ -13,7 +13,10 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Component\Translation\TranslatorBagInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
@@ -31,7 +34,10 @@ public function process(ContainerBuilder $container)
$definition = $container->getDefinition((string) $translatorAlias);
$class = $container->getParameterBag()->resolveValue($definition->getClass());
- if (is_subclass_of($class, 'Symfony\Component\Translation\TranslatorInterface') && is_subclass_of($class, 'Symfony\Component\Translation\TranslatorBagInterface')) {
+ if (!$r = $container->getReflectionClass($class)) {
+ throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $translatorAlias));
+ }
+ if ($r->isSubclassOf(TranslatorInterface::class) && $r->isSubclassOf(TranslatorBagInterface::class)) {
$container->getDefinition('translator.logging')->setDecoratedService('translator');
$container->getDefinition('translation.warmer')->replaceArgument(0, new Reference('translator.logging.inner'));
}
@@ -54,6 +54,11 @@ public function testProcess()
->method('getParameterBag')
->will($this->returnValue($parameterBag));
+ $container->expects($this->once())
+ ->method('getReflectionClass')
+ ->with('Symfony\Bundle\FrameworkBundle\Translation\Translator')
+ ->will($this->returnValue(new \ReflectionClass('Symfony\Bundle\FrameworkBundle\Translation\Translator')));
+
$pass = new LoggingTranslatorPass();
$pass->process($container);
}
@@ -22,7 +22,7 @@
"symfony/dependency-injection": "~3.3",
"symfony/config": "~3.3",
"symfony/event-dispatcher": "~3.3",
- "symfony/http-foundation": "~3.1",
+ "symfony/http-foundation": "~3.3",
"symfony/http-kernel": "~3.3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/filesystem": "~2.8|~3.0",
@@ -1,6 +1,13 @@
CHANGELOG
=========
+3.3.0
+-----
+
+ * added `ReflectionClassResource` class
+ * added second `$exists` constructor argument to `ClassExistenceResource`
+ * made `ClassExistenceResource` work with interfaces and traits
+
3.0.0
-----
@@ -21,16 +21,27 @@
*/
class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializable
{
+ const EXISTS_OK = 1;
+ const EXISTS_KO = 0;
+ const EXISTS_KO_WITH_THROWING_AUTOLOADER = -1;
+
private $resource;
- private $exists;
+ private $existsStatus;
+
+ private static $checkingLevel = 0;
+ private static $throwingAutoloader;
+ private static $existsCache = array();
/**
- * @param string $resource The fully-qualified class name
+ * @param string $resource The fully-qualified class name
+ * @param int|null $existsStatus One of the self::EXISTS_* const if the existency check has already been done
*/
- public function __construct($resource)
+ public function __construct($resource, $existsStatus = null)
{
$this->resource = $resource;
- $this->exists = class_exists($resource);
+ if (null !== $existsStatus) {
+ $this->existsStatus = (int) $existsStatus;
+ }
}
/**
@@ -54,22 +65,54 @@ public function getResource()
*/
public function isFresh($timestamp)
{
- return class_exists($this->resource) === $this->exists;
+ if (null !== $exists = &self::$existsCache[$this->resource]) {
+ $exists = $exists || class_exists($this->resource, false) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
+ } elseif (self::EXISTS_KO_WITH_THROWING_AUTOLOADER === $this->existsStatus) {
+ if (null === self::$throwingAutoloader) {
+ $signalingException = new \ReflectionException();
+ self::$throwingAutoloader = function () use ($signalingException) { throw $signalingException; };
+ }
+ if (!self::$checkingLevel++) {
+ spl_autoload_register(self::$throwingAutoloader);
+ }
+
+ try {
+ $exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
+ } catch (\ReflectionException $e) {
+ $exists = false;
+ } finally {
+ if (!--self::$checkingLevel) {
+ spl_autoload_unregister(self::$throwingAutoloader);
+ }
+ }
+ } else {
+ $exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
+ }
+
+ if (null === $this->existsStatus) {
+ $this->existsStatus = $exists ? self::EXISTS_OK : self::EXISTS_KO;
+ }
+
+ return self::EXISTS_OK === $this->existsStatus xor !$exists;
}
/**
* {@inheritdoc}
*/
public function serialize()
{
- return serialize(array($this->resource, $this->exists));
+ if (null === $this->existsStatus) {
+ $this->isFresh(0);
+ }
+
+ return serialize(array($this->resource, $this->existsStatus));
}
/**
* {@inheritdoc}
*/
public function unserialize($serialized)
{
- list($this->resource, $this->exists) = unserialize($serialized);
+ list($this->resource, $this->existsStatus) = unserialize($serialized);
}
}
@@ -0,0 +1,171 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Config\Resource;
+
+/**
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+class ReflectionClassResource implements SelfCheckingResourceInterface, \Serializable
+{
+ private $files = array();
+ private $className;
+ private $classReflector;
+ private $hash;
+
+ public function __construct(\ReflectionClass $classReflector)
+ {
+ $this->className = $classReflector->name;
+ $this->classReflector = $classReflector;
+ }
+
+ public function isFresh($timestamp)
+ {
+ if (null === $this->hash) {
+ $this->hash = $this->computeHash();
+ $this->loadFiles($this->classReflector);
+ }
+
+ foreach ($this->files as $file => $v) {
+ if (!file_exists($file)) {
+ return false;
+ }
+
+ if (@filemtime($file) > $timestamp) {
+ return $this->hash === $this->computeHash();
+ }
+ }
+
+ return true;
+ }
+
+ public function __toString()
+ {
+ return 'reflection.'.$this->className;
+ }
+
+ public function serialize()
+ {
+ if (null === $this->hash) {
+ $this->hash = $this->computeHash();
+ $this->loadFiles($this->classReflector);
+ }
+
+ return serialize(array($this->files, $this->className, $this->hash));
+ }
+
+ public function unserialize($serialized)
+ {
+ list($this->files, $this->className, $this->hash) = unserialize($serialized);
+ }
+
+ private function loadFiles(\ReflectionClass $class)
+ {
+ foreach ($class->getInterfaces() as $v) {
+ $this->loadFiles($v);
+ }
+ do {
+ $file = $class->getFileName();
+ if (false !== $file && file_exists($file)) {
+ $this->files[$file] = null;
+ }
+ foreach ($class->getTraits() as $v) {
+ $this->loadFiles($v);
+ }
+ } while ($class = $class->getParentClass());
+ }
+
+ private function computeHash()
+ {
+ if (null === $this->classReflector) {
+ try {
+ $this->classReflector = new \ReflectionClass($this->className);
+ } catch (\ReflectionException $e) {
+ // the class does not exist anymore
+ return false;
+ }
+ }
+ $hash = hash_init('md5');
+
+ foreach ($this->generateSignature($this->classReflector) as $info) {
+ hash_update($hash, $info);
+ }
+
+ return hash_final($hash);
+ }
+
+ private function generateSignature(\ReflectionClass $class)
+ {
+ yield $class->getDocComment().$class->getModifiers();
+
+ if ($class->isTrait()) {
+ yield print_r(class_uses($class->name), true);
+ } else {
+ yield print_r(class_parents($class->name), true);
+ yield print_r(class_implements($class->name), true);
+ yield print_r($class->getConstants(), true);
+ }
+
+ if (!$class->isInterface()) {
+ $defaults = $class->getDefaultProperties();
+
+ foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $p) {
+ yield $p->getDocComment().$p;
+ yield print_r($defaults[$p->name], true);
+ }
+ }
+
+ if (defined('HHVM_VERSION')) {
+ foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) {
+ // workaround HHVM bug with variadics, see https://github.com/facebook/hhvm/issues/5762
+ yield preg_replace('/^ @@.*/m', '', new ReflectionMethodHhvmWrapper($m->class, $m->name));
+ }
+ } else {
+ foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) {
+ yield preg_replace('/^ @@.*/m', '', $m);
+
+ $defaults = array();
+ foreach ($m->getParameters() as $p) {
+ $defaults[$p->name] = $p->isDefaultValueAvailable() ? $p->getDefaultValue() : null;
+ }
+ yield print_r($defaults, true);
+ }
+ }
+ }
+}
+
+/**
+ * @internal
+ */
+class ReflectionMethodHhvmWrapper extends \ReflectionMethod
+{
+ public function getParameters()
+ {
+ $params = array();
+
+ foreach (parent::getParameters() as $i => $p) {
+ $params[] = new ReflectionParameterHhvmWrapper(array($this->class, $this->name), $i);
+ }
+
+ return $params;
+ }
+}
+
+/**
+ * @internal
+ */
+class ReflectionParameterHhvmWrapper extends \ReflectionParameter
+{
+ public function getDefaultValue()
+ {
+ return array($this->isVariadic(), $this->isDefaultValueAvailable() ? parent::getDefaultValue() : null);
+ }
+}
@@ -51,4 +51,24 @@ public function testIsFreshWhenClassExists()
$this->assertTrue($res->isFresh(time()));
}
+
+ public function testExistsKo()
+ {
+ spl_autoload_register($autoloader = function ($class) use (&$loadedClass) { $loadedClass = $class; });
+
+ try {
+ $res = new ClassExistenceResource('MissingFooClass');
+ $this->assertTrue($res->isFresh(0));
+
+ $this->assertSame('MissingFooClass', $loadedClass);
+
+ $loadedClass = 123;
+
+ $res = new ClassExistenceResource('MissingFooClass', ClassExistenceResource::EXISTS_KO);
+
+ $this->assertSame(123, $loadedClass);
+ } finally {
+ spl_autoload_unregister($autoloader);
+ }
+ }
}
Oops, something went wrong.

0 comments on commit 03b7cf7

Please sign in to comment.