Skip to content

DependencyInjectionContainer id naming problem. #11761

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ivokoster opened this issue Aug 25, 2014 · 20 comments
Closed

DependencyInjectionContainer id naming problem. #11761

ivokoster opened this issue Aug 25, 2014 · 20 comments

Comments

@ivokoster
Copy link

Configured a service in the services.php with the id: 'testbundle.test_this'
Using it works perfect, except one strange behavior.

Calling it in your front controller with:
$this->get('testbundle.testthis');
Will throw no error, and gives you a NEW copy on the class, and replaces it in its container.
Calling $this->get('testbundle.test_this') afterwards will give you the NEW object.

Expected behavior: a error thrown of unknown id.

@stof
Copy link
Member

stof commented Aug 25, 2014

this is really weird. I never saw such behavior (and I saw errors for unknown ids though, which is even covered by tests)

@ivokoster
Copy link
Author

class DefaultController extends Controller
{
    public function indexAction(Request $request)
    {
        $test= $this->get('test.with_underscores');
        var_dump($test);
        $test= $this->get('test.withunderscores');
        var_dump($test);
        $test= $this->get('test.with_underscores');
        var_dump($test);
        exit;
    }
}

outputs:

object(Bundle\TestBundle\Manager\TestManager) object id 272
(class info here)
object(Bundle\TestBundle\Manager\TestManager) object id 444
(class info here)
object(Bundle\TestBundle\Manager\TestManager) object id 444
(class info here)

Only works with underscores. If anything other changes it will give a error.

