Thứ Sáu, 30 tháng 5, 2014

Specifying Validation and Submission Functions with hook_forms() [The Form API]

Sometimes, you have a special case where you want  to have many different forms but only a single validation or
submit function. This is called  code reuse, and  it’s a good idea in that kind of a situation. The node module, for
example, runs all kind of nodetypes  through its validation and  submission functions. So we need a way to map multipleform IDs to validation and  submission functions. Enter hook_forms().

When  Drupal is retrieving the form, it first looks for a function that defines the form based on the form ID (in our
code, we used the formexample_nameform() function for this purpose). If it doesn’t finthat function, it invokes hook_forms(), which queries all modules for a mapping of form IDs to callbacks. For example, node.module uses the
following code  to map all different kinds  of node form IDs to one handler:

/**
*   Implements hook_forms(). All  node forms  share  the  same form handler.
*/
function node_forms()  {
$forms  = array();
if ($types = node_get_types()) {
foreach   (array_keys($types) as  $type) {
$forms[$type   .'_node_form']['callback'] = 'node_form';
}
}
return  $forms;}

In our form example, we could implement hook_forms() to map another form ID to our existing code.
/**
*   Implements hook_forms().
*/
function formexample_forms($form_id,   $args) {
$forms['formexample_special'] = array( 'callback' => 'formexample_nameform');
return  $forms;}

Now, if we call drupal_get_form('formexample_special'), Drupal will first check for a function named formexample_special() that defines the form. If it cannot find this function, hook_forms() will be called,  and  Drupal will see that we have mappedthe form ID formexample_special to formexample_nameform. Drupal will call formexample_
nameform() to get the form definition, and  then attempt to call formexample_special_validate() and  formexample_special_submit() for validation and submission, respectively.

Call Order of Theme, Validation, and  Submission Functions

