Problem/Motivation
Plugins that need forms can currently only have one form, and it must be included in the plugin code directly.
We have the need in modules like Commerce and Outside In to provide multiple forms.
Also it is desirable to decouple the plugin form from the plugin itself.
Proposed resolution
Add a PluginFormFactory that inspects the plugin annotation for valid classes implementing PluginFormInterface.
Add a PluginAwareInterface for those decoupled forms to use, instead of making the plugin itself implement PluginFormInterface.
Add a PluginFormBase that combines PluginAwareInterface and PluginFormInterface
Remaining tasks
N/A
User interface changes
N/A
API changes
API additions only
Data model changes
N/A
Comment | File | Size | Author |
---|---|---|---|
#41 | interdiff.txt | 440 bytes | tim.plunkett |
#41 | 2763157-plugin-41.patch | 28.62 KB | tim.plunkett |
#39 | interdiff.txt | 13.85 KB | tim.plunkett |
#39 | 2763157-plugin-39.patch | 28.63 KB | tim.plunkett |
#37 | interdiff.txt | 12.56 KB | tim.plunkett |
Comments
Comment #2
tim.plunkettComment #3
tim.plunkettHere's a new approach, lifted from recent work on #2753941: [Experimental] Create Outside In module MVP to provide block configuration in Off-Canvas tray and expand site edit mode
Comment #5
tim.plunkettComment #6
tim.plunkettThis allows another block form that wishes to remove the visibility settings to be extremely simple:
Comment #7
tim.plunketttedbow pointed out that the code here is completely generic.
Reverting back to being non-block specific.
Comment #9
tim.plunkettNever really noticed how the current implementation of DefaultPluginManager::processDefinition() would fail for entity types.
Comment #10
tim.plunkettYou'd think that all plugins would have classes. Pretty central to them being plugins.
But nope, typed data doesn't do that! See core/config/schema/core.data_types.schema.yml, compare line 46 to 54.
Comment #12
neclimdulThat doesn't sound right.
Having a default form seems weird. If you expect one form to show up and see another... very weird. Would this have any implications with accidentally bypassing permissions on a form? Especially if someone where to proxy the operation from a url param and apply permissions at the route level.
I wonder for usability/flexibility if it makes sense to take operation as an argument rather then relying on the state of the object?
heh
inheritdoc?
Generic note, the term "operation" I guess makes sense in the terminology in forms but seems weird in the plugin interfaces. We just see "operation" in the method without much explanation as to how that connects to anything. I don't know if it makes sense to change it or not because its in Core not component but its an observation I wanted to put down as I was confused without explanation from Tim.
Comment #13
tedbowYes I think this not the best term. It seems like "context" but we probably don't want to reuse that term here. What about "form_context"?
I think the intention here is that some plugins might want a different form to show up in different "form_contexts"(if we change the term). But most plugins will not have a special form for a specific form_context.
For example in the related issue #2753941: [Experimental] Create Outside In module MVP to provide block configuration in Off-Canvas tray and expand site edit mode
Certain blocks might want to use a different form in the "offcanvas" form_context. We have to have a default form context because this a new concept that existing plugins won't know about. We can't expect all plugins to know about all form contexts(a new one might be used by contrib). So we have to have "default" form context that we use if the plugin doesn't have a specific one form context being asked for.
Comment #14
tim.plunkettAgreed. I don't have a better name for $operation yet, but this fixes the rest.
Comment #15
neclimdulNice, that already helps.
Comment #16
EclipseGc CreditAttribution: EclipseGc at Acquia commentedCan we do a proper use statement and fix the typehint?
This and the corresponding add in the diff all seems like an unrelated change, but it's a good one and probably not worth blocking on.
In general, I definitely don't hate this. That's actually probably high praise at this point in my life since mostly I hate things or I don't. :-D Not sure how $this->operation was getting set previous. Does the block entity just default that to default now? Can I change it? or do I need a different wrapper?
Eclipse
Comment #17
tim.plunkett1) Uh yeah that's a mistake, thanks PHPStorm. We even had the use statement already, just weren't using it.
2) That's needed to make the form reusable. See comment #6 above
So the operation was coming from the Block entity annotation:
Comment #18
tedbowSo I ran into a problem when I was trying to use this in #2753941: [Experimental] Create Outside In module MVP to provide block configuration in Off-Canvas tray and expand site edit mode
I may have been missing something but here is what I found:
I was trying to make a OffCanvas form class for the SystemBrandingBlock plugin.
My intention was to show the regular plugin settings plus the site name and slogan text fields. The idea being that if someone clicks on the site name and it opens the form in the Offcanvas tray they very well might want edit the site name. If you don't like this approach, the same problem arises if you just want to show a modified version of the plugin form.
The problem I found is because the way \Drupal\Core\Plugin\PluginFormManager and \Drupal\Core\DependencyInjection\ClassResolver work the new form object would have no knowledge of the actual plugin instance it was trying to make a form for. So it can't for instance use the plugin configuration for default form values like SystemBrandingBlock itself does
So I created \Drupal\Core\Plugin\PluginAwareInterface
And updated \Drupal\Core\Plugin\PluginFormManager to use in the same way it is using \Drupal\Core\Form\OperationAwareFormInterface
I am not sure if I should have made it PluginAwareFormInterface but I could the interface being used outside the form context.
Thoughts? I am missing an existing way to solve this problem? Is there a better way?
Comment #19
chx CreditAttribution: chx as a volunteer commentedHowever I see no documentation on how to use this capability. Perhaps PluginFormManagerInterface::getFormObject would be a good place to talk about how to change the annotation?
Comment #20
dawehnerI disagree with chx a bit. This is not just a nice patch, its like super super super nice! On the longrun it will enable a lot of usecases and make writing plugins less annoying.
I don't see an additional test case in DefaultPluginManager for that. This cannot really be it :)
<3 how this enables plugin forms to be separate from the plugins itself. This is just wonderful. To be clear though, on order to make that useful, we need to adapt a lot of the existing plugin codes, which rely plugin instances itself to implement the PluginFormInterface
This seems to be generic and could live in the component as well ...
Does this really have to be PluginInspectionInterface ... what about just allowing any kind of object.
Can we tell a bit more about the architecture in the documentation:
a) This allows to decouple forms from plugin classes
b) This allows to define multiple forms per plugin.
It would be nice to document why we need the PluginInspectionInterface here ... I guess its in order to be able to pull out the definition from the plugin?
... Can we explain what a secondary form does?
From the definition it looks more like a EmptyBlockForm class.
<3
For a little bit better readability we could check with
assertInstanceOf
for the different used classes.This could/should maybe be part of the DefaultPluginManager ... on the other hand, why is this default form related code not be part of just PluginFormManager ?
Comment #21
benjy CreditAttribution: benjy commentedThe patch is looking good, love the idea.
Comment #22
neclimdulMan I don't like being a bummer but we're just tossing interfaces all over the place now. I feel like we're stretching past the original goal of the issue and that makes me nervous. The interdiff in #18 could easily be its own patch with its own CR.
Comment #23
tedbowIt maybe should be it's own issue but if it was I would think the new issue should block this 1.
There really don't seem to be any point in allowing plugins to have multiple forms if those forms don't have any knowledge of the plugin instance itself. You can't really make a useful for in that case.
I was even thinking about changing this to enforce that $form_object be a instance of PluginAwareInterFace. Though maybe there is use case I am not thinking of.
Comment #24
tim.plunkett1) Added a test case
2) Agreed
3) Moved
4) Very true! Fixed
5) Done
6) Well, the implementation needs it to be PluginInspectionInterface, but the interface doesn't necessarily care... So moving this to an exception in the interface instead.
7) Renaming it. The name was just bad, and the docs matched :)
8) So true! Renamed :)
10) For the first part, we want to be sure it's the EXACT same object. The second half, we need to check for the operation and plugin as well...
11) I tried to make DefaultPluginManager implement PluginFormInterface, but it meant changing the constructor, which would break EVERY SINGLE plugin manager in core. Usually I wouldn't consider __construct to be an API, but DefaultPluginManager::__construct sure feels like one.
Moving the default form related code directly inline here is also possible, but I don't feel strongly about it.
Comment #26
bojanz CreditAttribution: bojanz at Centarro commentedGreat job on getting this started!
I actually reinvented this API for Commerce Payment just two days ago.
Stand-by for opinions.
I struggled with this too, and ended up going with $form_id since it felt more generic, but it doesn't feel significantly clearer (if at all).
+1000. Plugin forms should be plugin aware by default. I'd go as far as injecting $plugin before the dependencies, but I guess setter injection does the job too.
We should think about the right convention for namespacing plugin forms. Feels odd to dump them in "Form" when they aren't real forms, they don't implement the real FormInterface.
So how about "PluginForm"? With an optional (or required?) grouping by plugin class?
That way modules can do Drupal\mymodule\PluginForm\EmptyBlockForm or even Drupal\mymodule\PluginForm\MyBlock\EmptyBlockForm (if there are multiple plugins that need to declare the same form).
Isn't this technically the PluginFormFactory? We usually use Manager to mean "I had no clue what to name this class", but a more clear alternative is available here. And since getFormObject() is returning new instances, it might make more sense for it to be createForm() or createInstance().
Would be great to have some kind of PluginWithFormsInterface that gives us getFormClass() and hasFormClass(), that can then be used by the PluginFormManager.
Why not 'configuration' instead of 'default'? It is the configuration form, which has a lot more meaning than 'default'.
This immediately looked weird. Why are we not type hinting the PluginInspectionInterface? Then I saw above that it was dawehner's suggestion, and discussed it with him on IRC. I think that it makes total sense to type hint $plugin with PluginInspectionInterface. There is no reason not to have a minimal interface that says "this object is a plugin", and currently that's PluginInspectionInterface, it's extended by every single plugin type interface we have. It introduces no additional coupling. I think dawehner agrees now, but I won't put words in his mouth. So, let's reconsider (both here and for setPlugin() in the PluginAwareInterface).
Missing/improper docblocks:
Comment #27
bojanz CreditAttribution: bojanz at Centarro commentedNow on a more subjective note. It feels messy to try and support both the old configuration-form-on-the-plugin and the new plugin-form-outside-the-plugin approach in this API. Just look at the PluginFormInterface naming, buildConfigurationForm() makes no sense on plugin forms, since most plugin forms are not configuration forms. You usually have one central "configuration" form, the rest is its own thing.
Ideally I would do something like this:
1) Create a PluginConfigurationFormInterface as a copy of the current PluginFormInterface, deprecate the current PluginFormInterface methods in their favor. Switching all of core could be a single followup.
2) Add the more generic methods (buildForm / validateForm / submitForm) to PluginFormInterface
3) Repurpose PluginFormManager to only support the external forms (and fallback between them).
4) Convert the block configuration form to an external form in order to perform the fallback needed by your UI.
This gives us a clear distinction between the configuration form VS plugin forms, for cases where it matters.
EDIT: D'oh, actually that won't work cause the external plugin forms would still need to implement the old deprecated methods. So we might be stuck with the ugly-ugly-naming unless we invent a whole different interface for external forms.
Still, #3 and #4 make sense.
Comment #28
tim.plunkettDiscussed with @bojanz, @dawehner, and @tedbow in IRC.
$form_id is an even more specific name that means something else entirely. Sticking with $operation.
Punting on the namespace for today, will revisit that. Added to Remaining Tasks in issue summary.
Yes, PluginFormFactory is a much better name.
Also punting on the PluginWithFormsInterface, will revisit again. Added to Remaining Tasks in issue summary.
Switched from 'default' to 'configuration'
Restored the typehint of PluginInspectionInterface.
Also added PluginFormBase.
Also we decided NOT to do #27.
Comment #30
tim.plunkettFixed the test, had failed to clean some stuff up.
Comment #31
twistor CreditAttribution: twistor as a volunteer commentedAwesome. I've been working on something similar.
Can we add a
PluginFormFactory::hasForm()
method to determine if a plugin has a form without catching the exception?Does this need an
ltrim($definition['form'][$operation], '\\')
?Comment #32
tim.plunkettI think instead of PluginWithFormsInterface, we should go with #31.1, and I've made hasFormClass a public method, and getFormClass a protected method.
Also fixing #31.2, which is a good idea. Also running ltrim on get_class(), because even though I know it won't have a leading slash, there is nothing in the PHP docs guaranteeing that.
Additionally, I've gone with @bojanz's suggestion from IRC and codified the PluginForm namespace for these decoupled forms.
Finally, removing some of the changes I made to block code that are out of scope here.
As such, all of the remaining tasks are resolved, and I've updated the issue summary.
Comment #33
twistor CreditAttribution: twistor as a volunteer commentedThe code looks spot on. Since I don't have any real criticism, here are my own meandering thoughts.
Interesting choice. I've been using
PluginDir\Form
.Question: Does 'configure' make more sense than 'configuration', when considered along with 'add' and 'edit'?
I would love it if this is how we roll now.
Comment #34
XanoThanks for taking this on! There are a few things I believe are important to discuss to make sure this approach is conceptually sound:
PluginFormInterface
by wrapping it with a decorator inPluginFormFactory::createInstance()
.Patch review:
This needs rewriting to the third person singular.
I'm struggling to describe awareness myself, but I think we need to try and come up with something more descriptive than the current comment.
Plugin instances do not necessarily need to implement
PluginInspectionInterface
. Could we support that here, or is there no way around this?This needs support for
\Drupal\Component\Plugin\Definition\PluginDefinitionInterface
.This needs support for
\Drupal\Component\Plugin\Definition\PluginDefinitionInterface
.$pluginInstance
I'm not sure we should use class names in one-line constructor summaries, as they are invalid for child classes and cannot be automatically refactored.
Weird, but I like it!
Thanks for thinking about error handling! Seeing as plugin IDs are ambiguous if the plugin type is unknown, could we make this a bit more specific by including the plugin class name as well? It's not perfect, but it definitely helps according the handful community members who've complained to me about other such errors in the Plugin System, as it's a better indicator of where they need to search for the cause of the problem.
I like this bit about
PluginAwareInterface
! Can we document this onPluginFormInterface
as well?What about renaming this to
::getform()
for consistency with::hasFormClass()
andEntityFormBuilder::getForm()
?What about
::hasForm()
? It's shorter and calling code does not have to know about how the check is performed internally.Thanks for including an example conversion. Just a heads-up about a future re-roll: this conflicts with #2537732: PluginFormInterface must have access to the complete $form_state (introduce SubFormState for embedded forms), which was RTBC'd just this morning.
$pluginFormFactory
Thanks again for working on this useful feature :)
Comment #35
bojanz CreditAttribution: bojanz at Centarro commentedYay for the namespace change.
I still think hasFormClass and getFormClass methods belong on the plugin itself. After all, it's the plugin definition they are analyzing.
Feels weird to ask a factory if it has a form class. And making it $factory->hasForm() is even more confusing, makes me think it caches the created instances.
Questions:
Should setPlugin return $this, like setters usually do?
Shouldn't the definition key be "forms" instead of "form"? We tend to use plurals in these cases (multiple values).
Guessing it's "form" cause of the entity annotation, but there it's nested under "handlers".
Comment #36
twistor CreditAttribution: twistor as a volunteer commented"it's the plugin definition they are analyzing" is exactly the reason why it doesn't belong in the plugin. Personally, I'd be fine with moving this to DefaultPluginManager, or DefaultFormPluginManager, or something. There is an issue here with the ability to override how a form is derived per-plugin-type.
Fluent interfaces should be designed specifically for the purpose. Don't pollute the stack.
Comment #37
tim.plunkett#33
1) That is a much better suggestion!
#34
1) Borrowing the wording from \Symfony\Component\DependencyInjection\ContainerAwareInterface
2) No, see above discussion. We need this.
3/4) DefaultPluginManager::processDefinition() already exists and is only compatible with arrays. Plus, PluginDefinitionInterface is useless because it doesn't have an arbitrary get/set, those are only on EntityTypeInterface.
5) What's the difference? I'm not renaming it to setPluginInstance(), I think this is fine.
6) We use this ALL OVER. Take it up in a doc standards issue.
7) :)
8) InvalidPluginDefinitionException specifically asks for the plugin ID, and other exceptions in core do the same as here. As you point out, it's a more widespread problem, and I'm not going to start changing the status quo here.
9) Good idea! Done.
10) EntityFormBuilder::getForm() returns a FAPI array, this does not. I originally used getFormObject, but changing to createInstance upon other suggestions
11) Agreed.
12) Yep, I noticed that. I'll gladly reroll either patch depending on which lands first.
13) Hah, yes. Fixed.
#35 (going to pretend you numbered your feedback)
1) I tried adding that to plugins, it was very strange and didn't quite work out.
2) Agree with @twistor, I think it's fine without being fluent
3) I was indeed mimicking entity types. But you're right
#36
Going to post a patch now with the rest of the feedback, and then work on this.
Comment #39
tim.plunkettOn second look, this isn't too bad.
It provides the ability to have different approaches per plugin type.
Comment #41
tim.plunkettUgh stupid mistake when I copied commerce's docs.
Comment #42
bojanz CreditAttribution: bojanz at Centarro commentedAwesome! +1 for RTBC.
Comment #43
dawehnerI agree with bojanz
Comment #44
tim.plunkettAdded a CR: https://www.drupal.org/node/2773829
Comment #45
webchickRead through the code and actually all seems pretty straight-forward to me. The one part I got pretty confused by was:
...it's not clear to me how this actually defines multiple forms. Tim explained that there's a "magic" config = ... form set in the background. Fair enough. Fortunately, the new change record has a much better example.
Seems like this patch has lots of support from plugin system maintainers, as well as people with lots of experience implementing entities, and the test coverage is quite extensive, so... :)
Committed and pushed to 8.2.x (after removing an unused use statement - thanks https://github.com/alexpott/d8githooks!). Thanks!
Comment #47
Xano#37: Thanks! Those are good improvements :)
I've never liked this approach (also elsewhere, such as
PluginInspectionInterface::getId()
), becausePluginInspectionInterface::getDefinition()
gets us all the information we need without adding more methods to plugin classes that are often too big already. This also means plugin classes that provide multiple forms now require a dependency on\Drupal\Core
instead of just\Drupal\Component
.So far I've understood we call plugins what we discover, which generally means the definition/class, and instances are literal class instances of these plugins.
I raised #2653106: Introduce SubformInterface in #34. Is there a reason that was not taken into account for this issue? What technical reasons are there to use
PluginFormInterface
rather than a solution that builds on #2537732: PluginFormInterface must have access to the complete $form_state (introduce SubFormState for embedded forms)'sSubformStateInterface
?I wanted to ask this yesterday, but a browser failure threw away my comment and I had forgotten to include it when I re-wrote everything: why the fallback operation? There is no non-test usage of this. What are examples of wanting to fall back to a different form automatically?
It's not a standards issue. It would be nice to learn why you disagreed with the suggestion.
Comment #48
tim.plunkett1) Because getDefinition() could return either an array, or PluginDefinitionInterface (which has NO mechanism for inspecting the definition), there's no one way to inspect the definition. This way actually works.
2) I don't know anywhere we've made that distinction.
3) I stuck with PluginFormInterface for BC. And while SubformState has consensus, SubformInterface is less vetted.
4) #2753941: [Experimental] Create Outside In module MVP to provide block configuration in Off-Canvas tray and expand site edit mode is an example of needing a fallback
5) Until the docs standards say not to use the class name in the constructor, I see no reason to stop doing that. One-line docs for __construct are useless anyway. Any constructor that does more than assign parameters to properties is doing too much, there's no reason to document anything
Comment #49
neclimdulShoot this went in fast! Last I checked this was failing :(. A little disappointed because I wanted to discuss this more because "aware" could mean "give me a plugin" or "let me handle a plugin" and I'm not really comfortable with where it sits still.
Comment #50
tim.plunkett"Provides an interface for objects that depend on a plugin"
It mirrors ContainerAwareInterface
Comment #51
XanoThat means it's consistent, but not necessary that it's good. Both @neclimdul and I raised this issue now, and that's not because we're new to the concept.
Comment #52
bojanz CreditAttribution: bojanz at Centarro commented"Aware" is pretty standard speak for the interfaces that allow setter injection (which is all this is).
Not sure we can do better while still doing setter injection.
The only real alternative is using a custom class resolver to pass the $plugin before all other parameters in __construct / create (just like we do for $plugin_id and friends in plugin classes).
Comment #56
mikeryanNote this patch has some negative consequences at #2796953: [regression] Plugins extending from classes of uninstalled modules lead to fatal error during discovery.
On the other hand, this should be really helpful to allow contrib to implement forms for the core migration plugins...