Problem/Motivation

(Discovered while helping Gábor Hojtsy with the port of the https://www.drupal.org/project/securepages module to D8.)

See http://symfony.com/doc/current/components/dependency_injection/advanced....

E.g.:

  some_contrib_module.form_builder:
    class: \Drupal\some_contrib_module\FormBuilder
    decorates: form_builder
    parent: form_builder

i.e. some contrib module decorates core's form builder. It accepts the same arguments (hence parent: form_builder).

Rather than working, this causes the form_builder service to no longer be found. Even though an alias exists: form_builder points to some_contrib_module.form_builder.

Cause: \Drupal\Component\DependencyInjection\Container::has() does not look at aliases, even though \Drupal\Component\DependencyInjection\Container::get() does:

  public function has($id) {
    return isset($this->services[$id]) || isset($this->serviceDefinitions[$id]);
  }

  public function get($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
    if (isset($this->aliases[$id])) {
      $id = $this->aliases[$id];
    }
    …
  }

(Compare to \Symfony\Component\DependencyInjection\Container::has(), which also checks if it exists as an alias.)

Proposed resolution

-    return isset($this->services[$id]) || isset($this->serviceDefinitions[$id]);
+    return isset($this->services[$id]) || isset($this->serviceDefinitions[$id]) || isset($this->aliases[$id]);
   }

This was overlooked in #2497243: Replace Symfony container with a Drupal one, stored in cache

Remaining tasks

Review.

User interface changes

None.

API changes

None.

Data model changes

None.

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

Assigned: wim leers » Unassigned
Status: Active » Needs review
StatusFileSize
new962 bytes
new1.78 KB

The last submitted patch, 2: 2650812-2-FAIL-test-only.patch, failed testing.

gábor hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

Wow, looks nice.

chx’s picture

Note: ContainerBuilder::getDefinition is broken with aliases too. See TestServiceProvider. Someone should fix that perhaps too.

dawehner’s picture

Status: Reviewed & tested by the community » Needs work

Yeah, IMHO we should fix both here and check whether our implementation of aliases is actually properly in general.

fabianx’s picture

Yes, that bug is correct.

I had found this while porting back to service_container module, but forgot to create an issue before X-MAS break.

Sorry :/

https://github.com/LionsAd/service_container/pull/97/files#diff-24218d4b...

is the correct line - all other alias implementations are correct - I checked that.

wim leers’s picture

Issue tags: +Novice, +php-novice
wim leers’s picture

That then only leaves #5. Tagged so somebody else can finish this.

gábor hojtsy’s picture

@chx: But Drupal\Core\DependencyInjection has no getDefinition() override for Symfony\Component\DependencyInjection\Container. Do you mean we need to fix Symfony? What am I missing?

fabianx’s picture

#10: Yes, getDefinition() will need to be fixed or won't fix upstream.

gábor hojtsy’s picture

All right, are you suggesting we fix it on our side and open an upstream issue?

gábor hojtsy’s picture

So I looked into solving this. (Not sure this would be a novice by any chance). First let's define what we want. Our Container does only one level of alias resolution, eg:

  public function get($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
    if (isset($this->aliases[$id])) {
      $id = $this->aliases[$id];
    }

The patch suggest one level of alias resolution also in has(). Based on TestServiceProvider that chx pointed out, we would need infinite level alias resolution, something like this:

  /**
   * {@inheritdoc}
   */
  public function getDefinition($id) {
    while ($this->hasAlias($id)) {
      $id = (string) $this->getAlias($id);
    }
    parent::getDefinition($id);
  }

TestServiceProvider had this code for a similar purpose:

        for ($id = $original_id; $container->hasAlias($id); $id = (string) $container->getAlias($id));

Which one do we want to do? One level like all the container does at many places or multilevel, in which case we need to fix a lot more.

chx’s picture

Don't base your decisions on TestServiceProvider ; it's just something I hacked together. But note

        if (!array_key_exists($id, $this->definitions) && isset($this->aliasDefinitions[$id])) {
            return $this->get($this->aliasDefinitions[$id]);
        }

this is recursive in ContainerBuilder::get. Whether it's an accident or intentional, who knows. Certainly Container::get is not recursive.

Recommendation: don't be recursive.

gábor hojtsy’s picture

Status: Needs work » Needs review
Issue tags: -Novice, -php-novice
StatusFileSize
new3.47 KB
new1.88 KB

All right, added an override for getDefinition() and updated the test. I did not find a way to get the id from the definition, it does not seem to have a method for it sadly, so I kept the alias resolver for the id but not recursively anymore to mirror the resolver in getDefinition(). Let's see if this works :)

xjm’s picture

Issue tags: +Triaged core major

The D8 committers all agreed that this issue is a major bug. Thanks everyone for working on this.

gábor hojtsy’s picture

Any reviews, comments? :) With the modified test service provider I believe we have full test coverage.

dawehner’s picture

Status: Needs review » Needs work

Alright :)

With the modified test service provider I believe we have full test coverage.

