Skip to content

[DI] Include ids from method map in known services #19690

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
wants to merge 2 commits into from
Closed

[DI] Include ids from method map in known services #19690

wants to merge 2 commits into from

Conversation

ro0NL
Copy link
Contributor

@ro0NL ro0NL commented Aug 21, 2016

Q A
Branch? 2.7
Bug fix? yes
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets comma-separated list of tickets fixed by the PR, if any
License MIT
Doc PR reference to the documentation PR, if any

Service id's mapped thru $this->methodMap are allowed for in get, but apart from that they are not used. Ie. has returns false, but get returns.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Aug 22, 2016

$this->methodMap is a compilation artifact, thus any services there is/must be in $this->services.

@nicolas-grekas
Copy link
Member

Too quick comment, I didn't mean $this->services but all the get*Service() methods.

@@ -359,14 +358,19 @@ public function initialized($id)
public function getServiceIds()
{
$ids = array();
$reversedMethodMap = array_change_key_case(array_flip($this->methodMap), \CASE_LOWER);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the changes on this method are legitimate: get*Service are the only "source", methodMap must be in sync with the correspond list and we don't need to merge it.

Copy link
Contributor Author

@ro0NL ro0NL Aug 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests proves it works... you cant imagine how creative users can be. Sorry, couldnt resist ;-)

$this->methodMap is a compilation artifact

What if we make it a feature?

Copy link
Member

@nicolas-grekas nicolas-grekas Aug 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you cant imagine how creative users can

except that as you spotted, it never worked :)

What if we make it feature?

then it should go on master and should have a supporting use case

Copy link
Contributor Author

@ro0NL ro0NL Aug 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, i didnt consider PhpDumper::generateMethodName on this, which shows method names are generated from id.

However, they can get suffixed due duplicate methods, but that doesnt mean the id should change as well (this is the key in methodMap which can be perfectly unique).

I covered exactly that in tests (imagine a suffixed/generated method though).

@stof
Copy link
Member

stof commented Aug 22, 2016

@nicolas-grekas technically, this change is needed, because get*Service methods may not allow to retrieve the original service id anymore in all case (as we change the generated name to exclude invalid chars and avoid conflicts). Symfony itself went creative regarding the method map, and forgot to check that getServiceIds keeps working with that.

On a side note, older versions were also buggy because the conversion between id to method name is ambiguous (multiple ids could generate the same get*Service method, which is also why we made the change in the generation).

so 👍 for this bugfix

@ro0NL please add a test ensuring that getServiceIds works fine when the method map used a different name to avoid conflicts (see PhpDumperTest::testConflictingServiceIds for such a setup generating a different name)

@stof
Copy link
Member

stof commented Aug 22, 2016

Technically, get*Service methods are also a compilation artifact. So maybe we should use the method instead to check for services.
Btw, this would be safer. Due to ambiguity in the generation of factory methods (multiple ids can generate the same method name), has could tell you that a service exist while the factory would actually be registering it under a different id, which will do weird things (the service will not be shared properly, and each call to get it will replace the shared instance of the real id)

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 22, 2016

@stof are you proposing to revert the last commit, and be safe in all cases?

please add a test ensuring that getServiceIds works fine when the method map used a different name to avoid conflicts

If i understand correctly, i kinda did that in edd63a7#diff-8aeba2b88d0347062017ee160a6beb49R619

However i considered it from a user perspective.. (ie. map how you want)

@stof
Copy link
Member

stof commented Aug 22, 2016

If i understand correctly, I kinda did that in edd63a7#diff-8aeba2b88d0347062017ee160a6beb49R619

yeah indeed. I missed it sorry.

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 22, 2016

I think we should be safe in all cases. Yes, it's a compilation artifact but it's more or less designed as a feature. The reverse lookup is needed for sure.

Leaving; lowercasing and merging remaining service ids. Both are related to "different method convention", so if we go safe now this wont bother us ever again :)

