Adding Definition::addError() and a compiler pass to throw errors as exceptions

This commit is contained in:
Ryan Weaver 2017-09-22 13:54:05 -04:00 committed by Nicolas Grekas
parent 701d41cc33
commit a85b37a0e6
14 changed files with 194 additions and 9 deletions

View File

@ -22,6 +22,9 @@ use Symfony\Component\DependencyInjection\Reference;
*/
abstract class AbstractRecursivePass implements CompilerPassInterface
{
/**
* @var ContainerBuilder
*/
protected $container;
protected $currentId;

View File

@ -11,11 +11,15 @@
namespace Symfony\Component\DependencyInjection\Compiler;
@trigger_error('The '.__NAMESPACE__.'\AutowireExceptionPass class is deprecated since version 3.4 and will be removed in 4.0. Use the DefinitionErrorExceptionPass class instead.', E_USER_DEPRECATED);
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Throws autowire exceptions from AutowirePass for definitions that still exist.
*
* @deprecated since version 3.4, will be removed in 4.0.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class AutowireExceptionPass implements CompilerPassInterface

View File

@ -36,7 +36,7 @@ class AutowirePass extends AbstractRecursivePass
private $autowiringExceptions = array();
/**
* @param bool $throwOnAutowireException If false, retrieved errors via getAutowiringExceptions
* @param bool $throwOnAutowireException Errors can be retrieved via Definition::getErrors()
*/
public function __construct($throwOnAutowireException = true)
{
@ -44,10 +44,14 @@ class AutowirePass extends AbstractRecursivePass
}
/**
* @deprecated since version 3.4, to be removed in 4.0.
*
* @return AutowiringFailedException[]
*/
public function getAutowiringExceptions()
{
@trigger_error('Calling AutowirePass::getAutowiringExceptions() is deprecated since Symfony 3.4 and will be removed in 4.0. Use Definition::getErrors() instead.', E_USER_DEPRECATED);
return $this->autowiringExceptions;
}
@ -106,6 +110,7 @@ class AutowirePass extends AbstractRecursivePass
}
$this->autowiringExceptions[] = $e;
$this->container->getDefinition($this->currentId)->addError($e->getMessage());
return parent::processValue($value, $isRoot);
}

View File

