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.

CommentFileSizeAuthor
#35 interdiff.txt718 bytestim.plunkett
#35 form_execute_handlers-2309323-35.patch46.34 KBtim.plunkett
#32 form_execute_handlers-2309323-32-interdiff.txt1.39 KBberdir
#32 form_execute_handlers-2309323-32.patch46.34 KBberdir
#24 form_execute_handlers-2309323-24-interdiff.txt571 bytesberdir
#24 form_execute_handlers-2309323-24.patch46.64 KBberdir
#22 form_execute_handlers-2309323-22-interdiff.txt2.07 KBberdir
#22 form_execute_handlers-2309323-22.patch46.91 KBberdir
#20 form_execute_handlers-2309323-20-interdiff.txt9.93 KBberdir
#20 form_execute_handlers-2309323-20.patch46.85 KBberdir
#18 form_execute_handlers-2309323-15-interdiff.txt5.93 KBberdir
#18 form_execute_handlers-2309323-15.patch45.2 KBberdir
#15 form_execute_handlers-2309323-15-interdiff.txt3.71 KBberdir
#15 form_execute_handlers-2309323-15.patch45.99 KBberdir
#14 form_execute_handlers-2309323-14-interdiff.txt2.01 KBberdir
#14 form_execute_handlers-2309323-14.patch46.13 KBberdir
#12 form_execute_handlers-2309323-11.patch44.13 KBberdir
#11 form_execute_handlers-2309323-11-interdiff.txt6.08 KBberdir
#8 form_execute_handlers-2309323-8-interdiff.txt1.76 KBberdir
#8 form_execute_handlers-2309323-8.patch43.03 KBberdir
#6 form_execute_handlers-2309323-6-interdiff.txt687 bytesberdir
#6 form_execute_handlers-2309323-6.patch44.79 KBberdir
#3 form_execute_handlers-2309323-3-interdiff.txt42.82 KBberdir
#3 form_execute_handlers-2309323-3.patch45.15 KBberdir
#2 interdiff.txt2.34 KBtim.plunkett
#2 form_execute_handlers-2309323-2.patch4.59 KBtim.plunkett
#1 callback-methods-form-2309323-1.patch2.25 KBberdir

Comments

berdir’s picture

Status: Active » Needs review
StatusFileSize
new2.25 KB
tim.plunkett’s picture

Issue tags: +API addition
StatusFileSize
new4.59 KB
new2.34 KB

Added test coverage and one conversion in the wild.

berdir’s picture

The 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.

The last submitted patch, 2: form_execute_handlers-2309323-2.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 3: form_execute_handlers-2309323-3.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new44.79 KB
new687 bytes

Made the exact same mistake :)

Status: Needs review » Needs work

The last submitted patch, 6: form_execute_handlers-2309323-6.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new43.03 KB
new1.76 KB

...

dawehner’s picture