@@ -359,14 +358,19 @@ public function initialized($id)
public function getServiceIds()
{
$ids = array();
$reversedMethodMap = array_change_key_case(array_flip($this->methodMap), \CASE_LOWER);
foreach (get_class_methods($this) as $method) {
if (preg_match('/^get(.+)Service$/', $method, $match)) {
Copy link
Member

@nicolas-grekas nicolas-grekas Aug 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, shouldn't we remove this and rely sololy on methodMap?

the full method could be reduced to the following implementation, isn't it?:

public function getServiceIds()
{
    return array_keys($this->methodMap + $this->services);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends how this method should work before/after compilation.

We could lazily load the method map when needed, which also happens when dumping. Im assuming the methods wont change during runtime anyway :)

@@ -357,15 +356,20 @@ public function initialized($id)
*/
public function getServiceIds()
{
$ids = array();
$ids = array_keys($this->services + $this->methodMap);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still thinking we should only return this, no need to look for getters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be the other way around btw: $this->methodMap + $this->services to mimic current ordering (at prevent changing test cases, isn't it)?

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 24, 2016

So, we have 2 issues then;

  • state before compilation (we must depend on method_exists, get_class_method)
  • state after compilation (we can depend on methodMap)

Where state depends on isCompiled/isFrozen. However, we dont actually compile the method map (ie. in Container::compile, which is bit weird maybe). Now suddenly state depends on the PhpDumper.

Basically we have no clue methodMap is available by design :) there is no isDumped method or so. Hence, we cover both cases?

I tend to agree with @nicolas-grekas we should only consider state after compilation (although we can do both, like currently). However, i think we must keep it consistent, ie; removing get_class_methods in getServiceIds, means removing method_exists in has.

So perhaps compiling methodMap should be moved to Container::compile first, to make things clear. Or just lazy load it :)

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 24, 2016

Here's a wild idea, make methodMap truly a compilation artifact by moving it all to the dumped container.

Cleanup anything "method" related in Container (ie method_exists, etc.), and override the affected methods in the dumped container, think;

// ProjectServiceContainer extends Container
protected $methodMap = array(
   'id' => 'getFooService' // just dumped as is
);

public function has($id)
{
    return isset($this->methodMap[$id]) || parent::has($id);
}

This seems to follow current design of the dumper... ie. i think this is the way to go (methodMap is no more a hidden feature of the Container).

@stof
Copy link
Member

stof commented Oct 1, 2016

@ro0NL your proposal does not work for get (it must go in the middle of the code), and can only work for has if we kill the optimization avoiding unnecessary strtolower calls.

@ro0NL
Copy link
Contributor Author

ro0NL commented Oct 1, 2016

True, get needs a little refactoring, ie. it should use a doInitialize, doHas, etc. that we can override.

Im not sure this approach is worth the effort.. but i do think it's better design opposed to Container being the almighty thing.

@stof
Copy link
Member

stof commented Oct 1, 2016

@ro0NL The little refactoring needed would involve dropping the performance optimization added a few months ago. This is not a good idea IMO.

@ro0NL
Copy link
Contributor Author

ro0NL commented Oct 1, 2016

@stof this wouldnt work?

# class Container
    public function has($id)
    {
        for ($i = 2;;) {
            if ('service_container' === $id
                || isset($this->aliases[$id])
                || isset($this->services[$id])
            ) {
                return true;
            }
            if (--$i && $id !== $lcId = strtolower($id)) {
                $id = $lcId;
            } else {
                if (isset($this->privates[$id])) {
                    @trigger_error(sprintf('Checking for the existence of the "%s" private service is deprecated since Symfony 3.2 and won\'t be supported anymore in Symfony 4.0.', $id), E_USER_DEPRECATED);
                }

-               return method_exists($this, 'get'.strtr($id, $this->underscoreMap).'Service');
+               return $this->doHas($id);
            }
        }
    }

+   // extension point for performance (opposed to overriding has()))
+   // ie. this technically finalizes has()
+   protected function doHas($normalizedId)
+   {
+       return false;
+   }

Override doHas in dumped containers checking the method map additionally.

@nicolas-grekas
Copy link
Member

Closing in favor of #20113

@stof
Copy link
Member

stof commented Oct 5, 2016

@ro0NL with extra overhead of the method call in a performance-sensitive code path (get is more sensitive than has btw)

@ro0NL ro0NL deleted the di/include-method-map branch October 5, 2016 17:14
fabpot added a commit that referenced this pull request 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants