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.
| Comment | File | Size | Author |
|---|---|---|---|
| #30 | 2650812-29.patch | 1.78 KB | fabianx |
| #2 | 2650812-2.patch | 1.78 KB | wim leers |
| #2 | 2650812-2-FAIL-test-only.patch | 962 bytes | wim leers |
Comments
Comment #2
wim leersComment #4
gábor hojtsyWow, looks nice.
Comment #5
chx commentedNote: ContainerBuilder::getDefinition is broken with aliases too. See TestServiceProvider. Someone should fix that perhaps too.
Comment #6
dawehnerYeah, IMHO we should fix both here and check whether our implementation of aliases is actually properly in general.
Comment #7
fabianx commentedYes, 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.
Comment #8
wim leersComment #9
wim leersThat then only leaves #5. Tagged so somebody else can finish this.
Comment #10
gábor hojtsy@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?
Comment #11
fabianx commented#10: Yes, getDefinition() will need to be fixed or won't fix upstream.
Comment #12
gábor hojtsyAll right, are you suggesting we fix it on our side and open an upstream issue?
Comment #13
gábor hojtsySo 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:
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:
TestServiceProvider had this code for a similar purpose:
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.
Comment #14
chx commentedDon't base your decisions on TestServiceProvider ; it's just something I hacked together. But note
this is recursive in
ContainerBuilder::get. Whether it's an accident or intentional, who knows. CertainlyContainer::getis not recursive.Recommendation: don't be recursive.
Comment #15
gábor hojtsyAll 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 :)
Comment #16
xjmThe D8 committers all agreed that this issue is a major bug. Thanks everyone for working on this.
Comment #17
gábor hojtsyAny reviews, comments? :) With the modified test service provider I believe we have full test coverage.
Comment #18
dawehnerAlright :)
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.
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-166631116for exampleDon't we want to recursively go through aliases as the code in TestServiceProvider did? On top of that
\Drupal\Tests\Core\DependencyInjection\ContainerBuilderTestneeds its test coverageSomeone should add a novice issue to get rid of that insanity.
Mh, so
getAlias()returns an\Symfony\Component\DependencyInjection\Aliasinstance ...Comment #19
gábor hojtsyYes, I long for that future.
We discussed this above. I think you have a different opinion. Where does the container do recursive resolution?
Comment #21
wim leers#2749819: OptimizedPhpArrayDumper does not resolve references via aliases correctly just landed, which seems to be closely related.
Comment #22
fabianx commentedThanks, 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.
Comment #23
wim leersThanks!
Comment #24
fabianx commentedI 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)
Comment #25
fabianx commentedRTBC - just changed the order.
Comment #26
chx commentedTo 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.
Comment #27
fabianx commented#26: TestServiceProvider is KernelTestBase specific and has nothing to do with this.
We can however open a new issue for it.
Comment #28
alexpottHow come the array_key_exists() is being added?
I guess we're copying symfony...
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().
Comment #29
chx commentedOur
Containerdoes not contain this snippet from Symfony Container: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.
Comment #30
fabianx commentedGood 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.
Comment #33
catchCommitted/pushed to 8.2.x and cherry-picked to 8.1.x. Thanks!
Comment #35
chx commentedNote 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.