Properly caused by the strtr() in
Symfony\Component\DependencyInjection\Container.php:216
} elseif (method_exists($this, $method = 'get'.strtr($id, array('' => '', '.' => '', '' => '_')).'Service')) {

Where '_' are replaced by ''

No tests found for errorhandling on naming with a underscore
ContainerTest.php:205

@sstok
Copy link
Contributor

sstok commented Aug 25, 2014

https://github.com/symfony/symfony/blob/master/src/Symfony/Component/DependencyInjection/Container.php#L216 This is only done for synchronized services.

What is the exact service definition of your service?

@ivokoster
Copy link
Author

Here also: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/DependencyInjection/Container.php#L301

in services.php

$container->setDefinition('test.with_underscores', new Definition('Bundle\TestBundle\Manager\TestManager'))
    ->addArgument(new Reference('doctrine.orm.entity_manager'));

@alquerci
Copy link
Contributor

alquerci commented Sep 1, 2014

Think that PHP method name resolution is case insensitive.

For the method_exists() function:

"FooBar" == "Foobar"  // method name
"foo_bar" == "foobar" // service id

To fix it the method_exists call should be avoided in favour of a method map only.

@stof
Copy link
Member

stof commented Sep 1, 2014

Using a method map would not help allowing to have foobar and foo_bar in the container at the same time

@alquerci
Copy link
Contributor

alquerci commented Sep 2, 2014

@stof indeed, it's not possible to have foobar and foo_bar in the container at the same time defined by method.

Stop me if I'm wrong but to prevent collision we can do something.

On prerequisite the service id foo_bar is defined but not foobar:

  • ::get() should throw ServiceNotFoundException when called with foobar code.
  • ::has() should return False when called with foobar code.
  • ::set() should not synchronize foo_bar when called with foobar code.

@fabpot
Copy link
Member

fabpot commented Feb 12, 2015

As this is an edge which would break BC if we try to fix it, I think it's safe to mark this one as a won't fix.

@stof
Copy link
Member

stof commented Feb 12, 2015

@fabpot Given that these generated methods are internal APIs, we could use a different replacement for underscores rather then using the empty char (the current generation turns snake case ids into camel cased method names, but this does not play well with the fact that PHP methods are case insensitive)

@fabpot
Copy link
Member

fabpot commented Feb 12, 2015

@stof Indeed, we could keep the underscore.

@stof
Copy link
Member

stof commented Feb 13, 2015

except we currently use the underscore to replace dots (which would then conflict). We need to find another way to replace dots in such case

@TomasVotruba
Copy link
Contributor

What needs to be done here?

@oscherler
Copy link
Contributor

It’s a highly mind-boggling problem if the affected service has an event listener (or maybe also a compiler pass) affecting it: getting the service with a missing or extra underscore in the name will give you a new instance that hasn’t had the listener run on it. It can take a while to determine where the problem comes from.

So yes, it’s an edge case, but a very time-consuming one.

How performant does the conversion from service name to method name have to be? Could strtr be replaced by something else? The conversion is done every time get or has is called, but that’s not something that should be done inside a loop, for example, so maybe a small performance hit would be acceptable?

@stof
Copy link
Member

stof commented Sep 29, 2016

Well, we are now using a method map to handle. The remaining thing to fix this issue would be to make it the authoritative way for compiled container by dropping entirely the check for protected methods on the container.
@fabpot @nicolas-grekas what do you think about this change ? I'm all in favor of it. Regarding BC, it would break things only for people writing their own dumper for compiled container. And if we want to preserve BC, we could keep the existing logic, but only when the methodMap is empty (throwing a deprecation notice saying that the dumper should be updated to the new way)

@nicolas-grekas
Copy link
Member

Looks like a good idea. I don't think we'd need to keep BC at this level, extending the generated protected methods would be really fragile anyway imho.

@stof
Copy link
Member

stof commented Sep 29, 2016

the question is not about extending the generated protected methods, but about the way the list of compiled services are detected.
In previous versions (before the introduction of the methodMap to solve a related issue), the logic was based purely on detecting whether the factory method was defined in the class. Today, we could use the methodMap exclusively for that (and it may be faster by avoiding to do string transformations but doing a simple key lookup in an array). But if people are using the component standalone by writing a container by hand, they might not define the methodMap, and relying on the method detection logic.

Btw, I think keeping BC would not be hard. And the impact for people using the dumped container in Symfony would be limited to a if(empty($this->methodMap) extra check (returning false and so no extra logic) when trying to access (or check has()) for a non-existent service. Any known service would already return earlier anyway.

@nicolas-grekas
Copy link
Member

LGTM, let's try this way, BC bridge included then!

@stof
Copy link
Member

stof commented Sep 29, 2016

I will work on it

@stof
Copy link
Member

stof commented Sep 30, 2016

here you go

fabpot added a commit that referenced this issue Oct 8, 2016
…for dumped containers (stof)

This PR was merged into the 3.2-dev branch.

Discussion
----------

Use the method map as authoritative list of factories for dumped containers

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | only for weird broken cases
| Deprecations? | yes (but only for people doing weird things)
| Tests pass?   | yes
| Fixed tickets | #11761, #19690
| License       | MIT
| Doc PR        | n/a

The initial implementation of the method factory discovery was based on a naming convention for factory methods. However, this naming convention allowed to generate the same name for multiple ids. In the meantime, a method map was introduced to solve this issue (and others).
When accessing a service with a different id than the official one (thanks to ambiguities), this breaks the sharing of the service, as it creates a new instance each time and replaces the existing shared instance. This was also inconsistent between a dumped container (affected by this) and a non-dumped container (reporting a service not found error for the other id).

The method map is now the authoritative way to discover available service factories. When the dumped container was generated with a method map (which is the case when using the dumper shipped in the component), the logic based on defined methods is not executed anymore. This forbids using another id than the real one to access the service (preventing to trigger the broken behavior). So this breaks BC for people being lucky (i.e. they were using the broken id only once and *before* any usage of the official id) and fixes a WTF bug for all others.
When using a dumper which does not fill the method map, the old logic is still applied, but deprecation warnings are triggered on access to dumped services. Currently, this will trigger a deprecation warning for each new service instantiation. I have not found an easy way to trigger it only once (except adding a private property to remember we already triggered it, but is it worth it ?), but only people writing a project container by hand or writing their own dumper would ever see such deprecation anyway (as the core dumper generates the method map).

Additionally, this makes ``getServiceIds`` faster by avoiding doing a regex match for each method in the class.

Commits
-------

03b9108 Use the method map as authoritative list of factories for dumped containers
@jakzal jakzal closed this as completed Oct 12, 2016
@oscherler
Copy link
Contributor

A big (belated) thank you for your work on this. I would have liked to tackle it, but I lacked some understanding of how the container works. And also, you were so quick. ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests