Problem/Motivation
Features overrides the config.installer service to swap in its own class; see documentation on this approach.
This is done to address a usability issue for feature developers. By default, the config.installer service prevents installation of modules that provide configuration that already exists on the site. This means that it would be impossible to install a feature on the site it was created on. Features' override addresses this issue by making an exception for feature modules, enabling them to be installed despite their configuration already being present.
However, overriding the service risks incompatibility with other modules. For example, the Configuration Provider module overrides the same service, and so is incompatible with Features although it is written with Features in mind. See #2800839: Address conflict with Features over config.installer class. This issue is mitigated by the fact that other modules have the option of explicitly making their service modifications after Features through setting module weight.
While convenient to address a usability issue, Features' service override is not essential and should not override other modules that may have a higher priority for customization of the service.
One possible approach would be to remove the config.installer service override (delete FeaturesServiceProvider.php) and Instead register a new service, specifying the 'decorates' and 'decorator_priority' keys per Symfony documentation on service decoration:
features.config.installer:
class: Bar
public: false
decorates: config.installer
decoration_priority: 5
arguments: ['@features.config.installer.inner', '@features.manager']
According to the documentation:
decorators with higher priorities will be applied first
A high number (5) could be appropriate for Features, since we want other decorators to be able to override it (e.g., by having a priority of 1).
However, currently we're overriding a protected method, ::findPreExistingConfiguration(). While this is only called once, and by a public method, ::checkConfigurationToInstall(), that method interacts with many protected methods and properties. Addressing this issue through a decorator would require extensive code duplication; see the attempt in #9.
Proposed resolution
- Only set the class for
config.installerservice if it's still using the original core class.Log the override if made. If not, log the fact that no override was made and link to the status report for more details.Logging not available during service provision. - In
hook_requirements(), test for theconfig.installerclass. In install phase, warn if it's not the core class. At runtime phase, warn if it's not Features' override. If made, the runtime warning will show up on the status page.
Remaining tasks
User interface changes
API changes
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #32 | 2625310-32.patch | 9.44 KB | mpotter |
| #30 | interdiff-25-30.txt | 1.8 KB | mpotter |
| #30 | 2625310-30.patch | 9.71 KB | mpotter |
| #25 | 2625310-25.patch | 8.75 KB | mpotter |
| #24 | 2625310-24.patch | 8.75 KB | mpotter |
Comments
Comment #2
nedjoComment #3
dawehnerSo we should at least log that we did not replaced the service. We could get rid of the problem by decorating an existing ConfigInstaller, which though would require a couple of more lines of code in features, so basically
\Drupal\Core\Config\ConfigInstaller::checkConfigurationToInstalland all its dependent code. I guess for now that would be certainly too much for this issue.Comment #4
nedjo@dawehner:
Could you sketch in a bit how this would work? What I'd really like to achieve is, for example, to allow both Features and Configuration Share to do their stuff. In this case, their
ConfigInstalleroverrides are distinct; Features overrides::findPreExistingConfiguration()while Configuration Share overrides::getConfigToCreate(). Using a decorator, could we allow both to apply?Comment #6
nedjoThis looks like what I was missing: Symfony's approach to decorating services.
Updating the summary accordingly.
Comment #7
nedjoUpdated to major because this would be an API change so should go in before beta.
Comment #8
mpotter commentedDon't think it causes an API change in base Features, just in the config.installer replacement service.
Comment #9
nedjoHere's a first draft.
Because what we want to replace is in a protected method, we need to replace the public method that calls it--and all the protected methods and properties that are invoked from the parent or the method we originally replaced. So, yeah, it's a lot. But it does mean our intervention is a lot less likely to conflict with other modules.
Comment #11
nedjoI'm running into this again because I'm needing to use Configuration Provider and Features together, see #2800839: Address conflict with Features over config.installer class. The attempt to decorate the config.installer service was messy with so much forked code. Changing the issue summary to go back to the earlier approach of skipping the override if another module has already registered its class.
I'll work up a patch doing that and also using
hook_requirements()to issue warnings.Comment #12
nedjoOkay, here's a patch that does what's described in the "Proposed resolution" section of the issue summary.
This issue is mitigated by the fact that other modules may set their module weight and so make their service provider changes after Features.
Comment #14
nedjoRemoving logging--not available during service provision.
An awkward part of the patch is, from the context of
hook_requirements(), I couldn't immediately figure out how to determine the class assigned to theconfig.installerservice, so we're testing for original classes in one place, proxies in another.Comment #15
mpotter commentedIn the hook_requirements can you not get the class from the current container like you do in the alter code? (using \Drupal::getContainer()) ?
But wait, going back to #9. Why can't we use a Trait and just replace the protected method? You shouldn't need to replicate the public method that calls it. In your child class, calling the public method defined in the parent class will still invoke the protected method in your child class. So I'm thinking there is still a cleaner way to do this.
Here is my test of using Traits to override protected functions: https://gist.github.com/mike-potter/c380ebc4f05469cfec5a8cf464995e6d
Comment #16
mpotter commentedOh, I see, you weren't talking about Traits, you were talking about Decorators.
Let me give this more thought. I'm still leary of just relying on module weight. Seems like there should be a way to do this that is cleaner and would allow other modules to still inherit the Features config installer behavior.
Comment #17
nedjoI tried that, but
\Drupal::getContainer()returns aDrupal\Core\DependencyInjection\Containerrather than aDrupal\Core\DependencyInjection\ContainerBuilderobject.Comment #18
harivenuvhi,
In this patch (2625310-14.patch) translation function used incorrectly. Use $this->t() instead of t() I have created a interdiff.
Comment #19
mpotter commentedGoing back to the approach in #9. I found that I could decorate the existing config installer service by still using a subclass rather than having to re-implement everything in a separate class. Tested this and it properly allows pre-existing config in feature modules to be installed.
It also removes the need for the proxyclass for our service I believe, but someone could correct me if I'm wrong on that.
Comment #20
mpotter commentedAlso, FYI, this behavior already gets tested by FeaturesCreateUITest. If you don't have the config installer overridden several of the asserts will fail. So test coverage for this is good.
Comment #21
nedjo@mpotter: Thanks for looking into this.
With the decorator pattern, what we'd be trying to achieve here is that we can have a hierarchy of classes.
As far as I can tell, the way it works in Symfony is:
I suspect the problem with subclassing
ConfigInstalleris we'll never fall through. As a subclass ofConfigInstaller,FeaturesConfigInstallerwill implement allConfigInstallerInterfacepublic methods, so any subsequent decorator will never be called.Comment #22
mpotter commentedNedjo: So I tested this. I created another decorator for config.installer. If I set it's "decoration_priority" higher than 5 then Features config installer "wins". If I set it's "decoration_priority" lower than 5 then the new decorator wins.
So you are correct that only one can run at once, but that's at least the same as your proposal in #14, and I think this method is cleaner. I'll play around with whether I can get multiple decorators to both work at the same time with different methods.
Comment #23
mpotter commentedAh ha!!! It worked!
Nedjo: Here is what I did. I created another config.installer that *did* take the "inner" method:
Then, in that new installer I modified a public function. It properly executed BOTH the "anotherinstaller" as well as the Features decorator. Here is a patch file that shows the additional installer that I created. I think you can use this as a model in your other module. As long as your decoration_priority is lower than Features then both should work. In fact, maybe I'll raise the priority of Features installer to 9.
Basically, Features Installer wants to have the highest priority and doesn't need the "inner" service because it's directly overriding a protected method, rather than the normal way decorators use the "inner" service to override public functions.
Comment #24
mpotter commentedSo here is a slightly revised patch from #19 that adds the inner service in case it needs to be called and just to better follow decorator convention.
In the strictest case we might need to pull in the public checkConfigurationToInstall() and then have it call $this->configInstaller->findPreExistingConfiguration(), but so far I don't find that to be needed. So for now I'm just going to leave the override of the protected class.
Having a decorator service "extend" the existing service rather than using a completely separate class that implements the same interface is used already in core (see BigPipe for example). So this looks like a pattern people are already using. Only thing special here is that it seems to work for protected functions. But I honestly haven't found any real Symfony documentation on this which would exclude this possibility from working.
I also set the decorator priority to 9 in this patch so it's clear to other decorators that they should use a lower number.
Comment #25
mpotter commentedShoot, that is what I get for last minute copy/paste. Try this.
Comment #27
nedjoA difference there is the relevant interface, AttachmentsResponseProcessorInterface, has only one method, so there's no point in facilitating multiple decorators.
What's happening is the public method is called (in the parent) and it calls the protected method (overridden in the child).
Overall this doesn't feel like a good use of decoration.
In the concrete case of Configuration Provider, there I also need to override a protected method, but that one is called by multiple public methods--meaning using the decorator pattern would be much messier even than here in #9. If we use a decorator that extends the original class here in Features, I suspect I'll have to do the same in Configuration Provider, when my feeling is it's instead a valid use case for the approach of switching out service classes documented here, which is what we're currently doing in both Features and Configuration Provider. Using decoration in this way could complicate things for (theoretical) service decorators that are indeed able to provide a public method without forking most of the original class.
Yes, by both switching out the service class the implementations collide, but we can't resolve that problem through decorators that extend the original class.
I'd like to hear from dawehner on this, since he first suggested the decorator pattern.
I'm okay to just close this as "works as designed", or to go back to some form of alert as in #14.
From #18:
That's usually the case, but in this case the patch is on procedural code rather than a class.
Comment #28
mpotter commentedI think this is still a good use for decorators. Features is "layering" a piece of functionality on top of the normal config installer to allow Features to be imported. This is likely a piece of functionality that any site using BOTH Features and some other module would need. In your Config Provider, I'd still expect to be able to install a features module with existing config.
I'd recommend actually trying it rather than trying to determine whether it will work or not theoretically. I believe in Configuration Provider you should be able to use the same approach I'm using here and just supply your protected function. You don't need to worry about which public functions it is called from. Then give it a priority lower than features. Add some print statements temporarily to the features getExistingConfig function to see if it still gets called when enabling a module and if your protected function also gets called. I think there is actually a good chance that it will just work.
Rather than closing this as "works as designed" I'd much rather commit #25. I find the use of a decorator here *much* cleaner and better than just overwriting the existing config installer in the alter function like we previously did. Even if #25 doesn't quite work for your specific case, it enables many other cases of projects that override the config installer.
Comment #29
mpotter commentedI'd like to include #25 as part of RC1, so looking for final comments on whether this decorator approach is better than the existing alter method of overridding the normal service.
Comment #30
mpotter commentedHere is an update that uses $this->configInstaller instead of $this in two places. Also removed the start/end comments.
Comment #32
mpotter commentedAhh, those were both protected methods, so can't use this->configInstaller with them. Which is still fine. It will call whatever protected method is in lower-priority decorators, or in the base class. Still works and still allows other decorators. So back to mostly the same as #25 with just the comment and code cleanup.
Comment #33
mpotter commentedComment #34
mpotter commentedIn IRC, dawehner's comment was:
So, addressed his question with "No, those are protected methods, so this->configInstaller not needed". Thus, if #32 passes tests, I'm going to commit it unless somebody posts a strong objection.
Comment #36
mpotter commentedCommitted to 77534a3