Modify and Delete Record Operations - Drupal 7 Multi-Step Form (FAPI)

This post is part of a series. We discuss how to build a custom module using Drupal 7's API.

In our previous posts "Drupal 7 Multi-Step Form (FAPI) with Table Report" and  "Making Our Report Sortable and Paged - Drupal 7 Multi-Step Form (FAPI)", we created a multi-step form that allows users to add new records. We display a synchronized (dependent) table report which is both sortable and page-able.

In this post, we add  "Modify" and "Delete" record operations. The following screen-shots display our new form and report.

new form layout

In order to accommodate the new record operations, we add a field set. The field set notifies the user which record operation is currently selected ("Add" is selected by default). The field set acts as a container for the record operation form. Our module dynamically changes the field set and form elements when users select a record operation (E.G. Add, Modify or Delete).

The following screen-shots display the new "Modify" and "Delete" form elements.

 new delete

The "Modify" operation works the same way as the "Add" operation. Both operations are divided into 2 steps (AKA multi-step).

Our module now supports three record operations. Thus our module requires additional source code instructions. It's a good practice to keep your internal design (source code structure) modular. Keep the size of individual methods as small as possible. Well organized source code allows the developer to identify logic flow. Well organized code supports changeability. a measurement of how easy is your code to change.

In that spirit, we divide the form definition (hook_form implementation) into helper methods.

