Problem/Motivation
Config entities require validation for REST support. Add a generic "plugin ID" constraint and validate values in config entities.
Proposed resolution
Remaining tasks
None.
User interface changes
None.
API changes
New validation constraint is available to be used.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #69 | 2920682-69.patch | 12.21 KB | alexpott |
| #69 | 56-69-interdiff.txt | 1.81 KB | alexpott |
| #56 | 2920682-55.patch | 12.74 KB | alexpott |
| #56 | 51-55-interdiff.txt | 1.17 KB | alexpott |
| #51 | 2920682-51.patch | 12.73 KB | alexpott |
Comments
Comment #2
sam152 commentedComment #3
sam152 commentedOnly added the constraint to one config entity type so far, as each instance will need some manual work to figure out which plugin manager is associated.
Comment #5
sam152 commentedComment #6
alexpottThis looks untested.
This needs an issue to make it more sensible. Created #2925689: ConfigValidation class contains code that is brittle and changing for every addition
Comment #8
dawehnerCan we define a plugin_id type somewhere in core and add validate it, if it defines the plugin manager?
Comment #9
jibranDo we need an upgrade path for all the schema changes?
Comment #10
dawehner@jibran How would you upgrade something like this? What we need a change record explaining what module authors can do now.
Comment #11
cilefen commentedComment #12
borisson_#10: I also don't see a way to update schema's, a change record should be sufficient.
#6 also still needs to be fixed.
$violation_count?Comment #13
borisson_Reroll of #5, fixed my own nitpick of #12.
Comment #24
wim leers#3324150: Add validation constraints to config_entity.dependencies introduced the
ExtensionExists,ConfigExistsandRequiredConfigDependenciesvalidation constraints.So I propose that we rename this
to
PluginExists.Even more so because just
PluginIdimplies it's just a string sequence that would be a valid plugin ID, not that it is a valid reference to a plugin ID, i.e. to one that is actually available.And … as it so happens, that's exactly what we've been doing over at #3324140: Convert field_storage_config and field_config's form validation logic to validation constraints! @phenaproxima contributed a more extensive and flexible version of what's in this patch at https://git.drupalcode.org/project/drupal/-/merge_requests/3047/diffs?co.... Crediting him for that.
Heck, that even included indirect test coverage already!
Let's add explicit test coverage and add that commit of @phenaproxima's to core in this issue? 🤞
P.S.: changing parent, because this validation constraint would be helpful not only for config entities, but also config in general.
Comment #25
phenaproximaHere's the PluginExists constraint from that issue, with explicit test coverage.
Comment #26
wim leersWoah, awesome, thank you so much for that new explicit test, @phenaproxima! Read the code and test coverage in detail, and there's not a single thing I can complain about 🫣🤓
I only have two nits, both of which could easily be addressed on commit — or not at all, depending on what the committer thinks.
s/name/service ID/
s/name/FQCN/
This now just needs a change record just like #3324150: Add validation constraints to config_entity.dependencies got one. Keeping at until that's there, then this is RTBC 🚀🤩
Comment #27
phenaproximaDraft CR added: https://www.drupal.org/node/3330852
Comment #28
phenaproximaI ran the test from #3324984-12: Create test that reports % of config entity types (and config schema types) that is validatable in order to get a list of which parts of core's config schema should get the PluginExists constraint added (in a follow-up, to be sure). Here's what I came up with:
EDIT: Removed
block.block.*:pluginandeditor.editor.*:pluginbecause they are covered in subsequent patches.Comment #29
wim leersThanks for the analysis in #28!
That reminds me of something actually … we should actually be adding a new
type: pluginconfig schema type, and actually use that in all of the places you found in #28. But that'd result in a huge scope. 🤔How about we:
type: plugin_ideditor.editor.*:editor(see #3324984-3: Create test that reports % of config entity types (and config schema types) that is validatable:🔵 78% editor.editor.*). The beauty there is that you can update the focusedKerneltest you added recently (\Drupal\Tests\editor\Kernel\EditorValidationTest) to prove it works with a valid@Editorplugin but not with an invalid one. And then we'll actually know it works in reality because the CKEditor 5 test coverage is very exacting! (See\Drupal\Tests\ckeditor5\Kernel\ValidatorsTest::test(), whereEditorconfig entities are explicitly tested for expected validation errors too.)Comment #30
wim leersThe change record looks solid 👍 Just added a config schema-based example 😊
I spoke too soon in #29.1:
→ I was thinking something like:
… i.e. just like
type: label,type: uuidandtype: pathare examples of subtypes oftype: string, I was proposingtype: plugin_id. The problem with that is that all of the types I just mentioned do not have different subsets — there aren't different "buckets" of UUIDs or anything like that.So — consider #29.1 out of scope. I do think #29.2 is still very valuable though. Do you agree? I can see the case for just committing this as-is, but I think it's stronger with at least one concrete use.
Comment #31
phenaproximaI agree.
This patch adds plugin existence validation for editors.
Comment #32
phenaproximaHuh. I realized that block entities don't have a validation test yet 😳
So I added one (which also tests that the block's plugin ID is validated).
Comment #33
phenaproximaA little bit of cleanup so we're not relying on the internals of action_test as much.
Comment #34
wim leersLooks magnificent! 🤩
Comment #35
borisson_I have one tiny remark, but it is mostly preference.
To decrease complexity, we should probably reverse the first if statement here.
Since this is just preference, and doesn't actually matter at all, I'll leave this at RTBC. Really good to see that we also got at least a little bit of block entities test coverage as well.
Comment #36
phenaproximaIt's inheriting quite a bit of coverage from the base class, too.
Comment #37
larowlanQuick review
is it a name or an ID?
should we add the
: staticreturn type hereShould we catch
\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundExceptionherenote to self/other reviewers - no need to catch
\Drupal\Component\Plugin\Exception\PluginNotFoundExceptionhere because the second argument isFALSEYes, I agree with @borisson_ we should flip the logic and return early here to reduce complexity
in the other issue I reviewed and commented that I personally prefer to refactor away else/elseif - should we have a protected
getClassmethod here that had early returns (and no else/elseif)non_existent is a tame plugin ID for a test @phenaproxima, I was expecting something more humorous 😂
personal preference, but
sprintfwould make this easier to read, feel free to ignoreComment #38
phenaproximaComment #40
phenaproximaThat failure doesn't look related.
Comment #41
phenaproximaRefactored to use the class resolver, rather than ContainerAwareTrait and ContainerAwareInterface, because it's best to keep the number of container-referencing objects in memory as low as possible.
Comment #42
phenaproximaCrediting Wim and Lee for review.
Comment #44
phenaproxima🤔 That failure didn't look related either.
Comment #45
borisson_Back to rtbc, the points in #37 all seem very valid and they have been fixed. I also agree that the refactor to class resolver is a good thing +1.
Comment #46
alexpottI'm not sure I agree here. The problem is that the class resolver can do way more that resolve a service ID. However all of this is not necessary. See \Drupal\Core\Validation\ConstraintFactory::createInstance() - your constraint can inject a plugin manager instance into the constraint :)
Comment #47
wim leers#37.3++ 😆
#46: ooh, nice! So don't use
ContainerAware*norClassResolver, butContainerFactoryPluginInterface👍Comment #48
alexpott@phenaproxima reached out to me to explain #46. Here's what I mean in patch form.
Comment #49
alexpottThis is because of a PHPStan bug not correctly knowing that you can assign a read only property in a static method after a new static().
Comment #50
alexpottHere's a maybe slightly better way of doing the injection. It's tricky because we have to kinda normalise the options - but I think it's okay. Like a manager is required and the this will error nicely.
Comment #51
alexpottImproved the exception from create to be the same as if we'd called normalizeOptions(). In writing a unit test for that I got interesting deprecation messages from the debug class loader about needing return types on \Symfony\Component\Validator\Constraint::getRequiredOptions and \Symfony\Component\Validator\Constraint::getDefaultOption - so added them.
Comment #52
alexpottThere's no need to expose this outside the constraint now so I removed the public. We have the plugin manager for that.
Comment #53
phenaproximaPro tip:
isset()is variadic and only returns TRUE if all the arguments are set. So this can beif (!isset($configuration['manager'], $configuration['value'])).🤯 TIL
Comment #54
alexpott@phenaproxima re #53.1 - that doesn't work. The situation we have hear is that either value or manager must be set. If I make your change then \Drupal\Tests\Core\Plugin\PluginExistsConstraintTest fails as I would expect.
Comment #55
phenaproximaYeah, you're right. My bad!
Comment #56
alexpottThis formulation is perhaps more readable.
Comment #57
wim leers🤩 First
@testWithin core! 👍🤔 Why do we need to throw this explicitly if
::getRequiredOptions()already exists?Is this a consequence of relying on
\Drupal\Core\Validation\ConstraintFactoryto invoke::create()on our constraint since #50? Why doesn't\Symfony\Component\Validator\Constraint::normalizeOptions()handle this for us?(I'm not really opposed, but am kinda concerned: we're about to add many validation constraints, and if we introduce this pattern, it will be copied. So I want to make sure we really want to go this direction.)
Isn't the approach in #48 preferable because it requires no duplication?
Comment #58
phenaproximaIn general I would agree that #48 is probably preferable, for the same reasons: it requires no duplication or specialized unit testing, which -- if Wim is correct that this pattern will be copied -- is prone to mistakes.
This being said, I'm not strongly opposed to #56, if it's what we really want. But at least the concerns have been aired.
Comment #59
alexpottThe problem with #48 is that then you can only create PluginExistsConstraint via the ::create method because it is the only way to set the readonly $pluginManager property. In general I think setting properties via the constructor is preferable. This is also what PHPStan expects. I think that the use-case of a dynamically selecting the service based on constraint configuration is not at all going to be the norm so I don't think the solution in #56 is fine. Like if the service was not dynamic we'd just be injecting it into the constraint validator. This is a special case.
There is another option here. We could derive all the constraints from the list of plugin managers. And then the manager ID would be part of the plugin definition. And we'd have constraints like PluginExists.plugin.manager.block - or something like that. We need to do something like \Drupal\Core\Plugin\PluginManagerPass to discover all the plugin managers - we could even use that container pass to build the list.
Comment #60
phenaproximaThis is getting more and more complicated. Would it maybe make sense to just go back to the ContainerAwareInterface implementation in #38?
Comment #61
phenaproximaLet me be clear that I'm not gonna block this patch if people who know more than me choose to go with #56 or a variation thereof. It's complicated, though, and not a pattern previously seen in core, so maybe we should consider making it explicitly final and/or internal, to underline that this is, in fact, a special case.
Comment #62
wim leersI don't think that's necessary, because subclassing validation constraints is very unlikely to be helpful 😅
Fair! 👍
🫣 This seems excessive.
Let's go with #56. It's really only a single line of code that I'm complaining about — other than that this looks solid 👍
Comment #63
xjmAdded a release note. We're still handwaving a bit how contrib should declare their own dependency.
Comment #64
xjmSorry, that's the release note for the wrong issue. Carry on.
Comment #65
xjmIt still might, like, need one.
Comment #66
phenaproximaChanging to a feature request to clarify.
Comment #67
larowlanI don't think this warrants a release note, a change record is sufficient. There's nothing here that would be disruptive for a site-owner etc, just a new API for developers. As such, removing the tag.
Comment #68
larowlanAnother win for my 'else indicates a chance to refactor/elseif is a code smell' motto :)
neat
I flagged this in my review above (#37.3 and @phenaproxima felt it shouldn't be caught, but I'm still not 100% convinced.
A developer could rely on a service ID and that service may not be around yet/anymore because of a module install/uninstall - which could cause this to bubble up to a WSOD to the site user. If it happens during an install/uninstall the site might be in a bad state.
But then, now we're doing this in the create, we have no other option than an exception, so its kind of moot. With the previous code-path we could return a validation error.
So, let's leave it as is for now, and hope we don't get a bug report about it later.
We could add a
:staticreturn type here, but that feels like something we'd do in bulk across core with a phpstan rule to enforce it rather than a piecemeal approachThere's a slight edge-case here where this isn't true, if we were ultra conservative we'd return NULL and fail the validation here, so I went to investigate if it's a safe assumption.
I found
\Drupal\Component\Plugin\Factory\DefaultFactory::getPluginClasswhich seems to indicate it's not - i.e. it has code to handle a missing class.So in that scenario here, we'd get a TypeError which would halt execution (WSOD) as we've got strict types on and we're returning a NULL instead of a string.
So I think we need to allow for that, i.e. make the return type
?string.is_ahas mixed as it's first argument, so we'd need no other changes https://3v4l.org/dQDrGgetClass is awfully generic, there is an outside chance that Symfony adds a method with a similar name, should we make this
getPluginClassjust to be safe?So tl;dr, I think we should be conservative and change the name and return type on
getClassFine to self-RTBC if you agree and make those changes. Ping me for another review
Comment #69
alexpottTurns out that PluginExistsConstraintValidator:: getClass() is actually unnecessary code. The plugin system pretty much ensures that 'class' is set and provides a helper that throws exceptions when it's not. We can use this helper and let it throw exceptions when this info is not present which is consistent with what would happen if you tried to instantiate a plugin with the same ID via its plugin manager. This helper is already used throughout the plugin system to do exactly what we want.
Addressed #68 and self-rtbcing as per that comment.
Comment #71
larowlan🔥 love it
Committed to 10.1.x and published change record. Nice result folks
Comment #72
wim leersYay! This allowed me to simplify #3324140: Convert field_storage_config and field_config's form validation logic to validation constraints, and the good news is that this indeed shows an increase in the schema validatability in the test coverage I'm proposing for that — see #2164373-37: [META] Untie config validation from form validation — enables validatable Recipes, decoupled admin UIs … for the numbers.
Furthermore, we should adopt this everywhere where
type: stringis actually pointing to a plugin. Issue with initial patch: #3332593: Adopt PluginExists validator in relevant places.Comment #74
wim leersThis is now helping us at #3361534-15: KernelTestBase::$strictConfigSchema = TRUE and BrowserTestBase::$strictConfigSchema = TRUE do not actually strictly validate! 🥳