Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

[DX][DI] Add compiler pass which check extistence of service class name #11315

Closed
wants to merge 1 commit into from
@l3l0
Q A
Bug fix? no
New feature? yes
BC breaks? not sure - I hope that no
Deprecations? no
Tests pass? yes
Fixed tickets #11301
License MIT
@weaverryan
Collaborator

Nice work!

So, now the question is, does this break BC? Is there a situation where class will not exist during the compiler process but exist later at run-time?

@pyrech

Just my 2 cents about naming :) I would use 'class' rather than 'name' to be consistent with the "class" configuration of the service :
CheckServiceClassPass instead of CheckServiceNamePass
BadServiceClass instead of BadServiceName

Anyway, good job!

@l3l0

@pyrech good catch :) I changed names.

@Nek-

IMO it's not a BC break, but in reality it is for those who like hacking Sf, maybe somebody that read a lot of issue about DI will be able to confirm.

:+1: Anyway, nice work :) .

@Koc
Koc commented

How this change affects performance? Looks like it will loads all of the classes.

If yes - imho better add console commands for checking.

@l3l0

@Koc all classes should be loaded when container is compiled (not on every request), so probably can affect stuff in dev/debug mode IHMO. For prod configuration it should be fine. But I have not done any performance checks, so I cannot answer the question how this can affect performance.

@Koc
Koc commented

when container is compiled (not on every request)

I forgot about this, sorry. It occurs only when tracked resources was changed.

