Problem/Motivation
Drupal 8 core currently does not have any API for configuration export.
This means that tools such as drush or the drupal console have come up with their own way of exporting configuration. The way they do that is by reading directly from the active configuration storage service.
This will make it very difficult in the future to improve the workflow with for example #3028179: Config Environment module (original issue) without patching drush.
Currently CMI improvements working with config_filter get around this because config_filter decorates the sync storage and drush uses the sync storage service when no argument is passed to the commands and thus it only works in the default case.
With #2991683: Move configuration transformation API in \Drupal\Core\Config namespace we are adding a new API that will make working with it much easier and also work with the zip workflow. But it should remain the responsibility of core to make sure that the API is called properly.
Proposed resolution
Add a new service: config.storage.export
In a first version in 8.7 this will just return a read-only version of config.storage.
Drush and console can check if the new service exists and use it or continue using config.storage directly if the site runs on 8.6.
Then in 8.8 when the environment module becomes an experimental module drush and console will not have to be patched.
Remaining tasks
Write patch.
review
commit
User interface changes
None
API changes
API addition. New service.
Data model changes
None.
Release notes snippet
Code that exports configuration should use the new read-only configuration export storageconfig.storage.export instead of exporting configuration directly from the active storage. Exporting directly from the active storage will lead to unexpected behaviour when using features that use the new config transformation api.
| Comment | File | Size | Author |
|---|---|---|---|
| #39 | interdiff-3036193-36-39.txt | 598 bytes | bircher |
| #39 | 3036193-39.patch | 14.94 KB | bircher |
| #36 | interdiff-3036193-35-36.txt | 9.91 KB | bircher |
| #36 | 3036193-36.patch | 14.95 KB | bircher |
| #35 | interdiff-3036193-30-35.txt | 10.57 KB | bircher |
Comments
Comment #2
moshe weitzman commentedDrush will happily use the new factory described here.
Comment #3
bircherAttached a first patch.
The test asserts that the storage one gets from the service contains the same data but can not be used to change the active config.
Comment #4
bircherThe test class has the same method as #3016429: Add a config storage copy utility trait and fix ConfigManager::createSnapshot so that might be refactored at some point to the config test trait.
Linking the issue also because once it gets committed this code could also use it.
Comment #5
bircheralso it needs review now.
Comment #6
bircherI meant review :see_no_evil:
Comment #7
bircherI added a change record draft: https://www.drupal.org/node/3037022
Comment #8
bircherAttached a new patch that makes the ConfigController use the new service for downloading the zipped config.
Comment #9
andypostNot clear from summary what this factory decoration simplifying, from code it just decorates active storage with memory storage
Quick codereview
for constructors we still not using cameCase arguments in core(
returns storage, not interface
return require description according current coding standards
for compatibility reasons new dependency should be added last and trigger error that new argument will be required in 9.0.0
Comment #10
bircherAttached a patch that addresses the feedback from #9. Thanks for the review!
As for the reason, we are not really decorating the service though.
The point is to provide a API for exporting config. The config.storage service is not an API for exporting as it represents the active configuration. If we hope to be able to do more than we currently do we need to have a dedicated point for exporting.
In the future this is where the config transformation will call the export transformation which dispatches the events.
I hope the change record explains it properly.
Comment #11
johnwebdev commentedThe code is pretty straight forward, only some few nit-picks.
Guessing this is programmatically generated, perhaps we could say "Defines the export storage factory."
@TODO should be lower case.
Move @coversDefaultClass to the top
You could just type hint for string, and set $message to = '' as that's what assertEquals does. However we don't do strict typing so it doesn't really matter. ¯\_(ツ)_/¯
Comment #12
bircherThanks for the review!
Attached a new patch addressing the points raised.
Comment #13
mpotter commentedIn the patch, the `getExportStorage()` creates a MemoryStorage and copies the config to it, then returns. Then in your Change Record you show the example of using this returned storage to copy to a "sync" storage. Couldn't the `getExportStorage` simply be `exportStorage($dest)` and then in your Change Record you just show calling `\Drupal::service('config.export.factory')->exportStorage($sync)`?
I worry about the extra memory usage of creating the duplicate MemoryStorage object and also having the caller need to replicate the copy code. (although using the StorageCopyTrait will make this easier)
Perhaps still return the export storage and have it create the MemoryStorage only if the $sync argument is null?
Comment #14
mpotter commentedAfter call with @bircher I have a clearer understanding of this and why the factory just returns the new Storage object to be written. I updated the Change Record https://www.drupal.org/node/3037022 to reduce its complexity and make it more obvious that the caller can do anything they need to write the config and won't necessarily be replicating the code being used within the export factory.
So my comment #13 is handled.
Last nit-pick in reviewing: Since the service is called "config.export.factory", having the function called "getExportStorage" seems redundant and confusing.
change to just getStorage(). So usage would look like:
$source = \Drupal::service('config.export.factory')->getStorage();Comment #15
bircherattached the new patch
Comment #17
mpotter commentedgetStorage
testGetStorage
getStorage
getStorage
Comment #18
bircherof course.. working on too many things in parallel.
Comment #19
mpotter commentedThis looks great now! Will make it much easier for tools like Drush to support new stuff coming in CMI 2.
Comment #20
larowlanI like this, its a good step towards the goals
I don't know that this is something we normally do
previously this only ever read one item into memory at once, but now we read it all. Could we do this with yield and iterators to prevent OOM on sites with a lot of config?
Comment #21
alexpottI think #2 is a great idea and we need to address memory concerns here because this is reading all of config into memory. Anecdotally I've be told that sites with large amounts of field struggle with memory on config sync and this more add a new place for such sites to fail.
We should make more use of generators when dealing with the entire site config and this would be a great place to start.
Comment #22
bircherAttached are two possible solutions for not loading all of the config storage into memory.
I do not know how to use iterators in this context. The goal with this is that in the future we can do the storage transformation as described in #2991683: Move configuration transformation API in \Drupal\Core\Config namespace without drush or drupal console to have to update their code.
In defence of the patch that returns the active storage straight: This is no worse than what is currently happening. And by committing that we at least define an entry point to interact with to export configuration.
Comment #25
bircherI think something went wrong with the testbot, setting back to needs-review.
Comment #26
larowlanif we take this approach, could we decorate Active storage with a ReadOnlyStorage to enforce this?
Comment #27
bircherwe certainly can.
code and test from config_filter with changed namespaces.
Comment #28
alexpottCan we do a proper factory in the container?
So we have a service called 'config.export' that is not shared and created from a service called config.export.factory?
See https://symfony.com/doc/current/service_container/factories.html
That way you don't have to inject the factory and you get a storage interface object to work with.
Comment #30
bircherHmm of course it works better if we use the class from core :)
Yes we can make the storage also a service of course. But this will make the future api a bit more cumbersome, as we then need to have a storage class that essentially wraps a memory/database storage and then before returning the decorated storages data calls $this->initialise() which would then dispatch the transform event etc. Otherwise we risk of things being injected into other services that are expensive to construct.
So I would prefer the api to be the factory and not the storage itself.
Attached is a patch that shows with the storage as a service though.
I also renamed the method to just
get. I think the factory name already contains enough information.Comment #31
bircherComment #32
bircherThe more I am thinking about this the less I think providing the storage as a service is a good idea.
It may seem at first like a good idea, but one of the goals we are trying to achieve is to make it easy for core in the future to improve the configuration management. So there might be a potentially expensive operation involved.
Providing the storage as a service would mean that it can then be injected into other services, which means that the expensive operation would have to happen the first time one tries to interact with the storage (ie read from it, list its content etc because expensive constructors are bad). This in turn would mean that we would have to have a storage that can set off that expensive operation (and do so only once) so the storage would have to be set up with the transformer service etc.
I am not sure that would lead to a good design especially as compared to just the export factory taking care of the setting off the expensive operation. In particular with regards to future batching or other things we might want to do in the UI that drush and drupal console are less concerned about (our primary target for this api here).
Plus we can always add the storage service later if we come to the conclusion that this is really needed.
As demonstrated in the previous patch it is not difficult yet.
So I would vote for keeping things simple for now and expand the API when we need to.
If others think having the storage as a service is much better then we can of course continue with the patch from #30
Comment #34
bircherActually after even more reflection I had another idea.
With it we can do for now without the factory at all just passing the active storage to the read-only one as an argument in the service definition.
We can then in the future create a storage that gets the factory from the previous patches injected and just decorates the storage it gets from it.
Then the "factory" would have a database storage and the state and would make sure to do the expense transformation only once.
I am just on my phone now, so I can roll a patch for that only tomorrow evening.
Comment #35
bircherComment #36
bircherAttached a patch with a test using a memory storage instead of a prophesied one.
Comment #37
bircherComment #38
mpotter commentedThis looks very good to me. One very minor nitpick:
Should just be "$fixture = ["
Comment #39
bircherComment #40
mpotter commentedComment #41
catchThis could use a release notes snippet before commit, but otherwise looks good to me. Since it's 99% adding a minimal new class and test coverage I think it would be OK to get in before beta to unblock other work.
Comment #42
xjmComment #43
bircherfirst stab at the release note snippet.
Comment #44
bircherComment #45
larowlanThis looks good to me too.
Comment #46
catchSorry I missed the update here. We've missed the beta window but committed 52bd9a1 and pushed to 8.8.x. Thanks!
Comment #49
pameeela commentedChanging tag to highlights so we can call this out but it's not disruptive so doesn't need a full release note snippet.
Comment #50
bircherre-adding the tag requested in #41 and tagged in #46.
I don't know what the criteria for "disruptive" is or if the requirement has changed in the mean time. I think this is important for people that do configuration export manually or that write tools that do that.
Comment #51
xjmThanks @bircher. We use the tag "8.8.0 release notes" (or similar) for an issue that has some sort of disruptive impact beyond what a typical deprecation identifies: It requires a manual ugprade path step, or has a public BC break, or causes tests to fail for contrib, or affects which dependencies are packaged, or changes coding standards such that other Drupal code will need to be updated to comply. Anything that has a chance of breaking some usecase or isn't strictly allowable under policy. Typically, the release note snippet for those needs to explain, at a glance, how a site owner or module developer can tell if they need to change something, and what they need to change, with a link to the CR for more detailed explanations. See: https://www.drupal.org/issue-summaries#release-notes
"8.8.0 highlights", on the other hand, is used to separate out new features of the minor version that are worth celebrating in messaging about the release. A release snippet is still useful for such issues because it saves Gábor an hour of trying to understand the value of the change, but it's not a requirement to release the alpha. Sometimes it's used alongside the other tag if an issue both delivers significant value but also has disruptive impacts.
We used to use "8.8.0 release notes" for both types of things (several minors back), so committers occasionally forget and put the old tag on issues with no disruption. It's probable that that's what @catch did above. However, if this change does require changes of module developers or site owners beyond what any deprecation notices for it might surface, then the release note should be updated with an explanation of that and we can re-tag with "8.8.0 release notes".
Comment #52
bircherI changed the release note snippet. I hope it is succinct enough, yet conveys the "disruption" it has.
As such without any other change to 8.8.0 this wouldn't be a disruption. But we also added config_exclude to core, and if you don't update your code to use the new service here then all the other new config management features - such as the afore mentioned config_exclude - won't work. So developers of modules or drush/console commands need to update their code or it will be broken when users try to use the new features.