bug #23573 [Config] Make ClassExistenceResource throw on invalid parents (nicolas-grekas)

This PR was merged into the 3.3 branch.

Discussion
----------

[Config] Make ClassExistenceResource throw on invalid parents

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

Let's throw a more specific exception when a parent class/interface/trait is missing.
Fine tunes #23041

Commits
-------

53b01903ce [Config] Make ClassExistenceResource throw on invalid parents
This commit is contained in:
Fabien Potencier 2017-07-19 06:18:00 +02:00
commit 547185fe7a
6 changed files with 40 additions and 15 deletions

View File

@ -25,6 +25,7 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
private $exists; private $exists;
private static $autoloadLevel = 0; private static $autoloadLevel = 0;
private static $autoloadedClass;
private static $existsCache = array(); private static $existsCache = array();
/** /**
@ -57,6 +58,8 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
/** /**
* {@inheritdoc} * {@inheritdoc}
*
* @throws \ReflectionException when a parent class/interface/trait is not found
*/ */
public function isFresh($timestamp) public function isFresh($timestamp)
{ {
@ -68,12 +71,13 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
if (!self::$autoloadLevel++) { if (!self::$autoloadLevel++) {
spl_autoload_register(__CLASS__.'::throwOnRequiredClass'); spl_autoload_register(__CLASS__.'::throwOnRequiredClass');
} }
$autoloadedClass = self::$autoloadedClass;
self::$autoloadedClass = $this->resource;
try { try {
$exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false); $exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
} catch (\ReflectionException $e) {
$exists = false;
} finally { } finally {
self::$autoloadedClass = $autoloadedClass;
if (!--self::$autoloadLevel) { if (!--self::$autoloadLevel) {
spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass'); spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass');
} }
@ -112,7 +116,10 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
*/ */
private static function throwOnRequiredClass($class) private static function throwOnRequiredClass($class)
{ {
$e = new \ReflectionException("Class $class does not exist"); if (self::$autoloadedClass === $class) {
return;
}
$e = new \ReflectionException("Class $class not found");
$trace = $e->getTrace(); $trace = $e->getTrace();
$autoloadFrame = array( $autoloadFrame = array(
'function' => 'spl_autoload_call', 'function' => 'spl_autoload_call',
@ -138,6 +145,18 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
case 'is_callable': case 'is_callable':
return; return;
} }
$props = array(
'file' => $trace[$i]['file'],
'line' => $trace[$i]['line'],
'trace' => array_slice($trace, 0, 1 + $i),
);
foreach ($props as $p => $v) {
$r = new \ReflectionProperty('Exception', $p);
$r->setAccessible(true);
$r->setValue($e, $v);
}
} }
throw $e; throw $e;

View File

@ -124,8 +124,8 @@ class AutowirePass extends AbstractRecursivePass
if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) { if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
return $value; return $value;
} }
if (!$reflectionClass = $this->container->getReflectionClass($value->getClass())) { if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
$this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" does not exist.', $this->currentId, $value->getClass())); $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));
return $value; return $value;
} }
@ -388,7 +388,7 @@ class AutowirePass extends AbstractRecursivePass
unset($this->ambiguousServiceTypes[$type]); unset($this->ambiguousServiceTypes[$type]);
} }
if ($definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass())) { if ($definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass(), false)) {
return; return;
} }
@ -444,7 +444,7 @@ class AutowirePass extends AbstractRecursivePass
*/ */
private function createAutowiredDefinition($type) private function createAutowiredDefinition($type)
{ {
if (!($typeHint = $this->container->getReflectionClass($type)) || !$typeHint->isInstantiable()) { if (!($typeHint = $this->container->getReflectionClass($type, false)) || !$typeHint->isInstantiable()) {
return; return;
} }
@ -478,8 +478,8 @@ class AutowirePass extends AbstractRecursivePass
private function createTypeNotFoundMessage(TypedReference $reference, $label) private function createTypeNotFoundMessage(TypedReference $reference, $label)
{ {
if (!$r = $this->container->getReflectionClass($type = $reference->getType())) { if (!$r = $this->container->getReflectionClass($type = $reference->getType(), false)) {
$message = sprintf('has type "%s" but this class does not exist.', $type); $message = sprintf('has type "%s" but this class cannot be loaded.', $type);
} else { } else {
$message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists'; $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists';
$message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $this->createTypeAlternatives($reference)); $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $this->createTypeAlternatives($reference));

View File

@ -81,7 +81,7 @@ class FactoryReturnTypePass implements CompilerPassInterface
$class = $factory[0]; $class = $factory[0];
} }
if (!$m = $container->getReflectionClass($class)) { if (!$m = $container->getReflectionClass($class, false)) {
return; return;
} }
try { try {

View File

@ -66,7 +66,7 @@ class ResolveInstanceofConditionalsPass implements CompilerPassInterface
$instanceofTags = array(); $instanceofTags = array();
foreach ($conditionals as $interface => $instanceofDefs) { foreach ($conditionals as $interface => $instanceofDefs) {
if ($interface !== $class && (!$container->getReflectionClass($class))) { if ($interface !== $class && (!$container->getReflectionClass($class, false))) {
continue; continue;
} }

View File

@ -337,12 +337,15 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
* Retrieves the requested reflection class and registers it for resource tracking. * Retrieves the requested reflection class and registers it for resource tracking.
* *
* @param string $class * @param string $class
* @param bool $throw
* *
* @return \ReflectionClass|null * @return \ReflectionClass|null
* *
* @throws \ReflectionException when a parent class/interface/trait is not found and $throw is true
*
* @final * @final
*/ */
public function getReflectionClass($class) public function getReflectionClass($class, $throw = true)
{ {
if (!$class = $this->getParameterBag()->resolveValue($class)) { if (!$class = $this->getParameterBag()->resolveValue($class)) {
return; return;
@ -357,6 +360,9 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
$classReflector = $resource->isFresh(0) ? false : new \ReflectionClass($class); $classReflector = $resource->isFresh(0) ? false : new \ReflectionClass($class);
} }
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
if ($throw) {
throw $e;
}
$classReflector = false; $classReflector = false;
} }

View File

@ -359,7 +359,7 @@ class AutowirePassTest extends TestCase
/** /**
* @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException
* @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class does not exist. * @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class cannot be loaded.
*/ */
public function testClassNotFoundThrowsException() public function testClassNotFoundThrowsException()
{ {
@ -374,7 +374,7 @@ class AutowirePassTest extends TestCase
/** /**
* @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException
* @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadParentTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\OptionalServiceClass" but this class does not exist. * @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadParentTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\OptionalServiceClass" but this class cannot be loaded.
*/ */
public function testParentClassNotFoundThrowsException() public function testParentClassNotFoundThrowsException()
{ {
@ -744,7 +744,7 @@ class AutowirePassTest extends TestCase
public function provideNotWireableCalls() public function provideNotWireableCalls()
{ {
return array( return array(
array('setNotAutowireable', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setNotAutowireable()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class does not exist.'), array('setNotAutowireable', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setNotAutowireable()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class cannot be loaded.'),
array('setDifferentNamespace', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setDifferentNamespace()" references class "stdClass" but no such service exists. It cannot be auto-registered because it is from a different root namespace.'), array('setDifferentNamespace', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setDifferentNamespace()" references class "stdClass" but no such service exists. It cannot be auto-registered because it is from a different root namespace.'),
array(null, 'Cannot autowire service "foo": method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod()" must be public.'), array(null, 'Cannot autowire service "foo": method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod()" must be public.'),
); );