As you’ve seen, there are several places to give Drupal information about where your theme, validation, and  submission
functions are. Having  so many options can be confusing, so here’s  a summary of where Drupal looks, in order, for a
theme function, assumingyou are using  a theme named mytheme, and you’re calling drupal_get_form('formexample_
nameform'). This is, however, dependent upon your hook_theme() implementation.
First, if $form['#theme'] has been set to foo in the form definition then the order of checks that Drupal performs is as follows:
1.    themes/mytheme/foo.tpl.php // Template file provided  by theme.
2.    formexample/foo.tpl.php // Template file provided  by module.
3.    mytheme_foo()  // Function  provided  theme.
4.    phptemplate_foo() // Theme  function provided  by theme engine.
5.    theme_foo() // 'theme_' plus  the  value  of  $form['#theme'].

However, if $form['#theme'] has not been set in the form definition then the order is:
1.    themes/mytheme/formexample-nameform.tpl.php  // Template provided  by theme.
2.    formexample/formexample-nameform.tpl.php  // Template file provided  by module.
3.    mytheme_formexample_nameform()  // Theme  function provided  by theme.
4.    phptemplate_formexample_nameform() // Theme  function provided  by theme engine.
5.    theme_formexample_nameform()  // 'theme_' plus  the  form ID.
During form validation, a validator for the form is set in this order:
1.     function defined by $form['#validate']
2.    formexample_nameform_validate  // Form  ID plus  'validate'.

And when it’s tim to look for a function to handle form submittal, Drupal looks for the following:
1.     function defined by $form['#submit']
2.    formexample_nameform_submit  // Form  ID plus  'submit'.

Remember that forms can have multiple validation and  submission functions.

Writing a Validation Function

Drupal has a built in mechanism for highlighting form elements that fail validation and displaying an error message to the
user. Examine the validation function in our example to see it at work:
/**
*   Validate the  form.
*/
function formexample_nameform_validate($form,  &$form_state)  { if ($form_state['values'
['user_name'] == 'King  Kong')  {
// We  notify the  form API that   this field has  failed validation.
form_set_error('user_name',t('King  Kong is not  allowed  to  use  this form.'));}}

Note the use of form_set_error(). When  King Kong visits our form and  types  in his name on his giant  gorilla keyboard, he
sees an error message at the top of the page,  and  the field that contains the error has its contents highlighted in red, as shown in Figur 11-6.


Figure 11-6. Validation failures are indicated to the user.

 Perhaps he should have used his given name, Kong, instead. Anyway, the point is that form_set_error() files an error against our
form and  will cause validation to fail. Validation functions should do just that—validate. They should not, as a general rule,
change data. However, they may add information to the $form_state array, as shown in the next section. If your validation
function does  a lot of processing and  you want  to store the result to be used in your submit function, you have
two different options. You could use form_set_value() or $form_state.

Using form_set_value() to Pass Data

The most formal option is to create a form element to stash the data when you create your form in your form definition function,
and  then use form_set_value() to store the data. First, you create a placeholder form element:

$form['my_placeholder']  = array( '#type'  => 'value','#value'  => array());

Then, during your validation routine, you store the data:
// Lots  of  work  here  to  generate  $my_data  as part  of  validation.
...
// Now  save our work.
form_set_value($form['my_placeholder'],  $my_data,  &$form_state);
And you can then access the data in your submit function:
// Instead  of  repeating  the  work  we  did  in  the  validation  function,
// we  can just  use  the  data  that  we  stored.
$my_data  = $form_state['values']['my_placeholder'];

Or suppose you need to transform data to a standard representation. For example, you have a list of country codes in the database
that you will validate against, but your unreasonable boss insists that users be able to type their  country names in text fields.You
would need to create a placeholder in your form and  validate the users input using  a variety  of trickery so you can recognize both
The Netherlands” and  Nederland” as mapping to the ISO 3166 country code  NL.

$form['country']  = array( '#title'  => t('Country'), '#type'  => 'textfield','#description'  => t('Enter  your country.'));
// Create  a  placeholder.  Will  be filled  in  during  validation.
$form['country_code']  = array( '#type'  => 'value','#value'  => '');

Inside the validation function, you’d save the country cod inside the placeholder.

// Find out  if we have  a  match.
$country_code  = formexample_find_country_code($form_state['values']['country']); if ($country_code) {
// Found one.  Save  it so  that   the  submit  handler  can  see  it.
form_set_value($form['country_code'],  $country_code, &$form_state);
}else {
form_set_error('country',  t('Your country  was not  recognized.  Please  use a  standard  name or  country  code.'));}
Now, the submit handler can access the country code  in $form_values['country_code'].

Using $form_state to Pass Data

A simpler approach is to use $form_state to store the value. Since $form_state is passed to both validation and  submission
functions by reference, validation functions can store data there for submission functions to see. It is a good idea to use your 
module’s namespace within $form_state instead of just making up a key.

// Lots  of  work here  to  generate $weather_data  from slow  web service
// as  part  of  validation.
...
// Now  save  our  work in  $form_state.
$form_state['mymodulename']['weather'] = $weather_data
And you can then access the data in your submit function:
// Instead of  repeating the  work we did  in  the  validation function,
// we can  just use  the  data  that   we stored.
$weather_data  = $form_state['mymodulename']['weather'];

You may be asking, “Why not store the value in $form_state['values'] along  with the rest of the form field values?” That will
work too, but keep in mind that $form_state['values'] is the place  for form field values, not random data stored by modules.
Remember that because Drupal allows any module to attach validation and  submission functions to any form,  you cannot make the
as sumption that your module will be the only one working with the form state, and  thus data should be stored in a consistent and
predictable way.

Element-Specific Validation

Typically,  one validation function is used for a form. But it is possible to set validators for individual form elements as well as for
the entire form. To do that, set the #element_validate property for the element to an array containing the names of thevalidation
functions. A full copy of the element’s branch of the form data structure will be sent  as the first parameter. Here’s a contrived
example where we force the user to enter spicy or sweet  into a text field:

// Store  the  allowed  choices in  the  form definition.
$allowed_flavors = array(t('spicy'), t('sweet'));
$form['flavor'] = array( '#type' => 'textfield', '#title' => 'flavor','#allowed_flavors' => $allowed_flavors,
'#element_validate' => array('formexample_flavor_validate'));

Then  your element validation function would look like this:
function  formexample_flavor_validate($element, $form_state) {
if (!in_array($form_state['values']['flavor'], $element['#allowed_flavors'])) { form_error($element, t('You   must enter   spicy or  sweet.'));}}
The validation function for the form will still be called  after all element validation functions have been called.

Form  Rebuilding

During validation, you may decide that you do not have enough information from the user.  For example, you might run the form value through a textual analysis engine and  determine that there is a high probability that this content is spam. As a result, you want todisplay the form again  (complete with the values  the user  entered) but add a CAPTCHA to disprove your suspicion that this user is a robot. You can signal to Drupal that a rebuild is needed by setting $form_state['rebuild'] inside your validation function,like so:

$spam_score  = spamservice($form_state['values']['my_textarea']; if ($spam_score  > 70)  {
$form_state['rebuild'] = TRUE;
$form_state['formexample']['spam_score'] = $spam_score;}
In your form definition function, you would have something like this:

function formexample_nameform($form_state)  {
// Normal form definition  happens.
...
if (isset($form_state['formexample']['spam_score']) {
// If this is set, we are  rebuilding the  form;
// add the  captcha  form element  to  the  form.
...
}
...
}

Writing a Submit Function

The submit function is the function that takes care of actual form processing after the form has been validated. It executes only if
form validation passed completely and  the form has not been flagged for rebuilding. The submit function is expected to modify
$form_state['redirect'].

function formexample_form_submit($form,  &$form_state)  {
// Do  some stuff.
...
// Now  send  user  to  node number  3.
$form_state['redirect'] = 'node/3';}

If you have multiple functionhandling form submittal (see the “Submitting the Form”  section earlier in this chapter), the last
function to set $form_state['redirect'] will have the last word.

Changing Forms with hook_form_alter()

Using hook_form_alter(), you can change any form. All you need to know is the form’s ID. There are two approaches to alteringforms.

Altering Any Form

Let’s change the login form that is shown on the user login block and  the user login page.

function formexample_form_alter(&$form,  &$form_state,  $form_id)  {
// This  code  gets called for  every  form Drupal builds; use  an if statement
// to  respond  only  to  the  user  login block  and user  login  forms.
if ($form_id  == 'user_login_block' || $form_id  == 'user_login') {
// Add  a  dire   warning to  the  top  of  the  login  form.
$form['warning'] = array('#markup' => t('We  log  all  login attempts!'), '#weight' => -5);
// Change 'Log  in' to  'Sign in'.
$form['submit']['#value'] = t('Sign in');}}

Since $form is passed by reference, we have complete access to the form definition here  and  can make any changes we want. In
the example, we added some text using the default form element (see “Markup” later in this chapter) and  then reached in and
changed the value of the Submit button.

Altering a Specific Form

The previous approach works, but if lots of modules are altering forms  and  every form is passed to every hook_form_alter()
implementation, alarm bells may be going off in your head. “This is wasteful,” youre probably thinking. “Why not just construct
afunction from the form ID and  call that?” You are on the right track.  Drupal does exactly that. So the following function will
change the user login form too:

function   formexample_form_user_login_alter(&$form, &$form_state)  {
$form['warning'] = array('#value' => t('We  log  all  login attempts!'), '#weight' => -5);
// Change 'Log  in' to  'Sign in'.
$form['submit']['#value'] = t('Sign in');}
The function name is constructed from this:
modulename + 'form' + form  ID + 'alter'
For example,
'formexample'  + 'form' + 'user_login' + 'alter' results in the following: formexample_form_user_login_alter
In this particular case, the first form of hook_form_alter() is preferred, because two form IDs are involved (user_login for the
form at http://example.com/?q=user and  user_login_block for the form that appears in the user block).

Submitting Forms Programmatically with drupal_form_submit()

Any form that is displayed in a web browser can also be filled out programmatically. Let’s fill out our name and  favorite color
program matically:

$form_id  = 'formexample_nameform';
$form_state['values'] = array( 'user_name'   => t('Marvin'), 'favorite_color' => t('green'));
// Submit the  form using  these values.
drupal_form_submit($form_id, $form_state);

That’s all there is to it! Simply supply the form ID and  the values for the form, and  call
drupal_form_submit().

Dynamic Forms

We’ve been looking at simple one-page forms. But you may need to have users fill out a form that dynamically displays elements
on the form based on selections the user made as he or she filled out the form. The following example demonstrates how to
display form elements dynamically as the user picks various options while filling out the form.

Start by creating a directory in your site/all/modules/custom folder  named form_example_dynamicIn that directory, create a
form_example_dynamic.info file with the following information.
name = Form  Example –Creating  a  Dynamic Form description = An  example of  a  dynamic form. package  = Pro Drupal Development
core  = 7.x files[]=form_example_dynamic.module

Next create the form_example_dynamic.module file, and  begin by placing the following header information in the file.
<?php
/**
*   @file
*   An  example of  how to  use  the  new #states Form  API element, allowing
*   dynamic form behavior  with  very  simple  setup.
*/
With the header information in place, the next step  is to create a menu item  that a visitor can use to access the new form. The module provides a single menu entry that can be accessed via www.example.com/form_example_dynamic.
/**
*   Implements hook_menu().
*/
function form_example_dynamic_menu()  {
$items['form_example_dynamic'] = array( 'title' => t('Form  Example Dynamic Form'), 'page  callback' =>
'drupal_get_form','page  arguments'  => array('form_example_dynamic_form'), 'access callback' => TRUE,
'type' => MENU_NORMAL_ITEM);
return  $items;}
With the menu complete, I’m now ready  to create the form. The first item  displayed on the form is a series  of three radio buttons that allow a site visitor to select  a room type to reserve.
function form_example_dynamic_form($form, &$form_state)  {
$form['room_type'] = array( '#type' => 'radios',
'#options' => drupal_map_assoc(array(t('Study Room'),  t('Small Conference  Room'), t('Board Room'))),
'#title' => t('What  type  of  room do you require?'));

The next form item  is a fieldset that contains details about the study room and  uses the #states attribute to determine whether this item  should be displayed on the page.  The #states attribute sets whether the field set will be visible by examining the room_type
radio buttons to see whether the Study Room option was selected. If the Study Room option was selected, then the value is set to true and  the form will render the fieldset using jQuery.  The syntax  of the visibility test follows the syntax  of using selectors in
jQuery. In this case, we’re looking at an input element (the radio buttons) named room_type. We’re examining whether the value
of the input is Study Room.

$form['study_room'] = array( '#type' => 'fieldset','#title' => t('Study Room  Details'), '#states' => array('visible' => array(
':input[name="room_type"]' => array('value' => t('Study  Room')),),),);

The next item  shown on the form is two chec boxes that allow a visitor to provide details about the types  of equipment to be setup in the study room. In the example, I’ve limited those choices to chairs and  a PC I use the same #states approach as the
preceding field set. I want  the check boxes displayed only if the visitor has selected Study Room from the list of available rooms.

$form['study_room']['equipment'] = array( '#type' => 'checkboxes', '#options' => drupal_map_assoc
(array(t('Chairs'), t('PC'))), '#title' => t('What  equipment do you need?'),'#states' => array('visible' => array( // action to take.':input[name="room_type"]' => array('value' => t('Study  Room')),),),);

If the user  checked the Chairs check  box, I’ll display a text field that allows the visitor to enter the number of chairs to be set up
in the room prio to his or her arrival.  I’m using #action to control visibility of this text field, displaying the field only if the user
checked the Chairs check box.

$form['study_room']['chairs'] = array( '#type' => 'textfield','#title' => t('How  Many  Chairs Do  You Need?:'), '#size' => 4,'#states' => array(
'visible' => array(     // action to  take.':input[name="equipment[Chairs]"]' => array('checked' => TRUE),),),);

The next element on the form is another text box that allows a visitor to enter details about the type of PC to be set up in the
study room. Like the foregoing chairs item, I’m using #action to control visibility by checking to see whether the visitorchecked the PC chec box.

$form['study_room']['pc'] = array( '#type' => 'textfield','#title' => t('What  Type of  PC  do you need?:'),'#size' => 15,
'#states' => array('visible' => array(     // action to  take.':input[name="equipment[PC]"]' => array('checked' => TRUE),),
),);

The next set of form elements is displayed only if the visitor clicked  the Small Conference Room” radio buttonIt follows the
same pattern of using  the #actions attribute to determine whether form items should be visible based on a condition or action taken
by the visitor.
$form['small_conference_room'] = array( '#type' => 'fieldset', '#title' => t('small_conference_room Information'), '#states' => array('visible' => array(':input[name="room_type"]' => array('value' => t('Small Conference  Room')),),),);
$form['small_conference_room']['how_many_pcs'] = array( '#type' => 'select', 
'#title' => t('How  many  PCs do you need  set up in  the  small  conference room?'), '#options' => array(1  => t('One'),
2  => t('Two'),3  => t('Three'),4  => t('Four'),5  => t('Lots'),),);
$form['small_conference_room']['comment'] = array( '#type' => 'item', '#description' => t("Wow, that's a  long  time."),
'#states' => array( 'visible' => array(':input[name="how_many_pcs"]'  => array('value' => '5'),),),);
$form['small_conference_room']['room_name'] = array( '#type' => 'textfield',
'#title' => t('Which  room do you want to  use?:'),);
$form['small_conference_room']['hours'] = array( '#type' => 'select',
'#options' => drupal_map_assoc(array(t('Free'), t('Paid'))),
'#title' => t('Do you want to  reserve the  room when it is free (no  fees) or  paid  (prime time)?'),);

The following form element utilizes two conditional checks to determine whether the text field should be displayed. With #action youcan simply  list out any number of conditions that must be met before the form item  will be displayed. In this case, Ichec to see whether the visitor selected either Free or Paid from the preceding hours field.

$form['small_conference_room']['hours_writein'] = array( '#type' => 'textfield','#size'  =>50,
'#title' => t('Please enter   the  date  and time  you would like to  reserve the  room and the duration.'),
'#states' => array('visible' => array(     // Action  to  take: Make  visible. ':input[name="hours"]' => array('value' =>
t('Free')), ':input[name="hours"]' => array('value' => t('Paid')),),),);

The reminder form item  here  introduces a new visibility check  by verifying that the visitor seleted either Free or Paid and  that heor she entered something in the hours_writein field.

$form['small_conference_room']['reminder'] = array( '#type' => 'item',
'#description' => t('Remember to  enter   the  date, start time, and end time.'), '#states' => array(
'visible' => array('input[name="hours"]' => array('value' => t('Free')), 'input[name="hours"]' => array('value' =>
t('Paid')), 'input[name="hours_writein"]' => array('filled' => TRUE),),),);
$form['board_room']   = array( '#type' => 'fieldset','#title' => t('Board Room  Information'), '#states' => array(
'visible' => array(':input[name="room_type"]' => array('value' => t('Board  Room')),),),);
$form['board_room']['more_info']  = array( '#type'  => 'textarea',
'#title'  => t('Please  enter  the  date  and time  of  when  you would  like  to  reserve the board room'),);

$form['board_room']['info_provide']  = array( '#type'  => 'checkbox',
'#title'  => t('Check  here  if you have provided  information  above'), '#disabled'  => TRUE,
'#states'  => array('checked'  => array(// Action  to  take:  check  the  checkbox.':input[name="more_info"]'  => array('filled'  => TRUE),),),);
$form['expand_more_info']  = array( '#type'  => 'checkbox','#title'  => t('Check  here  if you want  to  add special instructions.'),);
$form['more_info']  = array( '#type'  => 'fieldset','#title'  => t('Special  Instructions'), '#collapsible'  => TRUE,
'#collapsed'  => TRUE,'#states'  => array( 'expanded'  => array(':input[name="expand_more_info"]'  => array('checked'  => TRUE),),),);
$form['more_info']['feedback']  = array( '#type'  => 'textarea',
'#title'  => t('Please  provide  any additional  details  that  will  help  us better  serve you.'),);
$form['submit']  = array( '#type'  => 'submit','#value'  => t('Submit  your information'),);\
return  $form;
}
function  form_example_dynamic_form_submit($form,  &$form_state)  { drupal_set_message(t('Submitting  values: @values',  array('@values'  =>var_export($form_state['values'],  TRUE))));}

With the module complete, I’ll enable the module and  visit the form at www.example.com/form_example_dynamic. The first
page of the form should look like Figure  11-7.


Figure 11-7 The initial state of the form

Selecting Study Room from the list of options reveals the next part  of the form (see figure 11-8), which asks
the visitor about the type of equipment to be set up in the room before he or she arrives.


Figure 11-8. Study Room  Details fieldset is displayed based on the previous option selected.

Selecting the Small Conference Room option instead of Study Room displays the form elements related to the Small
Conference Room (see figure 11-9).


Figure 11-9. The Small Conference room  form elements are displayed after
selecting Small Conference room  from the room  types.
If the visitor selects the Board Room from the list of room types, the details shown in figure 11-10 ardisplayed.

Figure 11-10 The Board Room  elements are displayed after selecting Board Room  from the list of room types.




Không có nhận xét nào:

Đăng nhận xét