@ -22,6 +22,13 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
*/
class CheckArgumentsValidityPass extends AbstractRecursivePass
{
private $throwExceptions;
public function __construct($throwExceptions = true)
{
$this->throwExceptions = $throwExceptions;
}
/**
* {@inheritdoc}
*/
@ -35,10 +42,20 @@ class CheckArgumentsValidityPass extends AbstractRecursivePass
foreach ($value->getArguments() as $k => $v) {
if ($k !== $i++) {
if (!is_int($k)) {
throw new RuntimeException(sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k));
$msg = sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k);
$value->addError($msg);
if ($this->throwExceptions) {
throw new RuntimeException($msg);
}
break;
}
throw new RuntimeException(sprintf('Invalid constructor argument %d for service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $this->currentId, $i));
$msg = sprintf('Invalid constructor argument %d for service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $this->currentId, $i);
$value->addError($msg);
if ($this->throwExceptions) {
throw new RuntimeException($msg);
}
}
}
@ -47,10 +64,20 @@ class CheckArgumentsValidityPass extends AbstractRecursivePass
foreach ($methodCall[1] as $k => $v) {
if ($k !== $i++) {
if (!is_int($k)) {
throw new RuntimeException(sprintf('Invalid argument for method call "%s" of service "%s": integer expected but found string "%s". Check your service definition.', $methodCall[0], $this->currentId, $k));
$msg = sprintf('Invalid argument for method call "%s" of service "%s": integer expected but found string "%s". Check your service definition.', $methodCall[0], $this->currentId, $k);
$value->addError($msg);
if ($this->throwExceptions) {
throw new RuntimeException($msg);
}
break;
}
throw new RuntimeException(sprintf('Invalid argument %d for method call "%s" of service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $methodCall[0], $this->currentId, $i));
$msg = sprintf('Invalid argument %d for method call "%s" of service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $methodCall[0], $this->currentId, $i);
$value->addError($msg);
if ($this->throwExceptions) {
throw new RuntimeException($msg);
}
}
}
}

View File

@ -0,0 +1,39 @@
<?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\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
/**
* Throws an exception for any Definitions that have errors and still exist.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class DefinitionErrorExceptionPass extends AbstractRecursivePass
{
/**
* {@inheritdoc}
*/
protected function processValue($value, $isRoot = false)
{
if (!$value instanceof Definition || empty($value->getErrors())) {
return parent::processValue($value, $isRoot);
}
// only show the first error so they user can focus on it
$errors = $value->getErrors();
$message = reset($errors);
throw new RuntimeException($message);
}
}

View File

@ -38,10 +38,14 @@ class InlineServiceDefinitionsPass extends AbstractRecursivePass implements Repe
*
* The key is the inlined service id and its value is the list of services it was inlined into.
*
* @deprecated since version 3.4, to be removed in 4.0.
*
* @return array
*/
public function getInlinedServiceIds()
{
@trigger_error('Calling InlineServiceDefinitionsPass::getInlinedServiceIds() is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED);
return $this->inlinedServiceIds;
}

View File

@ -59,14 +59,14 @@ class PassConfig
new RegisterServiceSubscribersPass(),
new ResolveNamedArgumentsPass(),
new ResolveBindingsPass(),
$autowirePass = new AutowirePass(false),
new AutowirePass(false),
new ResolveServiceSubscribersPass(),
new ResolveReferencesToAliasesPass(),
new ResolveInvalidReferencesPass(),
new AnalyzeServiceReferencesPass(true),
new CheckCircularReferencesPass(),
new CheckReferenceValidityPass(),
new CheckArgumentsValidityPass(),
new CheckArgumentsValidityPass(false),
));
$this->removingPasses = array(array(
@ -75,11 +75,11 @@ class PassConfig
new RemoveAbstractDefinitionsPass(),
new RepeatedPass(array(
new AnalyzeServiceReferencesPass(),
$inlinedServicePass = new InlineServiceDefinitionsPass(),
new InlineServiceDefinitionsPass(),
new AnalyzeServiceReferencesPass(),
new RemoveUnusedDefinitionsPass(),
)),
new AutowireExceptionPass($autowirePass, $inlinedServicePass),
new DefinitionErrorExceptionPass(),
new CheckExceptionOnInvalidReferenceBehaviorPass(),
));
}

View File

@ -44,6 +44,7 @@ class Definition
private $autowiringTypes = array();
private $changes = array();
private $bindings = array();
private $errors = array();
protected $arguments = array();
@ -959,4 +960,24 @@ class Definition
return $this;
}
/**
* Add an error that occurred when building this Definition.
*
* @param string $error
*/
public function addError($error)
{
$this->errors[] = $error;
}
/**
* Returns any errors that occurred while building this Definition.
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
}

View File

@ -18,6 +18,9 @@ use Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
/**
* @group legacy
*/
class AutowireExceptionPassTest extends TestCase
{
public function testThrowsException()

View File

@ -157,6 +157,9 @@ class AutowirePassTest extends TestCase
$this->assertEquals(DInterface::class, (string) $container->getDefinition('h')->getArgument(1));
}
/**
* @group legacy
*/
public function testExceptionsAreStored()
{
$container = new ContainerBuilder();

View File

@ -64,4 +64,15 @@ class CheckArgumentsValidityPassTest extends TestCase
array(array(), array(array('baz', array(1 => 1)))),
);
}
public function testNoException()
{
$container = new ContainerBuilder();
$definition = $container->register('foo');
$definition->setArguments(array(null, 'a' => 'a'));
$pass = new CheckArgumentsValidityPass(false);
$pass->process($container);
$this->assertCount(1, $definition->getErrors());
}
}

View File

@ -0,0 +1,53 @@
<?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\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\DefinitionErrorExceptionPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
class DefinitionErrorExceptionPassTest extends TestCase
{
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Things went wrong!
*/
public function testThrowsException()
{
$container = new ContainerBuilder();
$def = new Definition();
$def->addError('Things went wrong!');
$def->addError('Now something else!');
$container->register('foo_service_id')
->setArguments(array(
$def,
));
$pass = new DefinitionErrorExceptionPass();
$pass->process($container);
}
public function testNoExceptionThrown()
{
$container = new ContainerBuilder();
$def = new Definition();
$container->register('foo_service_id')
->setArguments(array(
$def,
));
$pass = new DefinitionErrorExceptionPass();
$pass->process($container);
$this->assertSame($def, $container->getDefinition('foo_service_id')->getArgument(0));
}
}

View File

@ -252,6 +252,9 @@ class InlineServiceDefinitionsPassTest extends TestCase
$this->assertSame('inline', (string) $values[0]);
}
/**
* @group legacy
*/
public function testGetInlinedServiceIdData()
{
$container = new ContainerBuilder();

View File

@ -387,4 +387,13 @@ class DefinitionTest extends TestCase
$def->setAutoconfigured(true);
$this->assertTrue($def->isAutoconfigured());
}
public function testAddError()
{
$def = new Definition('stdClass');
$this->assertEmpty($def->getErrors());
$def->addError('First error');
$def->addError('Second error');
$this->assertSame(array('First error', 'Second error'), $def->getErrors());
}
}