+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -859,6 +859,10 @@ public function doBuildForm($form_id, &$element, &$form_state) {
+        // Support method names of the form object.
+        if (is_string($process) && method_exists($form_state['build_info']['callback_object'], $process)) {
+          $process = array($form_state['build_info']['callback_object'], $process);
+        }

+++ b/core/lib/Drupal/Core/Form/FormSubmitter.php
@@ -129,6 +129,10 @@ public function executeSubmitHandlers(&$form, &$form_state) {
+        // Support method names of the form object.
+        if (is_string($function) && method_exists($form_state['build_info']['callback_object'], $function)) {
+          $function = array($form_state['build_info']['callback_object'], $function);
+        }

+++ b/core/lib/Drupal/Core/Form/FormValidator.php
@@ -69,6 +69,10 @@ public function executeValidateHandlers(&$form, &$form_state) {
+      // Support method names of the form object.
+      if (is_string($function) && method_exists($form_state['build_info']['callback_object'], $function)) {
+        $function = array($form_state['build_info']['callback_object'], $function);
+      }

Did you considered moving this to a helper method?

berdir’s picture

I 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?

berdir’s picture

Converted to a static method on FormBuilder, let the name bikeshedding begin. I can also change it to a trait.

berdir’s picture

StatusFileSize
new44.13 KB

The full patch didn't make it.

dawehner’s picture

We could also do an explicit unit test now that we have a public function.

berdir’s picture

Sure, can do that.

berdir’s picture

Updated for FormState

Status: Needs review » Needs work

The last submitted patch, 15: form_execute_handlers-2309323-15.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new45.2 KB
new5.93 KB

Re-roll, rename processCallback() to prepareCallback(), as suggested by tim.

Instead of FormBuider, we *could* move this method now also to FormState, thoughts?

tim.plunkett’s picture

Instead of FormBuider, we *could* move this method now also to FormState, thoughts?

That's a fine idea. Would you keep it static? I guess there's no reason to...

berdir’s picture

Yep, no need for it to be static.

Status: Needs review » Needs work

The last submitted patch, 20: form_execute_handlers-2309323-20.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new46.91 KB
new2.07 KB

Ups.

andypost’s picture

This 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-up
2) 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

  1. +++ b/core/lib/Drupal/Core/Entity/EntityForm.php
    @@ -317,11 +312,6 @@ protected function updateFormLangcode(FormStateInterface $form_state) {
       public function buildEntity(array $form, FormStateInterface $form_state) {
         $entity = clone $this->entity;
    -    // If you submit a form, the form state comes from caching, which forces
    -    // the controller to be the one before caching. Ensure to have the
    -    // controller of the current request.
    -    $form_state['controller'] = $this;
    

    is there a test to make sure that $this is not serialized?

  2. +++ b/core/modules/system/src/Controller/FormAjaxController.php
    @@ -8,6 +8,7 @@
    +use Drupal\Core\Form\FormBuilder;
    
    index 63a17d1..31b0950 100644
    --- a/core/modules/system/src/Form/CronForm.php
    
    +++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
    @@ -11,6 +11,7 @@
    +use Drupal\Core\Form\FormBuilder;
    

    unused

berdir’s picture

StatusFileSize
new46.64 KB
new571 bytes

1) 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.

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

is there a test to make sure that $this is not serialized?

This is definitely out of scope.

Awesome work @Berdir!

berdir’s picture

chx’s picture

Status: Reviewed & tested by the community » Needs work

Erm. 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.

chx’s picture

Issue summary: View changes

Talked to berdir, updating the issue summary with ::methodname as a suggestion.

chx’s picture

And 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.

tim.plunkett’s picture

Erm. What. #validate => 'foobar' , comes another class extending and adds a foobar method and suddenly THAT runs but the original validate doesn't?

That's the same problem as in HEAD, and with :: as a prefix. If you override the method, you're overriding it.

tim.plunkett’s picture

The talk of subclassing was a red herring, @Berdir explained the actual problem in IRC. Carry on.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new46.34 KB
new1.39 KB

Ok, 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.

tim.plunkett’s picture

I'll let @chx RTBC, but this is a fine change. Thanks @Berdir!

Status: Needs review » Needs work

The last submitted patch, 32: form_execute_handlers-2309323-32.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new46.34 KB
new718 bytes

Missed a spot.

chx’s picture

Status: Needs review » Reviewed & tested by the community

Thanks so much for the work, it does look great.

webchick’s picture

Status: Reviewed & tested by the community » Needs work

This looks absolutely fantastic but doesn't seem to apply after the other patches that went in today.

webchick’s picture

Status: Needs work » Fixed

Actually --3way works. (Thanks, Tim!)

Committed and pushed to 8.x. Thanks!

  • webchick committed 6a6e601 on 8.0.x
    Issue #2309323 by Berdir, tim.plunkett: Allow #submit and #validate to...
yched’s picture

Awesome 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)

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.