Problem/Motivation
We need a standardized and complete way to copy config between two config storages.
Many tools such as drush and drupal console and also a couple of modules do this all in their own way now and some do it in a incomplete fashion that may lead to strange behaviour in edge cases.
Examples in core:
\Drupal\Core\Config\ConfigManager::createSnapshot
misbehaves badly when storages are not in default collection.
\Drupal\Tests\ConfigTestTrait::copyConfig
ignores collections (so all tests based on this also ignore collections.)
related bug in drush: https://github.com/drush-ops/drush/issues/3572
related bug in drupal console: https://github.com/hechoendrupal/drupal-console/issues/1736
Modules implementing this logic on their own: config_split, config_sync, config_owner (and for sure a couple more)
Proposed resolution
Add a utility trait to copy config between storages which also takes care of collections.
Remaining tasks
port code from #2991683: Move configuration transformation API in \Drupal\Core\Config namespace
review
commit
User interface changes
none
API changes
todo
Data model changes
Release notes snippet
Comment | File | Size | Author |
---|---|---|---|
#39 | 3016429-37.patch | 8.6 KB | bircher |
#37 | 3016429-37-no-ref-test-only.patch | 8.6 KB | bircher |
#37 | interdiff-3016429-34-37.txt | 4.35 KB | bircher |
#37 | 3016429-37.patch | 8.6 KB | bircher |
#34 | interdiff-3016429-31-34.txt | 635 bytes | bircher |
Comments
Comment #2
bircherAdding the copier class with the MemoryStorage from #3015886: Add an in-memory config storage because it is so much simpler than setting up a temporary or virtual file system backed FileStorage. If the other issue is not committed in a reasonable time one might want to rewrite the test to use another config storage.
Attached is also a patch that uses the new copying mechanism in the also incomplete test trait.
Comment #3
bircherAttached the patch which has the usage in the test trait but not the code from #3015886: Add an in-memory config storage. (ie the code that can be postponed) leaving for needs review to not scare off potential reviewers.
Comment #4
borisson_This patch is super clear and really easy to read, I have found some really small things but not setting back to needs work for any of them. I think on all of them you could just answer "no" and I'd still be happy with this patch :)
I also already agreed at drupalcamp Ghent that delete first, copy everything to target was a good sequence.
Great work @bircher!
Should we try to throw an exception when someone tries to do this? Or can we imagine someone trying to do this?
Can we use strict comparison here (and in similar points in this method?)
This isn't very clear. we first copy everthing, and then we copy some other things. The first thing is collection-less config, right?
Can we document that more clearly?
This function tests both the copy and the fact that the copy command deletes before copying, should we split that up? Not sure, I like that this is only one method, but since this is a unit test there's hardly any cost to making it more clear?
Comment #5
Upchuk CreditAttribution: Upchuk as a volunteer commentedI agree, looks good. It definitely can be a statically used class as it's not meant to be injected anywhere and there are no other ways storage values should be copied over.
A couple of minor things on my part as well:
This made me wonder about what
getAllCollectionNames()
returns or should return. I see all the storage implementations returning the collection names without the default collection. Maybe we should document this on theStorageInterface::getAllCollectionNames()
no? That implementations should not return inclusive of the default name.No need to capitalise "Storage" I guess.
No need to pass this by reference no? It's an object so it's automatically by ref.
As borrison_ pointed out, I'd update this description to: "Copy the default collection" or something like this.
Comment #6
bircherThanks for the review. attached is the patch and interdiff, I didn't kick the test bot as the other issue is not committed yet.
RE #4
1) I don't think we can easily detect that a storage is the active storage in this layer. Also there is nothing preventing you from just writing to the active storage directly in general. So I think a warning is enough.
2) sure thing
3) I agree
4) well, the test asserts that the storage that target storage contains the identical configuration (including its collections), I don't know if that is two things. but I am always more happy for more tests.
RE #5
1) According to the documentation:
A configuration storage can contain multiple sets of configuration objects in partitioned collections. The collection key name identifies the current collection used.
So one could argue that only the partitions are collections and the default collection is the no-name collection of un-partitioned config. In any case I am not sure we need to update that documentation in this issue. but by all means: better documentation is better!2) of course
3) no need indeed
4) yes
Comment #7
borisson_Awesome, I'd rtbc this, but it's actually postponed, so not doing that yet. In any case this looks great.
Comment #8
bircherthis should just be
$property
With the latest patch form #3015886: Add an in-memory config storage we can return it straight. and then compare with
->getArrayCopy()
. Maybe we want to make the method name shorter too, or in fact make new methods$this->assertMemoryEquals()
and$this->assertMemoryNotEquals()
. to make the test more readable.Comment #9
alexpottLet's make this a trait with a protected static instead of a public static function. This makes it a bit less of generic utility that can be (ab)used to copy something to active storage.
Comment #10
bircherAttached the trait version.
Comment #11
mpotter CreditAttribution: mpotter at Phase2 commentedI wonder if this can be simplified rather than handling StorageInterface::DEFAULT_COLLECTION so differently. Here is a patch that passes the test, but this needs checking by somebody who understands the complexity of Collections and DEFAULT_COLLECTION more than I.
Comment #12
bircherMade the method static again and also added more tests.
I think now it is good to go.
Comment #13
bircher@mpotter discovered that in #2991683: Move configuration transformation API in \Drupal\Core\Config namespace we were also using it in
\Drupal\Core\Config\ConfigManager::createSnapshot
.So adding that bit here too.
Comment #14
bircherSo this is actually a bug fix, as discussed with @alexpott on slack.
Currently when you pass the
$snapshot_storage
not in its default collection to\Drupal\Core\Config\ConfigManager::createSnapshot
it will not have its default collection emptied out.What is worse is that it will but the collection config of whatever the $source_storage happens to be in to the collection of whatever $snapshot_storage happens to be in.
So if the source or target are not in their default collection createSnapshot creates very strange results.
Comment #15
johnwebdev CreditAttribution: johnwebdev commentedSome thoughts:
can we prevent this from happening programmatically instead of just a warning in the docbloc?
we could separate the empty logic here to its own method and throw an exception if the targeted storage isn't empty. that means the method is only responsible for one thing and not putting the target in its correct state first.
Comment #16
bircherThanks @johndevman for looking at this issue,
1. Unfortunately no. In the this context we have no knowledge what a storage is used for or if it is the active storage or not. The active storage is not read-only because it is used when config entities are saved etc. So nothing currently prevents you from trying to enable new modules by directly writing to
core.extensions
in the active storage. For some modules (that don't have any install hooks) or configuration to install this could work, but I think we can all agree that this is very wrong. We already made the method protected and into a trait so that it doesn't look too inviting and will be used only when appropriate. I would argue that preventing developers to ignore the clear warning is out of scope.2. I understand this idea and I have quarreled with it myself. But there is no one line method on the
StorageInterface
to know if it is "really empty" by which I mean all collections including the default are empty. So to check that in order to throw the exception one would have to do the same loop but have:if (!empty($target_collection->listAll())) {throw \Exception();}
instead of:
$target_collection->deleteAll();
To achieve the goal of providing a method of correctly copying configuration from one storage to another we would then also have to add a new method that calls this
emptyConfigStorage
method and then thecopyConfig
. So I am not sure the added complexity is justified. I am not against renaming thecopyConfig
method to something else, though.In addition I like that the deleteAll is called on the storage but then continues without making sure that the storage is indeed empty because that gives us more flexibility. We can test that too if really needed.
I updated the issue summary and added a change notice: https://www.drupal.org/node/3037305
Comment #17
alexpottI think I agree with @johndevman we should not do copy and delete in the same function. I'm not sure that copy should check for emptiness by default.
Comment #18
mpotter CreditAttribution: mpotter at Phase2 commentedI don't agree with removing the "empty/delete" logic. That is part of the copy process. We are trying to make the dest config the same as the source config, and that means adding new items, changing existing items, and deleting old items. This is implemented by emptying the dest, then adding the source. The fact it is "deleting" is an implementation detail and not something that should be split into a separate callable function. If the caller forgot to call the `emptyConfigStorage` then whey would have a mess, or they would get an exception for "storage not empty" and in general that is making it needlessly more complex for the caller.
I think `copyConfig` is still a valid name for this. It is not just `addConfig`. We aren't making a CRUD interface here.
Comment #19
bircherAttached is a quick patch with two separate methods, so that we can have an agreement and then pick.
I do not have a strong opinion on which is better. I tend to agree with @mpotter in #18 that deleting is an implementation detail. But I can see both arguments being valid.
This is like
rsync --recursive --delete
This is like copying files and deleting files with
cp
andrm
Comment #20
bircherAnd attached a compromise patch with new names:
syncStorage
: Make sure two storages are the same.copyStorage
: copy contentsclearStorage
: clear/delete allComment #21
mpotter CreditAttribution: mpotter at Phase2 commentedI can see the use-case of a newly created Storage object only wanting to call `copyStorage` without needing to clear it first. So I'm happy with this and the new names.
Wonder if DEFAULT_COLLECTION needs to come last in the array_merge here. Thinking of how this would work on a FileStorage where the parent directory needs to be deleted last when empty.
Comment #22
bircherRE #21 It shouldn't matter in which order they are in. The file storage doesn't delete the folder of the default collection and only deletes config files in it so this is fine.
This order is currently implemented in config splits export drush command and it works fine with a FileStorage.
Comment #23
mpotter CreditAttribution: mpotter at Phase2 commentedOK, with that answered I'm ready to RTBC this now.
Comment #24
alexpottOh noes seems like I had an xpost... I thought I'd commented on this hours ago. I'm not sure the compromise is a good deal. We've now got three methods to cope with and we've got to account for the difference between syncStorage and the ConfigSync class.
Let's choose one way (copy and clear) or another (sync - but with a better name). I'm not particularly in favour one on over the other. The 2 method approach offers future flexibility whilst the 1 method approach offers ease of use and consistency.
Comment #25
mpotter CreditAttribution: mpotter at Phase2 commentedI could go either way. I understand both sides. I actually like the name "syncStorage" because that's exactly what it does and doesn't get confused with whether Copy deletes stuff or just adds new stuff. I think @bircher was trying to support more flexibility and other use cases. It also makes the syncStorage a bit easier to understand by splitting the sub-functions into their own. Would we combine those back into the single sync method, or just mark them as private?
At this point I'd say to go with whichever has the best chance of getting committed. We only have a couple days left for 8.7 and it would be super helpful to get this into core so other parts of the initiative can start using it.
Comment #26
mpotter CreditAttribution: mpotter at Phase2 commentedTo help move this along, here is a version back to a single "syncStorage" method. This is the smallest refactor from current core and includes the bug fix from #14. Included an interdiff from #13 since this is going back to the single-method approach.
As mentioned above, I think "syncStorage" is a better indication of what this does. If somebody has a better name, please make a specific suggestion.
We could also rename the Trait itself from StorageCopyTrait to StorageSyncTrait but that would have made the interdiff harder to review.
Comment #27
bircherSo the discussion on the #config slack channel lead to a new suggestion which I now attached.
I left the name of StorageCopyTrait since if we need to we can add more methods here and refactor the one method we are adding here.
I was a bit on the fence before, but especially in light of maybe adding one day a way to have temporary storages that are not memory storages we need to make sure that when we copy config we empty the storage properly before. So I agree with the simple approach and if the need arises we can always expand it then.
I think we need someone with authority to decide that or another name is what we want.
Comment #28
johnwebdev CreditAttribution: johnwebdev commentedThis should be updated to reflect the name of the method.
Mean while I'm more +1 to have two methods, I understand the concerns adressed by bircher and mpotter and I buy it. I also think the naming makes sense, in my opinion 'replace' is the closest thing to describe what we actually do and I think the verbosity of actually stating that it's the contents of the storage that is replaced makes it clear to the user what's going on. This behaviour is similar to when you move a folder on the file system to another one where it already exists, you "Replace" it.
Comment #30
alexpott+1 on replaceStorageContents - nice work on the name.
Comment #31
bircheralso change the annotation on the test as the method is now called differently.
Comment #32
johnwebdev CreditAttribution: johnwebdev commentedComment #33
alexpottThe test method name too :)
Comment #34
birchergood catch..
Comment #35
johnwebdev CreditAttribution: johnwebdev commentedComment #36
larowlanThis patch makes a lot of sense, nice work. Some observations below
should not use => should not be used ?
do we need the pass by reference operator given this is an object and therefore is passed by reference by default?
what if collection is empty? should we
$this->fail()
cause we're in an unexpected state?ah, so this is why we have the code above - in some tests we test for collection names, but in others we don't. I think this is brittle, if we care about the behaviour in both cases, we should have an explicit case for with and without a collection, and we can easily do that with a dataProvider
Comment #37
bircherRE #36
1) Yes and also adding an article.
2) Yes, we need to pass it by reference because the collection is an internal state of the storage and the only way to change it is with
StorageInterface::createCollection
which returns a new storage. Attached is a version without the reference which fails the tests.3 & 4) should now be improved.
Comment #39
bircherattached is the already tested patch.
How does one make the testbot not fail patches that are to test only, I thought test-only would do the trick..
Comment #40
borisson_I think that only works when the test-only patch is the first uploaded patch.
I really like the new direction + name here, replaceStorageContents is very clear.
Comment #41
johnwebdev CreditAttribution: johnwebdev commentedInterdiff looks great.
We have better test coverage and proved why we need to pass by reference.
Back to RTBC.
Comment #43
Mixologicdispatcher/testbot issue. back to rtbc
Comment #44
alexpottCommitted and pushed 21c3504993 to 8.8.x and fe487bd5e2 to 8.7.x. Thanks!
Comment #48
quietone CreditAttribution: quietone at PreviousNext commentedpublish change record