function multi_step_form($form, &$form_state, $operation = 'add', $folk_id = NULL) {
  $form['record_operation'] = array(
      '#type' => 'hidden',
      '#value' => $operation,
  $form['report'] = array(
      '#type' => 'markup',
      '#markup' => drupal_render(folks_db_report()),
      '#weight' => 10,
  if ($operation == 'modify' || $operation == 'delete') {
    if (!isset($form_state['input']['op'])) {
      $db = new DbFolk();
      $folk = $db->get($folk_id);
      $form_state['storage']['first_name'] = $folk->getFirst_name();
      $form_state['storage']['last_name'] = $folk->getLast_name();
      $form_state['storage']['color'] = $folk->getColor();
    $form['id'] = array(
        '#type' => 'hidden',
        '#value' => $folk_id,
  switch ($operation) {
    case 'add' :
      $form['record_fieldset'] = array(
          '#type' => 'fieldset',
          '#weight' => 0,
          '#title' => t('Add New Person'),
          '#description' => t('Add a person by
          performing 2 steps. 1) Add the first and last name. 2) Choose the new
          person\'s favorite color.'),
      $submit_value = 'Submit - Complete Form';
      $form = _populate_form_steps($form, $form_state, $submit_value);
    case 'modify' :
      $form['record_fieldset'] = array(
          '#type' => 'fieldset',
          '#weight' => 0,
          '#title' => t('Edit Record'),
      $submit_value = t('Submit Change');
      $form = _populate_form_steps($form, $form_state, $submit_value);
    case 'delete' :
      $form['record_fieldset'] = array(
          '#type' => 'fieldset',
          '#weight' => 0,
          '#title' => t('Delete Record'),
      $submit_value = t('Confirm Delete');
      $additonal_elements = _add_delete_op_elements($form_state, $operation, $folk_id, $submit_value);
      $form = array_merge_recursive($additonal_elements, $form);
  return $form;

We add 2 new parameters to our method "$operation" and "$folk_id". Those values are delivered through our hook_menu() implementation (via wild card arguments).

By default Drupal does not create a separate path (i.e. action form attribute value) for it's forms. Therefore our form definition method (multi_step_form()) is called twice:

  • When the user requests the form for display.
  • When the user submits the form back to the server (Drupal).

We use $form_state['input']['op'] to determine whether the user is requesting or submitting the form. If the user is submitting the form, we skip database call (I.E. second call, we already have the data).

We apply the same logic for the record id ($form_id). The record id is never changed by the user. Therefore we use the hidden input element to persist the value across the form processing phases.

The switch statement directs the code to populate the appropriate elements for either a "Add". "Modify"  or "Delete" operation.

The "Delete" operation requires a different set of form elements (I.E. it is not a multi-step process). Thus we use the php array_merge_recursive() method to add the Delete operation to our form. Hence the statements:

$additonal_elements = _add_delete_op_elements($form_state, $operation, $folk_id, $submit_value);  
$form = array_merge_recursive($additonal_elements, $form);

The following helper methods manage preparation details for elements associated with each operation and step:

  • _populate_form_steps() -- Prepares elements for the Add or Modify record operation. It calls the following methods depending on which "Step" th form is on.
    • _handler_step_0()  -- Prepares elements for the fist step (Add or Modify).
    • _handler_step_1()  -- Prepares elements for the second step (Add or Modify).

Just like the form definition phase. we've divided the form submission phase into helper methods:

  • multi_step_form_submit() -- Determines whether we are processing a multi-step operation (Add or Modify) or the Delete operation.
    • _process_multi_step_submit() -- Processes the details for an Add or Modify submission.
    • _process_delete_operation_submit() -- Process the details for a Delete submission.

 Note! links to full source code listing are located at the end of this post.

Changes to our report.

The changes to our table report routine are relatively minor.

  • Add a "Modify" link to each row.
  • Add a "Delete" link to each row.
  • Add a "Operations" column to the header.

Per our previous posts "Drupal 7 Multi-Step Form (FAPI) with Table Report" and  "Making Our Report Sortable and Paged - Drupal 7 Multi-Step Form (FAPI)" our report logic is performed in a method called "folk_db_report()".

Here is our new table header:

 $header = array(
      'id' => array('data' => t('Id'), 'field' => 'u.id'),
      'first_name' => array('data' => t('First Name'), 'field' => 'u.first_name'),
      'last_name' => array('data' => t('Last Name'), 'field' => 'u.last_name'),
      'color' => array('data' => t('Favorite Color'), 'field' => 'u.color'),
      'operations' => array('data' => t('Operations'), 'colspan' => 2),


Here is our new row with the Modify and Delete links:

$rows[$folk->id] = array(
        'id' => toHtml('span', $folk->id),
        'first_name' => toHtml('span', $folk->first_name),
        'last_name' => toHtml('span', $folk->last_name),
        'color' => toHtml('span', $folk->color),
        'modify' => l('Modify', 'test/multi_step_form/modify/' . $folk->id, array('html' => TRUE)),
        'delete' => l('Delete', 'test/multi_step_form/delete/' . $folk->id, array('html' => TRUE)),

Changes to our database access routines and data class objects.

As a project progresses, adding more business requirements, it's often the case that you tweak your object model. Thus, our object model is updated.

We refactored the class names to comply with Drupal's naming conventions. We added a specialized exception handler.

We used the beta version of Netbeans 7.0 to perform the refactoring. Really nice feature. The IDE identifies all instances of where your class is used (across file listings). The IDE provides with a preview the changes (in a diff format). When you accept the changes, Netbeans renames all the associated use instances. IDE refactoring is very powerful tool that saves an enormous amount of programming time. We also used the refactoring tool to rename a few our class methods.

Like most modern IDE's, Netbeans is able to introspect object classes. The IDE uses the introspection to supply the programmer with coding "hints".

We like to use class constants to prevent spelling errors in our source. The IDE's code hints let you select your class constant (as opposed to typing in a database field name by hand). Netbeans for PHP is free and can be found Here. Again, most modern programming editors and IDE's provide code hints. We just wanted to provide Netbeans as an example of a tool we have tried and are happy with.


Full source code listings:


Drupal's API lets developers build a powerful custom functions and a rich user experience, with a small amount of code.

The starting pointing for developers is to refer to the reference implementations provided by the official Examples for Developers. The official Drupal API documentation is located here. Last, the most import reference is the Drupal core source code itself. The core source code is loaded with fantastic documentation, making it easy to follow and thus provides a great reference.