Problem/Motivation
Currently, our code is full of #submit and #validate referenced that look like this:
<?php
$form['submit']['#submit'][] = array($this, 'submitMethod');
That has 3 distinct problems.
1. It's bad DX, it's an array of arrays.
2. It's problematic for serialization because we can't control where and how the form object is serialized, which often has referenced to services and so on. I'd like to try us hard avoid serializing the form object, as we already re-initialize it into $form_state['build_info']['callback_object'] when getForm() is called again.
3. The above syntax leads to super-weird behavior because *the object the method will be called on is not the same as $form_state['build_info']['callback_object']* during form submission. This is especially bad for entity forms, which maintain $this->entity as the primary reference for the entity being built and needs hacks like https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21... and https://github.com/karanpoddar/monitoring/blob/gsoc-plugin/src/SensorFor... to function.
Proposed resolution
Allow #submit, #validate and probably other callbacks to be specified with just the method name. Check if the string is a valid method and convert it to the necessary structure for call_user_func_array() on the fly.
Edit: allow #submit, #validate and probably other callbacks to be specified with just the method name prefixed by ::. This should be familiar from the @covers annotations on PHPUnit test methods.
Remaining tasks
User interface changes
API changes
Only additions, as the old structure would still work but would be discouraged.
| Comment | File | Size | Author |
|---|---|---|---|
| #35 | interdiff.txt | 718 bytes | tim.plunkett |
| #35 | form_execute_handlers-2309323-35.patch | 46.34 KB | tim.plunkett |
| #32 | form_execute_handlers-2309323-32-interdiff.txt | 1.39 KB | berdir |
| #32 | form_execute_handlers-2309323-32.patch | 46.34 KB | berdir |
| #24 | form_execute_handlers-2309323-24-interdiff.txt | 571 bytes | berdir |
Comments
Comment #1
berdirComment #2
tim.plunkettAdded test coverage and one conversion in the wild.
Comment #3
berdirThe change in the profile form handler thing should fail because it is actually a form alter and not the form object.
Attached patch includes the tests but not that change.
Also adds support for this syntax for #process, #ajax 'callback's and #element_validate.
Also removes the $form_state['controller'] = $this hack in EntityForm.
And converts everything I could find.
Comment #6
berdirMade the exact same mistake :)
Comment #8
berdir...
Comment #9
dawehnerDid you considered moving this to a helper method?
Comment #10
berdirI did think about it, but wasn't sure where? Many are on different classes, do they have a common dependency? Should we make it a trait? Everything else seemed like adding more code than we'd be able to re-use. The actual code that executes the callback is sometimes slightly different, so it didn't seem like we could standardize more than that?
Comment #11
berdirConverted to a static method on FormBuilder, let the name bikeshedding begin. I can also change it to a trait.
Comment #12
berdirThe full patch didn't make it.
Comment #13
dawehnerWe could also do an explicit unit test now that we have a public function.
Comment #14
berdirSure, can do that.
Comment #15
berdirUpdated for FormState
Comment #18
berdirRe-roll, rename processCallback() to prepareCallback(), as suggested by tim.
Instead of FormBuider, we *could* move this method now also to FormState, thoughts?
Comment #19
tim.plunkettThat's a fine idea. Would you keep it static? I guess there's no reason to...
Comment #20
berdirYep, no need for it to be static.
Comment #22
berdirUps.
Comment #23
andypostThis is great to see that patch.
1) DX++ but there's a lot of of
array(get_class($this), 'someMethod')in core, could be fixed in follow-up2) Not sure the problem of 2 and 3 from subject is fixed, at least there's no tests
3) needs change record and update https://www.drupal.org/node/1734540 at least
is there a test to make sure that $this is not serialized?
unused
Comment #24
berdir1) Right, but most of them are not form callbacks but others, #pre_render, exists callbacks and similar stuff, where there is no form context to make this possible.
3) Not sure if we need a new change record, I will update https://www.drupal.org/node/1734540 when this is committed. For entity forms, because I remove that hack, it might actually be required to use it like this. Will create one.
2) / 1.: As discussed, it still is serialized, we don't care about that, we only care about using the right now for now. We could open a follow-up to look into that, but there's no guarantee that it will always be the case, due to mentioned non-form callbacks where it is still required for example. See also #2313931: Merge $form_state['controller'] with $form_state['build_info']['callback_object'] and add a method for it., where I removed that code too without the other changes from here, there tests are failing, I think because of this.
2. Removed.
Comment #25
tim.plunkettThis is definitely out of scope.
Awesome work @Berdir!
Comment #26
berdirCreated https://www.drupal.org/node/2325875
Comment #27
chx commentedErm. What.
#validate => 'foobar', comes another class extending and adds a foobar method and suddenly THAT runs but the original validate doesn't? That's a good idea how? Of course, this is a very contrived example and very unlikely to be a problem esp because camelcase vs underscores. But I think laying down landmines is not a particularly good idea even in places where we think noone will ever trod. Also, how are we going to document/explain/teach this?? It's simply not how PHP behaves.I recommend changing
'methodname'to'$this->methodname'. Yes it's a Drupalism still but there is no ambiguity and I believe it's fairly trivial for anyone to see what's up.Comment #28
chx commentedTalked to berdir, updating the issue summary with
::methodnameas a suggestion.Comment #29
chx commentedAnd yes: thanks for the great patch and great idea of using :: this will be a lot better than the
array($this, 'foo')we have currently.Comment #30
tim.plunkettThat's the same problem as in HEAD, and with :: as a prefix. If you override the method, you're overriding it.
Comment #31
tim.plunkettThe talk of subclassing was a red herring, @Berdir explained the actual problem in IRC. Carry on.
Comment #32
berdirOk, converted the patch manually to ::, also removed a left-over use in FormBuilderTest, interdiff only contains the actual implementation changes, I hope I got all the callbacks.
Comment #33
tim.plunkettI'll let @chx RTBC, but this is a fine change. Thanks @Berdir!
Comment #35
tim.plunkettMissed a spot.
Comment #36
chx commentedThanks so much for the work, it does look great.
Comment #37
webchickThis looks absolutely fantastic but doesn't seem to apply after the other patches that went in today.
Comment #38
webchickActually --3way works. (Thanks, Tim!)
Committed and pushed to 8.x. Thanks!
Comment #40
yched commentedAwesome change, great for native forms.
Would be nice if Field Widgets could leverage somthing like this for their own #fapi_callbacks (i.e methods that don't live in the main Form object)