@stof stof commented on the diff
...orkBundle/Tests/Controller/ControllerResolverTest.php
@@ -109,8 +109,8 @@ public function testGetControllerOnNonUndefinedFunction($controller, $exceptionN
public function getUndefinedControllers()
{
return array(
- array('foo', '\LogicException', 'Unable to parse the controller name "foo".'),
- array('foo::bar', '\InvalidArgumentException', 'Class "foo" does not exist.'),
+ array('myfoo', '\LogicException', 'Unable to parse the controller name "myfoo".'),
+ array('myfoo::bar', '\InvalidArgumentException', 'Class "myfoo" does not exist.'),
@stof Collaborator
stof added a note

why changing these tests ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...ependencyInjection/Compiler/CheckServiceClassPass.php
((4 lines not shown))
+ * 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\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Exception\BadServiceClassException;
+
+/**
+ * Checks your service has valid class name (service exists)
@stof Collaborator
stof added a note

service exists looks weird in this description

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof stof commented on the diff
...ependencyInjection/Compiler/CheckServiceClassPass.php
((31 lines not shown))
+ {
+ foreach ($container->getDefinitions() as $id => $definition) {
+ if ($this->allowsToCheckClassExistenceFor($definition) && !class_exists($definition->getClass())) {
+ throw new BadServiceClassException($id, $definition->getClass());
+ }
+ }
+ }
+
+ private function allowsToCheckClassExistenceFor(Definition $definition)
+ {
+ return $definition->getClass() && !$this->isFactoryDefinition($definition) && !$definition->isSynthetic();
+ }
+
+ private function isFactoryDefinition(Definition $definition)
+ {
+ return $definition->getFactoryClass() || $definition->getFactoryService();
@stof Collaborator
stof added a note

in case of a definition using factoryClass, it should check that the factory class exists

@l3l0
l3l0 added a note

Yeah right :) I will add that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof
Collaborator

I think this should run after the removing passes. It does not make sense to throw an exception for a service which gets removed

@l3l0

@stof I added checking class for factory_class. I moved compiler pass to part running after the removing passes - this somehow force me to resolve class name again using ParameterBag.

@l3l0 l3l0 changed the title from [DI] Add compiler pass which check extistence of service class name to [DX][DI] Add compiler pass which check extistence of service class name
...Component/DependencyInjection/Compiler/PassConfig.php
@@ -53,7 +53,7 @@ public function __construct()
new ResolveInvalidReferencesPass(),
new AnalyzeServiceReferencesPass(true),
new CheckCircularReferencesPass(),
- new CheckReferenceValidityPass(),
+ new CheckReferenceValidityPass()
@stof Collaborator
stof added a note

Please don't remove the trailing comma in multiline arrays. Our coding standards require them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Component/DependencyInjection/Compiler/PassConfig.php
@@ -66,7 +66,11 @@ public function __construct()
new AnalyzeServiceReferencesPass(),
new RemoveUnusedDefinitionsPass(),
)),
- new CheckExceptionOnInvalidReferenceBehaviorPass(),
+ new CheckExceptionOnInvalidReferenceBehaviorPass()
@stof Collaborator
stof added a note

same here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Component/DependencyInjection/Compiler/PassConfig.php
@@ -66,7 +66,11 @@ public function __construct()
new AnalyzeServiceReferencesPass(),
new RemoveUnusedDefinitionsPass(),
)),
- new CheckExceptionOnInvalidReferenceBehaviorPass(),
+ new CheckExceptionOnInvalidReferenceBehaviorPass()
+ );
+
+ $this->afterRemovingPasses = array(
+ new CheckServiceClassPass()
@stof Collaborator
stof added a note

I would make it the last one of the removing passes instead (keeping the after removing passes as the extension point)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...dencyInjection/Exception/BadServiceClassException.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Symfony\Component\DependencyInjection\Exception;
@stloyd
stloyd added a note

Missing license note.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@l3l0

@stof @stloyd done. I squashed commits as well.

@l3l0 l3l0 [DI] Add compiler pass which check extistence of service class and
factory_class name (using class_exists() ) if that is possible - definition is not syntetic or is
not definition via factory method.
e42e350
@inso

What about adding check for existence of factory method?

@iltar

What if I use a factory method and my class is not a class but an interface? This is perfectly legit right now and can cause a BC break if not allowed any more.

See #11279

services:
    session.flash_bag:
        class:           Symfony\Component\HttpFoundation\Session\FlashBagInterface
        factory_service: session
        factory_method:  getFlashBag
@weaverryan
Collaborator

@iltar You have a point! Perhaps after checking class_exists, if it is not found, we can also check interface_exists. That would add almost no extra overhead to the compile process (since using an interface as the class doesn't happen very often).

@l3l0 what do you think?

@fabpot fabpot commented on the diff
...dencyInjection/Exception/BadServiceClassException.php
((10 lines not shown))
+ */
+
+namespace Symfony\Component\DependencyInjection\Exception;
+
+class BadServiceClassException extends RuntimeException
+{
+ public function __construct($id, $className, $key)
+ {
+ parent::__construct(
+ sprintf(
+ 'Class "%s" not found. Check the spelling on the "%s" configuration for your "%s" service.',
+ $className,
+ $key,
+ $id
+ )
+ );
@fabpot Owner
fabpot added a note

this should be on one line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jakzal
Collaborator

I like to give interfaces in my factory service definitions, so I'd vote for adding interface_exists().

The reason for configuring an interface instead of a class is proper autocompletion (coding against an interface yada yada yada).

@weaverryan
Collaborator

The only pending issues that has been added on this PR are

1) Adding an interface_exists check
2) Moving the exception message onto one line (fabpot's comment).

@l3l0 Can you make those changes? We're in the last week before 2.6 feature freeze and I would love to see if we can get this considered for a merge :).

@fabpot
Owner

We must also add support for the new way to register factories. See #12008

@fabpot
Owner

@l3l0 If you don't have time in the next coming days, I can also finish the PR for you, just let me know.

@fabpot
Owner

The problem I can see is memory consumption as when compiling the container, it will load all classes referenced in the container. So, we should check that first as I fear it's going to slow down the very first request a lot.

@stof
Collaborator

@fabpot this will likely have an impact in dev indeed. It needs to be measured.
For the prod, it should be OK as this will happen when running cache:warmup in the CLI

@weaverryan
Collaborator

Ah, good point about performance! I would love to have this better error message, but certainly not if it significantly increases memory or the loading of the container in the dev environment.

@l3l0

@fabpot Sure I haven't time recently and probably will be better if you take care about it :)

@fabpot
Owner

I've just tested the impact on the memory and I think the increase is not acceptable. On a barebone Symfony Standard Edition project, it takes 20MB of memory to run app/console without any cache. After applying the patch, it takes 25MB of memory, so an increase of 5MB (and remember that there are no third-party bundle enabled).

So, except if we only run this when debug is enabled, I'm :-1: on this change.

FWIW, the patch does not work as is as it does not take into account interfaces:

diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckServiceClassPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckServiceClassPass.php
index 1f0897e..71427b0 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/CheckServiceClassPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckServiceClassPass.php
@@ -31,11 +31,17 @@ class CheckServiceClassPass implements CompilerPassInterface
     {
         $parameterBag = $container->getParameterBag();
         foreach ($container->getDefinitions() as $id => $definition) {
-            if ($this->allowsToCheckClassExistenceForClass($definition) && !class_exists($parameterBag->resolveValue($definition->getClass()))) {
-                throw new BadServiceClassException($id, $definition->getClass(), 'class');
+            if ($this->allowsToCheckClassExistenceForClass($definition)) {
+                $class = $parameterBag->resolveValue($definition->getClass());
+                if (!class_exists($class) && !interface_exists($class) && !trait_exists($class)) {
+                    throw new BadServiceClassException($id, $definition->getClass(), 'class');
+                }
             }
-            if ($this->allowsToCheckClassExistenceForFactoryClass($definition) && !class_exists($parameterBag->resolveValue($definition->getFactoryClass()))) {
-                throw new BadServiceClassException($id, $definition->getFactoryClass(), 'factory_class');
+            if ($this->allowsToCheckClassExistenceForFactoryClass($definition)) {
+                $class = $parameterBag->resolveValue($definition->getFactoryClass());
+                if (!class_exists($class) && !interface_exists($class) && !trait_exists($class)) {
+                    throw new BadServiceClassException($id, $definition->getFactoryClass(), 'factory_class');
+                }
             }
         }
     }
@stof
Collaborator

I suggest closing this in favor of https://github.com/matthiasnoback/symfony-service-definition-validator/ which covers this kind of validation already, as well as a bunch of other rules (it checks that the mandatory arguments are passed for instance).
And when using the third-party bundle to run the validation, it is very easy to enable it only in the dev environment.

@fabpot fabpot closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 19, 2014
  1. @l3l0

    [DI] Add compiler pass which check extistence of service class and

    l3l0 authored
    factory_class name (using class_exists() ) if that is possible - definition is not syntetic or is
    not definition via factory method.
This page is out of date. Refresh to see the latest.
View
4 src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php
@@ -109,8 +109,8 @@ public function testGetControllerOnNonUndefinedFunction($controller, $exceptionN
public function getUndefinedControllers()
{
return array(
- array('foo', '\LogicException', 'Unable to parse the controller name "foo".'),
- array('foo::bar', '\InvalidArgumentException', 'Class "foo" does not exist.'),
+ array('myfoo', '\LogicException', 'Unable to parse the controller name "myfoo".'),
+ array('myfoo::bar', '\InvalidArgumentException', 'Class "myfoo" does not exist.'),
@stof Collaborator
stof added a note

why changing these tests ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
array('stdClass', '\LogicException', 'Unable to parse the controller name "stdClass".'),
array(
'Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar',
View
57 src/Symfony/Component/DependencyInjection/Compiler/CheckServiceClassPass.php
@@ -0,0 +1,57 @@
+<?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\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Exception\BadServiceClassException;
+
+/**
+ * Checks your service exists by checking its definition "class" and "factory_class" keys
+ *
+ * @author Leszek "l3l0" Prabucki <leszek.prabucki@gmail.com>
+ */
+class CheckServiceClassPass implements CompilerPassInterface
+{
+ /**
+ * Checks if ContainerBuilder services exists
+ *
+ * @param ContainerBuilder $container The ContainerBuilder instances
+ */
+ public function process(ContainerBuilder $container)
+ {
+ $parameterBag = $container->getParameterBag();
+ foreach ($container->getDefinitions() as $id => $definition) {
+ if ($this->allowsToCheckClassExistenceForClass($definition) && !class_exists($parameterBag->resolveValue($definition->getClass()))) {
+ throw new BadServiceClassException($id, $definition->getClass(), 'class');
+ }
+ if ($this->allowsToCheckClassExistenceForFactoryClass($definition) && !class_exists($parameterBag->resolveValue($definition->getFactoryClass()))) {
+ throw new BadServiceClassException($id, $definition->getFactoryClass(), 'factory_class');
+ }
+ }
+ }
+
+ private function allowsToCheckClassExistenceForClass(Definition $definition)
+ {
+ return $definition->getClass() && !$this->isFactoryDefinition($definition) && !$definition->isSynthetic();
+ }
+
+ private function allowsToCheckClassExistenceForFactoryClass(Definition $definition)
+ {
+ return $definition->getFactoryClass() && !$definition->isSynthetic();
+ }
+
+ private function isFactoryDefinition(Definition $definition)
+ {
+ return $definition->getFactoryClass() || $definition->getFactoryService();
@stof Collaborator
stof added a note

in case of a definition using factoryClass, it should check that the factory class exists

@l3l0
l3l0 added a note

Yeah right :) I will add that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+}
View
1  src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php
@@ -67,6 +67,7 @@ public function __construct()
new RemoveUnusedDefinitionsPass(),
)),
new CheckExceptionOnInvalidReferenceBehaviorPass(),
+ new CheckServiceClassPass(),
);
}
View
27 src/Symfony/Component/DependencyInjection/Exception/BadServiceClassException.php
@@ -0,0 +1,27 @@
+<?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\Exception;
+
+class BadServiceClassException extends RuntimeException
+{
+ public function __construct($id, $className, $key)
+ {
+ parent::__construct(
+ sprintf(
+ 'Class "%s" not found. Check the spelling on the "%s" configuration for your "%s" service.',
+ $className,
+ $key,
+ $id
+ )
+ );
@fabpot Owner
fabpot added a note

this should be on one line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+}
View
106 src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckServiceClassPassTest.php
@@ -0,0 +1,106 @@
+<?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 Symfony\Component\DependencyInjection\Compiler\CheckServiceClassPass;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+class CheckServiceClassPassTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @expectedException \Symfony\Component\DependencyInjection\Exception\BadServiceClassException
+ * @expectedExceptionMessage Class "\InvalidName\TestClass" not found. Check the spelling on the "class" configuration for your "a" service.
+ */
+ public function testThrowsBadNameExceptionWhenServiceHasInvalidClassName()
+ {
+ $container = new ContainerBuilder();
+ $container->register('a', '\InvalidName\TestClass');
+
+ $this->process($container);
+ }
+
+ public function testDoesNotThrowExceptionIfClassExists()
+ {
+ $container = new ContainerBuilder();
+ $container->register('a', '\Symfony\Component\DependencyInjection\Compiler\CheckServiceClassPass');
+ $container->register('b', '\stdClass');
+ $container->register('c', '\Symfony\Component\DependencyInjection\Tests\Compiler\MyTestService');
+ $container
+ ->register('d', '\stdClass')
+ ->setFactoryService('\Symfony\Component\DependencyInjection\Tests\Compiler\MyTestService')
+ ->setFactoryMethod('factoryMethod')
+ ;
+
+ $this->process($container);
+ }
+
+ public function testSynteticServicesClassNamesAreNotChecked()
+ {
+ $container = new ContainerBuilder();
+ $container
+ ->register('a', '\InvalidName\TestClass')
+ ->setSynthetic(true)
+ ;
+
+ $this->process($container);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\DependencyInjection\Exception\BadServiceClassException
+ * @expectedExceptionMessage Class "\InvalidName\TestClass" not found. Check the spelling on the "factory_class" configuration for your "a" service.
+ */
+ public function testFactoryMethodServicesClassNamesAreNotChecked()
+ {
+ $container = new ContainerBuilder();
+ $container
+ ->register('a', '\stdClass')
+ ->setFactoryClass('\InvalidName\TestClass')
+ ->setFactoryMethod('factoryMethod')
+ ;
+
+ $this->process($container);
+ }
+
+ public function testServicesWithoutNameAreNotChecked()
+ {
+ $container = new ContainerBuilder();
+ $container
+ ->register('a')
+ ;
+
+ $this->process($container);
+ }
+
+ public function testSynteticServicesNameAreNotChecked()
+ {
+ $container = new ContainerBuilder();
+ $container
+ ->register('a', '\InvalidName\TestClass')
+ ->setSynthetic(true)
+ ;
+
+ $this->process($container);
+ }
+
+ protected function process(ContainerBuilder $container)
+ {
+ $pass = new CheckServiceClassPass();
+ $pass->process($container);
+ }
+}
+
+class MyTestService
+{
+ public function factoryMethod()
+ {
+ }
+}
View
16 src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php
@@ -111,4 +111,20 @@ public function testProcessInlinesWhenThereAreMultipleReferencesButFromTheSameDe
$this->assertFalse($container->hasDefinition('b'));
$this->assertFalse($container->hasDefinition('c'), 'Service C was not inlined.');
}
+
+ /**
+ * @expectedException \Symfony\Component\DependencyInjection\Exception\BadServiceClassException
+ * @expectedExceptionMessage Class "\NotExist\InvalidClass" not found. Check the spelling on the "class" configuration for your "a" service.
+ */
+ public function testFriendlyErrorIsThrowedWhenServiceHasBadClassName()
+ {
+ $container = new ContainerBuilder();
+
+ $container
+ ->register('a', '\NotExist\InvalidClass')
+ ;
+
+ $container->compile();
+ }
+
}
View
1  src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php
@@ -1,6 +1,7 @@
<?php
require_once __DIR__.'/../includes/classes.php';
+require_once __DIR__.'/../includes/foo.php';
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
View
13 src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/ProjectExtension.php
@@ -4,6 +4,19 @@
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
+//Services have to exist
+class FooClass
+{
+}
+
+class BAR
+{
+}
+
+class Foo
+{
+}
+
class ProjectExtension implements ExtensionInterface
{
public function load(array $configs, ContainerBuilder $configuration)
View
12 src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php
@@ -59,3 +59,15 @@ public function __construct(BarClass $bar)
$this->bar = $bar;
}
}
+
+class Baz
+{
+}
+
+class Request
+{
+}
+
+class ConfClass
+{
+}
Something went wrong with that request. Please try again.