Skip to content

Commit

Permalink
feature #21419 [DI][Config] Add & use ReflectionClassResource (nicola…
Browse files Browse the repository at this point in the history
…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
fabpot committed Feb 2, 2017
2 parents 87273d9 + 37e4493 commit 03b7cf7
Show file tree
Hide file tree
Showing 26 changed files with 616 additions and 176 deletions.
Expand Up @@ -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>
Expand All @@ -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'));
}
Expand Down
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/Symfony/Component/Config/CHANGELOG.md
@@ -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
-----

Expand Down
57 changes: 50 additions & 7 deletions src/Symfony/Component/Config/Resource/ClassExistenceResource.php
Expand Up @@ -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;
}
}

/**
Expand All @@ -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);
}
}
171 changes: 171 additions & 0 deletions src/Symfony/Component/Config/Resource/ReflectionClassResource.php
@@ -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);
}
}
Expand Up @@ -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);
}
}
}

0 comments on commit 03b7cf7

Please sign in to comment.