Seriously, are we really falling back into the old days of having implicit test coverage. I guess we want to hate us in the future again.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php
    @@ -95,4 +95,15 @@ public function __sleep() {
    +  public function getDefinition($id) {
    +    $id = strtolower($id);
    

    Clearly, we should document why we override the method to document that the symfony parent resolves parameters on compile time only, see https://github.com/symfony/symfony/issues/17110#issuecomment-166631116 for example

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php
    @@ -95,4 +95,15 @@ public function __sleep() {
    +    if ($this->hasAlias($id)) {
    +      $id = $this->getAlias($id);
    ...
    +    return parent::getDefinition($id);
    

    Don't we want to recursively go through aliases as the code in TestServiceProvider did? On top of that \Drupal\Tests\Core\DependencyInjection\ContainerBuilderTest needs its test coverage

  3. +++ b/core/modules/simpletest/src/TestServiceProvider.php
    @@ -33,13 +33,13 @@ function register(ContainerBuilder $container) {
           foreach (['router.route_provider' => 'RouteProvider'] as $original_id => $class) {
    

    Someone should add a novice issue to get rid of that insanity.

  4. +++ b/core/modules/simpletest/src/TestServiceProvider.php
    @@ -33,13 +33,13 @@ function register(ContainerBuilder $container) {
    +        $id = $container->hasAlias($original_id) ? $container->getAlias($original_id) : $original_id;
    

    Mh, so getAlias() returns an \Symfony\Component\DependencyInjection\Alias instance ...

gábor hojtsy’s picture

I guess we want to hate us in the future again.

Yes, I long for that future.

Don't we want to recursively go through aliases as the code in TestServiceProvider did?

We discussed this above. I think you have a different opinion. Where does the container do recursive resolution?

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

fabianx’s picture

Assigned: Unassigned » fabianx

Thanks, Wim.

I am taking a look what is missing here now.

The recursive alias resolving should not be needed, because the dumpers do end-to-end alias resolution.

However it might be a bug in ContainerBuilder upstream, too - that only one level is resolved.

wim leers’s picture

Thanks!

fabianx’s picture

Status: Needs work » Needs review

I think #5 and #6 do not belong in this issue at all.

I unfortunately had forgotten about it as the Test and Patch had been so straightforward, so I had assumed this had been committed a long time ago, already.

If someone calls getDefinition(), it is their responsibility to ensure the alias-chain is resolved.

If someone calls has() it is the containers responsibility to check that if there is an alias then the container has the service.

So while #2 is a straightforward bug fix, the getDefinition() is a feature that should be rather be put in as a feature request upstream.

=>

RTBC for #2.

My only nit is that the aliases check should come first, so I will upload a patch to do that. (Just so that it matches the Symfony order)

fabianx’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new1.83 KB

RTBC - just changed the order.

chx’s picture

To demonstrate , can we kill most of TestServiceProvider ? Pretty please? It's so much better when we actually have a demo of how to get things done.

fabianx’s picture

#26: TestServiceProvider is KernelTestBase specific and has nothing to do with this.

We can however open a new issue for it.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
@@ -366,7 +366,7 @@ public function set($id, $service, $scope = ContainerInterface::SCOPE_CONTAINER)
+    return isset($this->aliases[$id]) || isset($this->services[$id]) || isset($this->serviceDefinitions[$id]) || array_key_exists($id, $this->services);

How come the array_key_exists() is being added?

I guess we're copying symfony...

    public function has($id)
    {
        for ($i = 2;;) {
            if ('service_container' === $id
                || isset($this->aliases[$id])
                || isset($this->services[$id])
                || array_key_exists($id, $this->services)
            ) {
                return true;
            }
            if (--$i && $id !== $lcId = strtolower($id)) {
                $id = $lcId;
            } else {
                return method_exists($this, 'get'.strtr($id, $this->underscoreMap).'Service');
            }
        }
    }

If we're going to implement the array_key_exists() check then I think a service in the container with a NULL value needs to be tested - since this is the difference between that method and the isset(). Personally I think this issue should go in without that and a separate issue should be made to discuss the array_key_exists().

chx’s picture

Our Container does not contain this snippet from Symfony Container:

        if (null === $service) {
            if (self::SCOPE_CONTAINER !== $scope) {
                unset($this->scopedServices[$scope][$id]);
            }

            unset($this->services[$id]);
        }

I know I used a NULL set before to remove a service... but definitely other parts of Symfony Container use array_key_exists. So there's no good guidance from the Symfony side. Add an unset() ? Definitely a separate issue.

fabianx’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new1.78 KB

Good catch - I missed that I had changed more than the order, back to RTBC.

I'll open a follow-up for https://github.com/symfony/dependency-injection/commit/6bd9ea28a6ba75227... and https://github.com/symfony/dependency-injection/commit/a875db7ae4f22cd21... to potentially sync that in.

Those are the only relevant changes since the container was committed.

--

And yes the array_key_exists() was totally wrong here. We skip that check for performance reasons and inherently assume that NULL == 'service does not exist and needs to be re-created'. Thanks Alex and chx.

Which is fine, because if one studies the Symfony code closely it can be seen that $services[$key] can never be NULL. So the check is superfluous.

Here is the right patch.

  • catch committed be7d7b2 on 8.2.x
    Issue #2650812 by Wim Leers, Fabianx, Gábor Hojtsy, chx, dawehner,...

  • catch committed 60686ca on 8.1.x
    Issue #2650812 by Wim Leers, Fabianx, Gábor Hojtsy, chx, dawehner,...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.2.x and cherry-picked to 8.1.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

chx’s picture

Note that even after this the alias feature is of very limited use because it doesn't move/merge service tags and although the new .inner service is set to non-public that does not help because findTaggedServiceIds doesn't check for isPublic . So if the original service was tagged with something then a) the decorated service won't be tagged appropriately b) even if you tag the outer service (the decorator) manually, findTaggedServiceIds will find the inner service which is very unlikely to be what you want.

This should at least be documented somewhere or perhaps an upstream patch submitted to make this feature more useful.

Until then, you can follow the code example in TestServiceProvider. As usual, Symfony is a poor fit to Drupal.