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.installer service 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 the config.installer class. 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

Comments

nedjo created an issue. See original summary.

nedjo’s picture

Status: Active » Needs review
StatusFileSize
new758 bytes
dawehner’s picture

+++ b/src/FeaturesServiceProvider.php
@@ -23,7 +23,10 @@ class FeaturesServiceProvider extends ServiceProviderBase {
+    }
   }

So 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::checkConfigurationToInstall and all its dependent code. I guess for now that would be certainly too much for this issue.

nedjo’s picture

Status: Needs review » Active

@dawehner:

We could get rid of the problem by decorating an existing ConfigInstaller

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 ConfigInstaller overrides are distinct; Features overrides ::findPreExistingConfiguration() while Configuration Share overrides ::getConfigToCreate(). Using a decorator, could we allow both to apply?

Status: Active » Needs work

The last submitted patch, 2: features-service-alter-2625310-2.patch, failed testing.

nedjo’s picture

Title: Make config.installer service override conditional » Decorate config.installer service rather than overriding it
Priority: Normal » Major
Issue summary: View changes

This looks like what I was missing: Symfony's approach to decorating services.

Updating the summary accordingly.

nedjo’s picture

Updated to major because this would be an API change so should go in before beta.

mpotter’s picture

Priority: Major » Normal

Don't think it causes an API change in base Features, just in the config.installer replacement service.

nedjo’s picture

Status: Needs work » Needs review
StatusFileSize
new16.21 KB

Here'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.

Status: Needs review » Needs work

The last submitted patch, 9: 2625310-9.patch, failed testing.

nedjo’s picture

Title: Decorate config.installer service rather than overriding it » Address conflict with other modules over config.installer class
Issue summary: View changes
Status: Needs work » Active
Related issues: +#2800839: Address conflict with Features over config.installer class

I'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.

we should at least log that we did not replaced the service.

I'll work up a patch doing that and also using hook_requirements() to issue warnings.

nedjo’s picture

Issue summary: View changes
Status: Active » Needs review
StatusFileSize
new3.31 KB

Okay, 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.

Status: Needs review » Needs work

The last submitted patch, 12: 2625310-12.patch, failed testing.

nedjo’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new1.21 KB
new2.63 KB

Removing 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 the config.installer service, so we're testing for original classes in one place, proxies in another.

mpotter’s picture

In 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

mpotter’s picture

Oh, 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.

nedjo’s picture

In the hook_requirements can you not get the class from the current container like you do in the alter code? (using \Drupal::getContainer()) ?

I tried that, but \Drupal::getContainer() returns a Drupal\Core\DependencyInjection\Container rather than a Drupal\Core\DependencyInjection\ContainerBuilder object.

harivenuv’s picture

StatusFileSize
new1.39 KB

hi,

In this patch (2625310-14.patch) translation function used incorrectly. Use $this->t() instead of t() I have created a interdiff.

mpotter’s picture

StatusFileSize
new8.32 KB

Going 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.

mpotter’s picture

Also, 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.

nedjo’s picture

Issue summary: View changes

@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:

  • A public method is called on the service.
  • The outermost decorator is invoked. If it implements the public method, that's called. Otherwise, we fall through to the next decorator. If there is no next decorator, we fall through to the original class.

I suspect the problem with subclassing ConfigInstaller is we'll never fall through. As a subclass of ConfigInstaller, FeaturesConfigInstaller will implement all ConfigInstallerInterface public methods, so any subsequent decorator will never be called.

mpotter’s picture

Nedjo: 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.

mpotter’s picture

StatusFileSize
new3.08 KB

Ah ha!!! It worked!

Nedjo: Here is what I did. I created another config.installer that *did* take the "inner" method:

  features.config.anotherinstaller:
    class: Drupal\features\FeaturesAnotherConfigInstaller
    public: false
    decorates: config.installer
    decoration_priority: 4
    arguments: ['@features.config.anotherinstaller.inner', '@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher']

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.

mpotter’s picture

StatusFileSize
new8.75 KB

So 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.

mpotter’s picture

StatusFileSize
new8.75 KB

Shoot, that is what I get for last minute copy/paste. Try this.

The last submitted patch, 24: 2625310-24.patch, failed testing.

nedjo’s picture

see BigPipe for example

A difference there is the relevant interface, AttachmentsResponseProcessorInterface, has only one method, so there's no point in facilitating multiple decorators.

Only thing special here is that it seems to work for protected functions.

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:

Use $this->t() instead of t()

That's usually the case, but in this case the patch is on procedural code rather than a class.

mpotter’s picture

I 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.

mpotter’s picture

I'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.

mpotter’s picture

StatusFileSize
new9.71 KB
new1.8 KB

Here is an update that uses $this->configInstaller instead of $this in two places. Also removed the start/end comments.

Status: Needs review » Needs work

The last submitted patch, 30: 2625310-30.patch, failed testing.

mpotter’s picture

StatusFileSize
new9.44 KB

Ahh, 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.

mpotter’s picture

Status: Needs work » Needs review
mpotter’s picture

In IRC, dawehner's comment was:

that is all you need? Nice
that's kind of an interesting approach, this is for sure
but don't you still have to call to this->configInstaller as much as possible?

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.

  • mpotter committed 77534a3 on 8.x-3.x
    Issue #2625310 by mpotter, nedjo, harivenu_zyxware: Address conflict...
mpotter’s picture

Status: Needs review » Fixed

Committed to 77534a3

Status: Fixed » Closed (fixed)

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