Yesterday I posted that I have started working on a new form API for Drupal. Today it's time for an explanation of what I did and why I chose to do it that way.
First of all, the code is still incomplete. If you enable all modules, you can go to the URL path
/fubar to see a simple rendered form. However, after you submit the form nothing will happen. There are some validation and POST/GET data retrieval functions, but they are not used yet.
The code is based on objects. Form API introduced renderable (form) elements and in Drupal 7 this has been taken further (there are renderable elements for pages, hyperlinks and more). A lot of people complained about the renderable structure being one big array, so what I did was convert renderable elements to objects and remove the entire renderable functionality out of form API into a new system. This is one of the basic steps that needs to be taken (and I believe we should move all the renderable element code from theme.inc to this new system as well once this gets into Drupal 8). The renderable element system will replace hook_element_info(), drupal_render(), etc.
So, basically, every form element is a subclass of
elementstng_Element. You'll notice I prefixed every class with
elementstng_, to make a find and action as easy as possible in case this gets into core. Those prefixes just need to be removed when we get to that point, so you already know how the Drupal core functions and classes will be named now.
In fapitng.elements.inc you can see all the form element classes. There are a couple of extra base classes for shared functionality between different form elements (like
fapitng_FormInput for elements that accept input). If you think some properties are missing, check out
If you want you work with forms, you will probably want to start in fapitng.module, where you will find a large
@file comment about the API's architecture which I will summarise here. There are seven stages divided in three groups for handling forms:
These stages correspond to methods of
fapitng_Form or of its parent classes. In the current form API there is less separation between the different stages and it's not always clear what they do, that's why I created so many clearly defined stages in my code.
If you want to handle a form, you can either use the procedural functions
fapitng_form(). The first one creates a new form and handles all construction stages, so you get the entire form structure as it is being used, apart from possible changes made in pre render callbacks.
fapitng_form() is a wrapper function used for displaying a form in the interface.
When building a new form, you'll need to provide two things: a form ID and an array of build callbacks. The form ID is only used for internal identification, and values for element's name attributes are derived from it as well. This eliminates the need for hook_forms(). If the form is being built, the build callbacks are called one by one and the form object is passed on to it. Each callback can then build the form it sees fit. Using this approach, no form will ever have to call system_settings_form() it its own build function again, allowing developers to make smaller, more specific build functions that can be combined when actually building the form. This makes the functions more reusable. Validate and submit callbacks are automatically derived from all of the build callbacks.
Form validation has undergone a major change, based on my discussions with Jeff Eaton at DrupalCon DC last year. There are no form API specific validators anymore, except for the form itsellf. For elements we use validators that accept a value and possible arguments and return a boolean. This way validators are usable throughout the entire system and not only for forms.
$element->validate_callbacks['drupal_valid_token'] = array(
'message' => t('This form is outdated. Reload the page and try again.'),
'arguments' => array($element->form->id),
fapitng_FormToken. It adds the validator drupal_valid_token() to the list of validate callbacks for token elements. The element value is always passed on as the first argument to the callback, followed by possible extra arguments (
$element->form->idin this case). If the callback returns
FALSE, the element hasn't passed validation and the specified message is added to
$form->validis set to
FALSEas well as soon as one of the form's elements hasn't passed validation. When rendering the form again, the messages can be displayed using drupal_set_message(). This allows for a more flexible validation process, while minimising the amount of validators. It also doesn't pollute the interface if a form is handled programmatically, but it still needs to be validated.
This explanation should get you going with the code I wrote. I hope the code is self explanatory enough for you to understand. I still need to write a lot of code comments for functions and methods. Please forgive me for not having done that already. It's mostly because I didn't feel like writing lots of documentation for code that will probably change a lot in the near future anyway. THOUGHTS.txt contains todo's, things that have already been converted from the current API to mine and things that still need to be thought through.
I'm available for comments at @BartFeenstra.
I hate Drupal's form API. I love it, but I hate it as well. At times, it allows you to do amazing things with your forms, all from the safe surroundings of PHP, which you have grown to hate and love just as much. At other times you sit behind your desk, cursing and abusing your keyboard, because the one property you really need isn't available for the element you are using, or because form.inc is such an incomprehensible pile of junk.
Within the Drupal community, the usual response when someone criticises how Drupal works–or how it doesn't– is the infamous "File a patch!", which people hate and love as well (developers love it, users hate it). I decided to ignore this comment. Not because I'm a developer-hating user (I'm a user-hating developer). Not because I'm an arrogant prick (I am, but just not now), but because a single patch just wouldn't do.
I made a first stab at what I hope will become Drupal 8's new form API. It uses OOP, has better documentation and it's far more flexible. Get it now and tell me why it sucks, and how it should work instead. Then realise that you already know my answer...