Form API Changes for Drupal 7, Part 1: $form_state changes

You may know that lots of delicious things have happened to Drupal's Form API in Drupal 7. (Only a geek can say "delicious" and "Form API" in the same sentence. Try it!) The finest minds in the business have been working on it, I can assure you. Give effulgentsia, fago, frando, and chx a big hug when you see them, because Form API is much improved. (Sorry to those of you I forgot to name, but THANKS!)

I'm going to do a series covering Form API changes, starting with this one. I won't attempt to cover the deep details, just the things that ordinary developers might use:

  1. $form_state changes and form builder function signature changes
  2. AJAX Forms changes
  3. New properties (#attached and many friends)

Let me know if you have other topics to suggest.

OK, to business. This article is mostly parroted from the api.drupal.org topic: Form Generation. Thanks to Alex Bronstein (effulgentsia) for his reviews and contributions to that doc.

Don't forget that the form builder function signature changed!

In Drupal 6 the form builder function looked like this:

function my_module_funky_form(&$form_state) { ... }

but in Drupal 7 it's

function my_module_funky_form($form, &$form_state, ... ) { ... }

$form_state in Drupal 7

Mostly the members of the $form_state array are the same ones you know and love from Drupal 6:

  • $form_state['values']: An associative array of values that have been submitted to the form. The validation and submit functions use this array for nearly all their decisionmaking. (Note that #tree determines whether the values are a flat array or an array whose structure parallels the $form array.) This is nearly the same as it was in D6.
  • $form_state['rebuild']: If the submit handler sets $form_state['rebuild'] to TRUE, submission is not completed and instead the form is rebuilt using any information that the submit function has made available to the form builder function via $form_state. This is commonly used for wizard-style multi-step forms, add-more buttons, and the like. For further information see drupal_build_form(). This is the same as D6.
  • $form_state['redirect']: a URL that will be used to redirect the form on submission. See drupal_redirect_form() for complete information. This should always be used instead of drupal_goto() in a forms context. Note that $form['#redirect'] went away in Drupal 7 and no longer has any effect.
  • $form_state['storage']: $form_state['storage'] is no more! It used to be the place for application-specific values, but now it has no specific meaning. Now nearly all $form_state keys persist in a multi-step form, so the recommended approach is to use $form_state['your_module']['whatever']. ($form_state['storage'] still works for persistent storage, just like $form_state['timbuktu'] works.)
  • $form_state['triggering_element': (read-only) The form element that triggered submission. This is the same as the deprecated $form_state['clicked_button']. It is the element that caused submission, which may or may not be a button (in the case of AJAX forms.) This is often used to distinguish between various buttons in a submit handler, and is also used in AJAX handlers.
  • $form_state['cache']: The typical form workflow involves two page requests. During the first page request, a form is built and returned for the user to fill in. Then the user fills the form in and submits it, triggering a second page request in which the form must be built and processed. By default, $form and $form_state are built from scratch during each of these page requests. In some special use-cases, it is necessary or desired to persist the $form and $form_state variables from the initial page request to the one that processes the submission. A form builder function can set 'cache' to TRUE to do this. One example where this is needed is to handle AJAX submissions, so ajax_process_form() sets this for all forms that include an element with a #ajax property. (In AJAX, the handler has no way to build the form itself, so must rely on the cached version created on each page load, so it's a classic example of this use case.) Note that the persistence of $form and $form_state across successive submissions of a multi-step form happens automatically regardless of the value for 'cache'. You probably won't need to use $form_state['cache']. And note that $form['#cache'] is gone in D7 and now has no effect on anything.
  • $form_state['input']: The array of values as they were submitted by the user. These are raw and unvalidated, so should not be used without a thorough understanding of security implications. In almost all cases, code should use the data in the 'values' array exclusively. The most common use of this key is for multi-step forms that need to clear some of the user input when setting 'rebuild'.

Drupal 7 FAPI Resources:

Next time: Part 2: AJAX forms changes.

18 comments

by sun on Wed, 2010-06-16 13:20

Careful, hold on - some further critical/major patches are not in yet.

Bare essentials won't change, of course.

by Barry Jaspan on Wed, 2010-06-16 21:38

I started looking at how to port macro.module to D7. I am concerned that the new $form_state structure makes it difficult or impossible to do in general. Questions so far:

  1. When saving a form submission as a macro to replay later, which parts of $form_state need to be saved to be passed to drupal_form_submit() later? They cannot all be saved, because some parts (e.g. $form_state['groups'] which I don't understand yet) contain self-referential loops.

  2. How can a module completely override the form's submission handler (e.g. like in D5/6 could you do by changing $form['#submit'])? Every button has its own submit hook. Is there a way to know which one is the "real" submit/save/do button? Obviously for some forms that question isn't valid, but for many it is. On the node form, for example, how should macro.module know that $form['actions']['submit'] is the correct button to act on?

Probably this comment thread isn't the right place for this discussion. Feel free to email me. :-)

by rfay on Wed, 2010-06-16 21:48
  1. If there is a $form_state['groups'] then it was because your form created it; it's not a standard FAPI item. It will be automatically persisted, so you don't have to do anything about saving it.

  2. '#submit' still works fine both at the form and the button level; I don't know of any change in how it works. See http://api.drupal.org/api/drupal/developer--topics--forms_api_reference....

You can tell which button submitted the form by looking at $form_state['triggering_element']. Often $form_state['triggering_element']['#value'] is useful to figure this out.

Edit: The Examples for Developers project has a Form Example that is pretty up-to-date for D7, and it may help. I'll edit that into the article.

by Barry Jaspan on Wed, 2010-06-16 22:12
  1. You have to save $form_state if you want to replay the form submission later, long after it is persisted, possibly on a different site. This is macro.module's use case.

  2. submit works, but by emptying it you are not removing the form's actual submit handler, which is attached to a button. Which button? You can find out from $form_state['triggering_element'], but only after the form is submitted. There is no way to know which button is the "actual" submit button ahead of time, which means you can't replace the form's submit handler unless you have special-case knowledge. This is probably an unavoidable consequence of the fact that forms can now have multiple submit buttons that do all kinds of different things. The extra capability makes some use cases much more complicated or impossible.

by effulgentsia on Thu, 2010-06-17 11:55

Hi Barry. Regarding #1, for most single-step forms, you only need to include 'values' within $form_state when calling drupal_form_submit(). Note, I think the name 'values' is a little unfortunate here, as it should be called 'input', and if you look at drupal_form_submit(), you'll see that it gets moved to 'input' (and outside of drupal_form_submit(), 'values' always means the FAPI-processed values rather than user-submitted input, and is therefore, initialized empty at the beginning of drupal_process_form()).

Back to your question though, if you're having macro.module replay the submission of a multi-step form, then you're correct that you'll need to deal with $form_state saving yourself, since drupal_form_submit() bypasses the form cache. You'll need to emulate form_set_cache(), which saves all of $form_state except the keys in form_state_keys_no_cache().

Edit by rfay: Updated to drupal_form_submit() per effulgentsia's comment

by effulgentsia on Thu, 2010-06-17 12:20

Regarding #2, I don't think there's any change in D7 with respect to D6 in this regard. Most forms still use $form['#submit'] (usually implicitly by naming the function FORM_ID_submit()) as the "primary" submit handler and leave no #submit defined on the "primary" button, so that it uses the form-level #submit. Button-level #submit is usually only placed on the auxiliary buttons (see for example, comment_form()). node_form() is an exception to this, in that it uses a button-level #submit even for the "Save" button, but this is also the case in D6. Note that in http://drupal.org/node/735800#comment-3019950, fago is arguing that we might want to do more of this (not in D7 core, but possibly in D8 or in D7 contrib), so if you want to put a stop to that, you might want to contact him to discuss how that would adversely affect what you're working on.

You're correct that there's no clean way currently to override all #submit handlers (both form-level and button-level). The overlay module came up with the approach of overlay_element_info_alter() and overlay_form_after_build(). fago is suggesting in http://drupal.org/node/763376#comment-2918114 that we move the last bit of form_builder() that does the finalization stuff for #type='form' into a form #after_build function. That would allow code to implement a form #after_build function that runs after that one that could alter $form_state['submit_handlers']. I guess we'll see if we manage to get that implemented for D7.

by blainelang on Sat, 2010-07-31 08:13

Thanks Ray, your post has been helpful as well the D7 examples module but I've not been able to get the redirect option to work.

Using D7 Alpha 6: The submit handler defined in my module's HOOK_form_alter is being picked up, but NOT being used. In all cases, the default drupal node_form_submit() is being used.

Take any custom module and try setting a new redirect for all forms as a test and for this example, redirect to the watchdog event log.

in HOOK_form_alter()

$form['submit'][] = 'mymodule_submithandler';

function mymodule_content_form_submit($form, &$form_state) {
  $form_state['redirect'] = 'admin/reports/dblog';
}

I believe that now, when editing any site content and submitting the edit form, the site should redirect to the event log page but does not. If I edit function node_form_submit() in node.pages.inc and comment out the lines that set $form_state['redirect'], if $node->nid then the redirect works as expected. I've tried setting my modules weight to 1001 in the systems table but no success.

Any ideas or is this a D7 bug?

by rfay on Sat, 2010-07-31 08:23

In your hook_form_alter(), you say

$form['submit'][]

I assume you were actually doing

$form['#submit'][]

by blainelang on Sat, 2010-07-31 08:32

Sorry - yes it's really:
$form['#submit'][] = 'mymodule_content_form_submit';

That now matches the name of the mymodule submit handler in this example. Issue was me changing names of functions for this post. I can confirm that using an IDE Debugger, the submit handler is executing and the redirect works if I comment out that line in the D7 function node_form_submit() - which I was doing only for testing/confirmation.

by rfay on Sat, 2010-07-31 08:43

Hi - Please try a simple form with $form_state['redirect'] - I think you'll find that it works ok in that context.

Then try hook_form_alter() on that simple form, adding a #submit which changes $form_state['redirect']. I suspect that will work as well.

Then, it sounds like you have either a misunderstanding in the particular form_alter you're doing (against a node form, right) or you have found a core bug, probably in the node form stuff. But it could be the former.

I'll be around later today in irc (rfay) if you want to pastebin your code and would be willing to take a look.

by blainelang on Sat, 2010-07-31 14:57

The module that I'm working on will need to alter existing content types - where if the user is editing them (using the overlay) from with the module's page (call it a dashboard view), then we want to return the user to that dashboard. This is all working fine except for the redirect. The redirect would appear to be registered correctly because if I comment out the line in node_form_submit() then the redirect registered by mymodule takes effect.

I received help from DamZ on IRC and was recommended that I use this syntax in HOOK_form_alter and it worked. No other code changes where needed.

$form['actions']['submit']['#submit'][] = 'mymodule_content_form_submit'

I was not able to find much info on this syntax and am still wondering why the original method was not working and if there is a D7 issue that needs to be logged.

by rfay on Sat, 2010-07-31 15:14

In this you're doing what one would probably do more often: Add a custom submit handler to a specific button (the button named 'submit').

Before you were changing the submit handler for the form.

by blainelang on Sat, 2010-07-31 15:31

Thanks Randy for the clarification and I'm pleased this technique is working but I really only want to change the redirect and let the normal submit handlers complete and do their updates and logic processing.

This is why I was using the first technique and what your post and other D7 FAPI docs mention but it no worky for me.

I am hoping that this technique which is now redirecting is not going to add a new limitation. The submit handler that is being mapped in mymodule_form_alter is just changing the value of $form_state['redirect'] as my previous notes mentioned.

So far my testing is positive and content updates are saving and mymodule redirect triggering when it's supposed to.

by aac on Thu, 2011-09-15 13:14

Could you please elaborate more on the use of $form_state['storage'] in multi-step forms. Thanks in advance for any help!!

by rfay on Thu, 2011-09-15 13:43

In Drupal 7, $form_state['storage'] has no actual direct support. It used to be an official place to store inter-submit data, but now nearly everything is persistent in $form_state. So it's better to use $form_state['mymodulename_somestash']. The idea is that in a multi-step form, you can store data that might be beyond what's in $form_state['values'] and help your form to operate. You may get some help from the Form Example in the Examples Product, which has some multistep examples that show this, if I remember right.

by Pangus on Tue, 2011-12-27 19:47

Hello Randy,

I wonder if you could help me with a Drupal 7 ajax form problem I'm having. It's obviously a common problem. I've found many workarounds and some discussion on what to do to fix it for good in future Drupals but none of it has worked for me. I'm hoping you might have a good fix for it. Anyway, here it is...

I have many submit buttons on an ajax form and I want them all to say 'Process'. Therefore I set the #value to be 'process' in all of them. This however, confuses drupal. In my triggered function, the triggered_element always appears to be the last button. I cannot determine which button was clicked.

Any suggestions greatly appreciated.

by rfay on Tue, 2011-12-27 19:54

I guess I'm not familiar with that problem; it would be great if you'd post links to the discussions you've encountered so that others don't have to repeat the process.

I think you'd be best to post in http://drupal.stackexchange.com asking for help - please post a link to your question here.

It does confuse me why you'd want multiple buttons with the same name in the same form.

If your problem is multiple forms that have the same ID (and all have submit buttons) then you have to use hook_forms() to handle this case, sadly. Amazon Store module has an example of doing just that.

by Pangus on Wed, 2011-12-28 10:28

Thanks Randy.

Why do I want buttons with the same name? I'm not sure that's exactly what I want. What I want is buttons that have the same text written on them; 'Process'. The only way I know to set this is by setting the #value property. Setting the #name property as suggested by some stops me from getting a callback on some of my buttons, probably as they are all supposed to have #name as 'op' ( See stackexchange link )

Here are the links to others with similar problems. I can't find the one with discussion about changes for future Drupals right now but I'll post that too if I come across it again.

http://stackoverflow.com/questions/6520775/why-is-the-the-triggering-ele...

http://drupal.org/node/274983

http://drupal.stackexchange.com/questions/3633/ajax-form-that-changes-a-...

I've just been reading through all this again in the light of a new day and I think there's a few more avenues I can explore. If I can't find a solution, I'll post a question on http://drupal.stackexchange.com and link you in. If I fix it, I'll post what worked for me.

Thanks.

Drupal theme by Kiwi Themes.