You are here

subform.module in Subform 7

Same filename and directory in other branches
  1. 5 subform.module

Defines a subform element type.

Basic usage example:

$form['my_node_subform'] = array(
  '#type' => 'subform',
  '#subform_id' => 'node_form',
  '#subform_file' => array(
    'module' => 'node',
    'name' => 'node.pages',
  ),
  '#subform_arguments' => array(
    $node,
  ),
  '#subform_default_triggering_element' => array(
    'actions',
    'submit',
  ),
);

Understanding the code ---------------------- Order of the functions in this file is an indication of the order of execution when processing subforms, while the function names indicate their context.

Note that depending on whether a form needs rebuilding, some functions can get called twice.

File

subform.module
View source
<?php

/**
 * @file
 * Defines a subform element type.
 *
 * Basic usage example:
 * @code
 *   $form['my_node_subform'] = array(
 *     '#type' => 'subform',
 *     '#subform_id' => 'node_form',
 *     '#subform_file' => array('module' => 'node', 'name' => 'node.pages'),
 *     '#subform_arguments' => array($node),
 *     '#subform_default_triggering_element' => array('actions', 'submit'),
 *   );
 * @endcode
 *
 * Understanding the code
 * ----------------------
 * Order of the functions in this file is an indication of the order of
 * execution when processing subforms, while the function names indicate their
 * context.
 *
 * Note that depending on whether a form needs rebuilding, some functions can
 * get called twice.
 */

/**
 * Implements hook_element_info().
 */
function subform_element_info() {
  $types['subform'] = array(
    '#input' => TRUE,
    // Actual value determined in subform_element_process()
    '#executes_submit_callback' => TRUE,
    '#required' => TRUE,
    '#limit_validation_errors' => FALSE,
    '#value_callback' => 'subform_element_value',
    '#process' => array(
      'subform_element_process',
    ),
    '#element_validate' => array(
      'subform_element_validate',
    ),
    '#submit' => array(
      'subform_element_submit',
    ),
    '#theme_wrappers' => array(
      'subform',
    ),
    '#subform_arguments' => array(),
    '#subform_file' => NULL,
  );
  return $types;
}

/**
 * Implements hook_theme().
 */
function subform_theme() {
  return array(
    'subform' => array(
      'render element' => 'element',
    ),
  );
}

/**
 * Implements hook_init().
 *
 * If the current request is a ajax form submission and contains the special
 * input key '_subform_element_name', the ajax path or callback expects the
 * form structure and state of the subform. If this is the case rewrite $_POST
 * and $_FILES so they contain the subform specific data only.
 *
 * During a ajax form submission only functions starting with "subform_form_"
 * are called; all "subform_element_" and all "subform_parent_" functions are
 * irrelevant.
 *
 * @see subform_form_child_pre_render()
 *   Contains the code that renames input elements inside subforms and allows
 *   for ajax form submissions.
 * @see subform_form_process()
 * @see subform_form_after_build()
 *   Functions that get called during a ajax form submission.
 */
function subform_init() {
  if (isset($_POST['_subform_element_name'])) {
    $subform_name = $_POST['_subform_element_name'];
    $wrapper_input = $_POST;
    $_POST = isset($_POST[$subform_name]) ? $_POST[$subform_name] : array();
    if (!empty($wrapper_input)) {
      foreach ($wrapper_input as $name => $value) {
        if (strpos($name, $subform_name . '-') === 0) {
          $_POST[$name] = $value;
        }
      }
    }

    //$_POST['_subform_wrapper_input'] = $wrapper_input;

    // This is hacky, but it works: _subform_element_name is supposed to
    // be the first non-form-input value.
    $non_form_input = FALSE;
    foreach ($wrapper_input as $key => $value) {
      if ($key == '_subform_element_name') {
        $non_form_input = TRUE;
      }
      elseif ($non_form_input) {
        $_POST[$key] = $value;
      }
    }
    if (!empty($_FILES['files'])) {
      foreach ($_FILES['files']['name'] as $old_name => $value) {
        if (strpos($old_name, $subform_name . '-') === 0) {

          // Remove the subform element name prefix.
          $new_name = substr($old_name, strlen($subform_name . '-'));
          foreach (array(
            'name',
            'type',
            'tmp_name',
            'error',
            'size',
          ) as $key) {
            $_FILES['files'][$key][$new_name] = $_FILES['files'][$key][$old_name];
            unset($_FILES['files'][$key][$old_name]);
          }
        }
      }
    }
  }
}

/**
 * Value callback for the subform element.
 */
function subform_element_value(&$element, $input, &$form_state) {
  $element['#name'] = $subform_name = (isset($form_state['subform_name']) ? $form_state['subform_name'] : 'subform') . '-' . implode('-', $element['#parents']);
  if (!isset($form_state['temporary']['subform'][$subform_name])) {
    $form_state['temporary']['subform'][$subform_name] = array();
  }
  $subform_state =& $form_state['temporary']['subform'][$subform_name];

  // Ensure some defaults; if already set they will not be overridden.
  $subform_state += form_state_defaults();
  $subform_state['subform_name'] = $subform_name;
  $subform_state['subform_element_parents'] = $element['#array_parents'];
  $subform_state['subform_parent'] = array(
    'form_id' => $form_state['complete form']['#form_id'],
    'build_id' => $form_state['complete form']['#build_id'],
  );
  $subform_state['temporary']['subform_parent_state'] =& $form_state;

  // Pass in subform arguments.
  if (!empty($element['#subform_arguments'])) {
    $subform_state['build_info']['args'] = $element['#subform_arguments'];
  }

  // Load subform files.
  if (!empty($element['#subform_file'])) {
    $file = $element['#subform_file'];
    if (is_array($file)) {
      $file += array(
        'type' => 'inc',
        'name' => $file['module'],
      );
      module_load_include($file['type'], $file['module'], $file['name']);
    }
    elseif (file_exists($file)) {
      require_once DRUPAL_ROOT . '/' . $file;
    }
    $subform_state['build_info']['files'][] = $element['#subform_file'];
  }

  // The subform inherits certain properties from the subform element.
  foreach (array(
    '#access',
    '#disabled',
    '#allow_focus',
    '#subform_default_triggering_element',
  ) as $property) {
    if (isset($element[$property]) && !isset($subform[$property])) {
      $subform_state['subform_properties'][$property] = $element[$property];
    }
  }
  if (isset($form_state['input'][$subform_name]) && is_array($form_state['input'][$subform_name])) {
    $input = $form_state['input'][$subform_name];
  }
  elseif (isset($element['#default_value']) && is_array($element['#default_value'])) {
    $input = $element['#default_value'];

    // Prevent default value from triggering form validation/submission.
    unset($input['form_id']);
  }

  // Include nested subform input.
  if (!empty($form_state['input'])) {
    foreach ($form_state['input'] as $name => $value) {
      if (strpos($name, $subform_name . '-') === 0) {
        $input[$name] = $value;
      }
    }
  }
  return $input;
}

/**
 * Processes a subform element.
 *
 * @see drupal_build_form()
 * @see drupal_process_form()
 *   Contains code that is duplicated here.
 */
function subform_element_process($element, &$form_state) {
  $form_id = $element['#subform_id'];
  $subform_name = $element['#name'];
  $subform_state =& subform_get_state($subform_name, $form_state);
  $subform_state['input'] = isset($element['#value']) && is_array($element['#value']) ? $element['#value'] : array();
  $element['#subform_errors'] = array();
  $element['#subform_error_messages'] = array();
  if ($subform_state['rebuild']) {
    $subform = subform_element_rebuild($element, $form_state);
  }
  else {
    subform_buffer('start', $element);

    // Copied from drupal_build_form()
    // Check the cache for a copy of the subform in question.
    $check_cache = isset($subform_state['input']['form_id']) && $subform_state['input']['form_id'] == $form_id && !empty($subform_state['input']['form_build_id']);
    if ($check_cache) {
      $subform = form_get_cache($subform_state['input']['form_build_id'], $subform_state);
    }

    // Build subform from scratch.
    if (!isset($subform)) {
      if ($check_cache) {
        $subform_state_before_retrieval = $subform_state;
      }
      $subform = drupal_retrieve_form($form_id, $subform_state);
      drupal_prepare_form($form_id, $subform, $subform_state);
      if ($check_cache) {
        $uncacheable_keys = array_flip(array_diff(form_state_keys_no_cache(), array(
          'always_process',
          'temporary',
        )));
        $subform_state = array_diff_key($subform_state, $uncacheable_keys);
        $subform_state += $subform_state_before_retrieval;
      }
    }

    // Copied from drupal_process_form()
    $subform_state['values'] = array();

    // With $_GET, these forms are always submitted if requested.
    if ($subform_state['method'] == 'get' && !empty($subform_state['always_process'])) {
      if (!isset($subform_state['input']['form_build_id'])) {
        $subform_state['input']['form_build_id'] = $subform['#build_id'];
      }
      if (!isset($subform_state['input']['form_id'])) {
        $subform_state['input']['form_id'] = $form_id;
      }
      if (!isset($subform_state['input']['form_token']) && isset($subform['#token'])) {
        $subform_state['input']['form_token'] = drupal_get_token($subform['#token']);
      }
    }

    // Retain the unprocessed $subform in case it needs to be cached.
    $subform_state['temporary']['subform_unprocessed'] = $subform;
    $subform = form_builder($form_id, $subform, $subform_state);

    // Display any errors thrown during form building.
    subform_buffer('end', $element, TRUE);
  }

  // If the subform contains a file element, update the parent form accordingly.
  if (isset($subform_state['has_file_element'])) {
    $form_state['has_file_element'] = TRUE;
  }

  // Subform elements are allowed to set #limit_validation_errors to TRUE, to
  // limit validation errors to errors within the subform only.
  if (isset($element['#limit_validation_errors']) && $element['#limit_validation_errors'] === TRUE) {
    $element['#limit_validation_errors'] = array(
      $element['#parents'],
    );
  }

  // If the subform contains a triggering element, set the subform element to be
  // the triggering element of the parent form.
  if (isset($subform_state['triggering_element']) && empty($subform_state['temporary']['subform_no_triggering_element'])) {
    $element['#executes_submit_callback'] = $subform_state['triggering_element']['#executes_submit_callback'];
    $form_state['triggering_element'] = $element;
  }
  $form_state['complete form']['#after_build']['subform_parent_after_build'] = 'subform_parent_after_build';
  $element['#subform'] = $subform;
  return $element;
}

/**
 * Constructs a new $subform from the information in $subform_state.
 *
 * @see drupal_rebuild_form()
 *   Contains code that is duplicated here.
 */
function subform_element_rebuild($element, &$form_state) {
  $subform_state =& subform_get_state($element['#name'], $form_state);
  if (isset($form_state['rebuild_info'])) {
    $subform_state['rebuild_info'] = $form_state['rebuild_info'];
  }
  $old_subform = isset($subform_state['temporary']['subform_unprocessed']) ? $subform_state['temporary']['subform_unprocessed'] : NULL;
  subform_buffer('start', $element);

  // TODO prevent caching in drupal_rebuild_form() as we are doing it later
  // in subform_parent_after_build().
  $element['#subform'] = drupal_rebuild_form($element['#subform_id'], $subform_state, $old_subform);
  subform_buffer('end', $element, TRUE);
  return $element;
}

/**
 * Implements hook_form_alter().
 */
function subform_form_alter(&$form, &$form_state, $form_id) {

  // Detect whether this is a form within a subform element.
  if (!empty($form_state['subform_name'])) {
    $form['#process']['subform_form_process'] = 'subform_form_process';
    $form['#after_build']['subform_form_after_build'] = 'subform_form_after_build';
  }
}

/**
 * Process callback for form elements within subform elements.
 *
 * @see subform_form_alter()
 *   Registers this callback.
 */
function subform_form_process($element, &$form_state) {

  // The subform inherits certain properties from the subform element.
  if (!empty($form_state['subform_properties'])) {
    foreach ($form_state['subform_properties'] as $property => $value) {
      $element[$property] = $value;
    }
  }
  return $element;
}

/**
 * After-build callback for form elements within subform elements.
 *
 * @see subform_form_alter()
 *   Registers this callback.
 */
function subform_form_after_build($element, &$form_state) {
  if (!isset($form_state['triggering_element'])) {

    // Indicate that no actual triggering element is set; form_builder() will
    // set the first button as the triggering element.
    $form_state['temporary']['subform_no_triggering_element'] = TRUE;

    // Allows to set an alternative default triggering element.
    if (!empty($element['#subform_default_triggering_element'])) {
      $triggering_element =& subform_array_get_nested_value($element, $element['#subform_default_triggering_element'], $triggering_element_exists);
      if ($triggering_element_exists) {
        $form_state['triggering_element'] = $triggering_element;
      }
    }
  }

  // Set a #pre_render callback on all elements within a subform.
  $element['#pre_render']['subform_form_pre_render'] = 'subform_form_pre_render';
  subform_form_after_build_traverse_children($form_state['subform_name'], $element);
  return $element;
}

/**
 * Helper function to set a #pre_render callback on all elements within a subform.
 */
function subform_form_after_build_traverse_children($subform_name, &$element) {
  foreach (element_children($element) as $key) {
    subform_form_after_build_traverse_children($subform_name, $element[$key]);
  }
  $element['#subform_name'] = $subform_name;
  $element['#pre_render']['subform_form_child_pre_render'] = 'subform_form_child_pre_render';
}

/**
 * After-build callback for form elements containing subform elements.
 *
 * @see subform_element_process()
 *   Registers this callback.
 */
function subform_parent_after_build(&$element, &$form_state) {

  // Subform elements may contain the first button in the whole form. If the
  // parent form contains no actual triggering element, set this subform to
  // be the triggering element.
  if (!empty($form_state['temporary']['subform']) && !isset($form_state['triggering_element'])) {
    foreach ($form_state['temporary']['subform'] as $subform_name => &$subform_state) {
      $subform_element =& subform_array_get_nested_value($element, $subform_state['subform_element_parents'], $subform_exists);
      if ($subform_exists && !empty($subform_element['#subform_has_first_button'])) {
        $form_state['triggering_element'] = $subform_element;
        break;
      }
    }
  }
  foreach ($form_state['temporary']['subform'] as $subform_name => &$subform_state) {

    // The triggering element of the parent form may set specific triggering
    // elements for its subforms using #subform_triggering_element which accepts
    // the following format:
    //
    // @code
    // array(
    //   $form_id => array($parents, $of, $subforms, $triggering_element),
    // );
    // @endcode
    if (isset($form_state['triggering_element']['#subform_triggering_element'][$subform_state['complete form']['#form_id']])) {
      $triggering_element =& subform_array_get_nested_value($subform_state['complete form'], $form_state['triggering_element']['#subform_triggering_element'][$subform_state['complete form']['#form_id']], $triggering_element_exists);
      if ($triggering_element_exists) {
        subform_set_triggering_element($triggering_element, $subform_state);
      }
    }

    // If the input is not going to be processed cache the subform here. Caching
    // the subforms here allows for making changes to subform states in any
    // process or after-build callback.
    if ((!$form_state['process_input'] || !$subform_state['process_input']) && empty($subform_state['programmed']) && $subform_state['cache'] && empty($subform_state['no_cache'])) {
      form_set_cache($subform_state['complete form']['#build_id'], $subform_state['temporary']['subform_unprocessed'], $subform_state);
    }
  }

  // While this is also done later in form_builder() we need the triggering
  // element here to set the after-validate/submit handlers correctly.
  if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) {
    $form_state['triggering_element'] = $form_state['buttons'][0];
  }

  // Add after-validate/submit handlers to this parent form.
  foreach (array(
    'validate',
    'submit',
  ) as $type) {
    if (isset($form_state['triggering_element']['#' . $type])) {
      $form_state['triggering_element']['#' . $type]['subform_parent_after_' . $type] = 'subform_parent_after_' . $type;
    }
    else {
      if (!isset($element['#' . $type])) {
        $element['#' . $type] = array();
      }
      $element['#' . $type]['subform_parent_after_' . $type] = 'subform_parent_after_' . $type;
    }
  }
  return $element;
}

/**
 * Validation handler for the subform element.
 *
 * It validates the subform element by validating the subform.
 *
 * @see drupal_process_form()
 *   Contains code that is duplicated here.
 */
function subform_element_validate(&$element, &$form_state) {
  $subform_state =& subform_get_state($element['#name'], $form_state);

  // Validate the subform if we have a correct form submission.
  if ($subform_state['process_input']) {
    subform_buffer('start', $element);

    // drupal_validate_form() expects a unique form id; use the subform name.
    drupal_validate_form($element['#name'], $element['#subform'], $subform_state);

    // Only display validation errors of the subform if the subform element is
    // required or if it is considered the triggering element.
    subform_buffer('end', $element, $element['#required'] || $form_state['triggering_element']['#name'] == $element['#name']);

    // Propagate rebuild key up if set during validation.
    if (!empty($subform_state['rebuild'])) {
      $form_state['rebuild'] = TRUE;
    }
  }
}

/**
 * Validation handler to be run after all other validation handlers.
 *
 * @see subform_parent_after_build()
 *   Registers this callback.
 */
function subform_parent_after_validate(&$form, &$form_state) {
  if (!empty($form_state['temporary']['subform'])) {

    // If we're not going to submit the parent form we need to cache its subforms here,
    // except when the form is going to be rebuild as subform_element_process()
    // will cache it during the rebuild.
    $submit = $form_state['submitted'] && !form_get_errors() && !$form_state['rebuild'];
    foreach ($form_state['temporary']['subform'] as $subform_name => &$subform_state) {

      // Cache subform if neccesary. Caching the subforms here allows for making
      // changes to subform states in any validation handler.
      if (!$submit && !$form_state['rebuild'] && empty($form_state['programmed']) && !$subform_state['rebuild'] && $subform_state['cache'] && empty($subform_state['no_cache'])) {
        form_set_cache($subform_state['values']['form_build_id'], $subform_state['temporary']['subform_unprocessed'], $subform_state);
      }
    }
  }
}

/**
 * Submit handler for submitting a single subform.
 *
 * @param $form
 *   An associative array containing the structure of the parent form.
 * @param $form_state
 *   A keyed array containing the current state of the parent form.
 */
function subform_element_submit(&$form, &$form_state) {
  if ($form_state['triggering_element']['#type'] == 'subform') {
    $subform_state =& subform_get_state($form_state['triggering_element']['#name'], $form_state);
    $subform_element =& subform_array_get_nested_value($form, $subform_state['subform_element_parents'], $subform_exists);
    if ($subform_exists) {
      subform_buffer('start', $subform_element);
      subform_submit_subform($subform_element['#subform']['#form_id'], $subform_element['#subform'], $subform_state);

      // Always display errors of subforms requested specifically to submit.
      subform_buffer('end', $subform_element, TRUE);

      // As this submit handler only executes the subform, rebuild the
      // parent form, but only if subform element does not have
      // #limit_validation_errors set.
      $form_state['rebuild'] = empty($subform_element['#limit_validation_errors']);
    }
  }
}

/**
 * Submit handler for submitting all subforms.
 *
 * @param $form
 *   An associative array containing the structure of the parent form.
 * @param $form_state
 *   A keyed array containing the current state of the parent form.
 */
function subform_submit_all(&$form, &$form_state) {
  if (!empty($form_state['temporary']['subform'])) {
    foreach ($form_state['temporary']['subform'] as $subform_name => &$subform_state) {
      $subform_element =& subform_array_get_nested_value($form, $subform_state['subform_element_parents'], $subform_exists);
      if ($subform_exists) {
        subform_buffer('start', $subform_element);
        subform_submit_subform($subform_element['#subform']['#form_id'], $subform_element['#subform'], $subform_state);

        // Only display subform errors if it was actually executed.
        subform_buffer('end', $subform_element, $subform_state['executed']);

        // As subform_submit_all() most of the time will be called in combination
        // with parent form's default submit handlers only request a form rebuild
        // here if the subform does so too.
        if (!empty($subform_state['rebuild'])) {
          $form_state['rebuild'] = TRUE;
        }
      }
    }
  }
}

/**
 * Submit handler to be run after all other submit handlers.
 *
 * @see subform_parent_after_build()
 *   Registers this callback.
 */
function subform_parent_after_submit(&$form, &$form_state) {
  if (!empty($form_state['temporary']['subform'])) {
    $redirect = empty($form_state['programmed']) && empty($form_state['rebuild']) && empty($form_state['no_redirect']) && (!isset($form_state['redirect']) || $form_state['redirect'] !== FALSE);
    foreach ($form_state['temporary']['subform'] as $subform_name => &$subform_state) {
      if (!$subform_state['executed']) {

        // Not executed subforms need to be rebuild if the parent form does too.
        if ($form_state['rebuild']) {
          $subform_state['rebuild'] = TRUE;
        }
      }

      // Clear subform's cache if parent is redirecting or subform is executed.
      if ($redirect || $subform_state['executed']) {

        // We'll clear out the cached copies of the form and its stored data
        // here, as we've finished with them. The in-memory copies are still
        // here, though.
        if (!variable_get('cache', 0) && !empty($subform_state['values']['form_build_id'])) {
          cache_clear_all('form_' . $subform_state['values']['form_build_id'], 'cache_form');
          cache_clear_all('form_state_' . $subform_state['values']['form_build_id'], 'cache_form');
        }
      }

      // Prevent subforms from rebuilding while they shouldn't.
      if (isset($form_state['rebuild']) && !$subform_state['rebuild']) {

        // Reset subform state.
        $subform_state = form_state_defaults();

        // Remove subform input, including input for nested subforms.
        foreach ($form_state['input'] as $name => $value) {
          if ($name == $subform_name || strpos($name, $subform_name . '-') === 0) {
            unset($form_state['input'][$name]);
          }
        }
      }

      // Cache subform if neccesary. Caching the subforms here allows for making
      // changes to subform states in any submit handler.
      if (!$redirect && empty($form_state['rebuild']) && empty($form_state['programmed']) && empty($subform_state['rebuild']) && !empty($subform_state['cache']) && empty($subform_state['no_cache'])) {
        form_set_cache($subform_state['values']['form_build_id'], $subform_state['temporary']['subform_unprocessed'], $subform_state);
      }
    }
  }
}

/**
 * Pre-render callback for subforms.
 *
 * Remove the form option from the theme wrapper, so there will not be nested
 * forms.
 *
 * @see subform_form_after_build()
 *   Registers this callback.
 */
function subform_form_pre_render($element) {
  $element['#theme_wrappers'] = array_diff($element['#theme_wrappers'], array(
    'form',
  ));
  return $element;
}

/**
 * Pre-render callback for elements within subforms.
 *
 * Prefixes the #name of all input elements except files in $form with $prefix
 *
 * @see subform_form_after_build()
 *   Registers this callback.
 */
function subform_form_child_pre_render($element) {
  $subform_name = $element['#subform_name'];
  if (!isset($element['#access']) || $element['#access']) {

    // Prefix the #attributes[name] of all input elements with the subform name.
    // Don't prefix #name as theme_form_element() needs the original #name to
    // set correct classes on which CSS and Javascript might depend.
    if (!empty($element['#name'])) {

      // Name set in #attributes has preference.
      $element_name = isset($element['#attributes']['name']) ? $element['#attributes']['name'] : $element['#name'];
      if ($element['#type'] == 'file') {
        $element_name = substr($element_name, 6, -1);
        $element['#attributes']['name'] = 'files[' . $subform_name . '-' . $element_name . ']';
      }
      elseif ($element['#type'] != 'subform') {
        $element_name = explode('[', $element_name, 2);
        $element_name[0] = '[' . $element_name[0] . ']';
        $element['#attributes']['name'] = $subform_name . implode('[', $element_name);
      }
    }

    // Prepend the special input key '_subform_element_name' to ajax settings.
    if (!empty($element['#attached']['js'])) {
      foreach ($element['#attached']['js'] as &$js) {
        if (isset($element['#id']) && is_array($js) && isset($js['data']['ajax'][$element['#id']])) {

          // Note that the submit array is rebuild so '_subform_element_name' is
          // the first element in the array, which is very important for subform_init().
          $js['data']['ajax'][$element['#id']]['submit'] = array(
            '_subform_element_name' => $subform_name,
          ) + $js['data']['ajax'][$element['#id']]['submit'];
        }
      }
    }

    // Correct input element names in #states.
    if (!empty($element['#states'])) {
      foreach ($element['#states'] as $state => $conditions) {
        $element['#states'][$state] = array();
        foreach ($conditions as $selector => $condition) {
          $selector = preg_replace('%\\[name="([^["]+)(.+)?"\\]%', '[name="' . $subform_name . '[$1]$2"]', $selector);
          $element['#states'][$state][$selector] = $condition;
        }
      }
    }
  }
  return $element;
}

/**
 * Copy of drupal_array_get_nested_value() supporting return-by-reference.
 *
 * http://drupal.org/node/991066
 */
function &subform_array_get_nested_value(array &$array, array $parents, &$key_exists = NULL) {
  $ref =& $array;
  foreach ($parents as $parent) {
    if (is_array($ref) && array_key_exists($parent, $ref)) {
      $ref =& $ref[$parent];
    }
    else {
      $key_exists = FALSE;
      $null = NULL;
      return $null;
    }
  }
  $key_exists = TRUE;
  return $ref;
}

/**
 * Returns a reference on a subform's state.
 *
 * @param $subform_name
 *   Unique identifier of the requested subform ($subform_element['#name']).
 * @param $form_state
 *   A keyed array containing the current state of the parent form.
 *
 * @return
 *   A reference to the state of requested subform or FALSE if non-existant.
 */
function &subform_get_state($subform_name, &$form_state) {
  if (!isset($form_state['temporary']['subform'][$subform_name])) {
    $return = FALSE;
    return $return;
  }
  return $form_state['temporary']['subform'][$subform_name];
}

/**
 * Returns the form state of a subform's parent form.
 *
 * @param $subform_state
 *   A keyed array containing the current state of the subform.
 *
 * @return
 *   The state of subform's parent form.
 */
function subform_get_parent_state($subform_state) {
  if (!empty($subform_state['subform_name'])) {
    if (isset($subform_state['temporary']['subform_parent_state'])) {
      return $subform_state['temporary']['subform_parent_state'];
    }
    elseif ($cached = cache_get('form_state_' . $subform_state['subform_parent']['build_id'], 'cache_form')) {
      return $cached->data + form_state_defaults();
    }
  }
  return FALSE;
}

/**
 * Helper function to manually set the triggering element of a subform.
 *
 * This function can be used on to manually set the triggering element of
 * a subform. This might be useful when #subform_default_triggering_element or
 * #subform_triggering_element are not sufficient.
 *
 * Example:
 * @code
 * function example_parent_form_process($form, &$form_state) {
 *   $subform_element = $form['my']['subform']['element'];
 *   $subform = $subform_element['#subform'];
 *   $subform_state = &subform_get_state($subform_element['#name'], $form_state);
 *
 *   // Something conditional
 *   if ($form_state['values']['foo'] == 'bar') {
 *     $action = 'do_something_special';
 *   }
 *   else {
 *     $action = 'default';
 *   }
 *   subform_set_triggering_element($subform['actions'][$action], $subform_state);
 * }
 * @endcode
 *
 * @param $element
 *   Element to set as the triggering element.
 * @param $form_state
 *   Form state of the subform where $element belongs to.
 *
 * @see form_builder()
 *   Contains code that is duplicated here.
 */
function subform_set_triggering_element($element, &$form_state) {
  $form_state['triggering_element'] = $element;

  // If the triggering element specifies "button-level" validation and submit
  // handlers to run instead of the default form-level ones, then add those to
  // the form state.
  foreach (array(
    'validate',
    'submit',
  ) as $type) {
    if (isset($form_state['triggering_element']['#' . $type])) {
      $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type];
    }
  }

  // If the triggering element executes submit handlers, then set the form
  // state key that's needed for those handlers to run.
  if (!empty($form_state['triggering_element']['#executes_submit_callback'])) {
    $form_state['submitted'] = TRUE;
  }

  // Special processing if the triggering element is a button.
  if (isset($form_state['triggering_element']['#button_type'])) {
    $form_state['values'][$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value'];
    $form_state['clicked_button'] = $form_state['triggering_element'];
  }
}

/**
 * Buffers errors and uploads seperating them per (sub)form.
 */
function subform_buffer($op, &$subform_element, $propagate = FALSE) {
  switch ($op) {
    case 'start':
      subform_buffer_errors($op, $subform_element, $propagate);
      subform_buffer_files($op, $subform_element);
      break;
    case 'end':
      subform_buffer_files($op, $subform_element);
      subform_buffer_errors($op, $subform_element, $propagate);
      break;
  }
}

/**
 * Buffers errors seperating them per (sub)form.
 */
function subform_buffer_errors($op, &$subform_element, $propagate = FALSE) {
  static $errors_stack = array();
  static $sections_stack = array();
  static $messages_stack = array();
  $errors =& drupal_static('form_set_error', array());
  $sections =& drupal_static('form_set_error:limit_validation_errors');
  switch ($op) {
    case 'start':
      $errors_stack[] = $errors;
      $sections_stack[] = $sections;
      $messages_stack[] = isset($_SESSION['messages']['error']) ? $_SESSION['messages']['error'] : array();
      $errors = $subform_element['#subform_errors'];
      $sections = NULL;
      if (isset($_SESSION['messages']['error'])) {
        unset($_SESSION['messages']['error']);
      }
      break;
    case 'end':
      $subform_element['#subform_errors'] += $errors;
      if (isset($_SESSION['messages']['error'])) {
        $subform_element['#subform_error_messages'] += $_SESSION['messages']['error'];
      }
      $errors = array_pop($errors_stack);
      $sections = array_pop($sections_stack);
      $_SESSION['messages']['error'] = array_pop($messages_stack);
      if (empty($_SESSION['messages']['error'])) {
        unset($_SESSION['messages']['error']);
      }

      // If the subform contains errors inform the parent form, but only if
      // the subform is required.
      if ($propagate && !empty($subform_element['#subform_errors'])) {
        $prior_errors = $errors;
        form_error($subform_element);

        // If the error was added (not skipped by #limit_validation_errors) to
        // the parent form also display subform's error messages.
        if ($errors != $prior_errors && !empty($subform_element['#subform_error_messages'])) {
          $subform_element['#subform_has_errors'] = TRUE;
          if (!isset($_SESSION['messages']['error'])) {
            $_SESSION['messages']['error'] = array();
          }
          $_SESSION['messages']['error'] += $subform_element['#subform_error_messages'];
          $subform_element['#subform_error_messages'] = array();
        }
      }
      break;
  }
}

/**
 * Buffers uploads seperating them per (sub)form.
 */
function subform_buffer_files($op, &$subform_element) {
  static $files_stack = array();
  $subform_name = $subform_element['#name'];
  switch ($op) {
    case 'start':
      $files_stack[] = $prior_files = $_FILES;
      $_FILES = array();
      if (!empty($prior_files['files'])) {
        foreach ($prior_files['files']['name'] as $old_name => $value) {
          if (strpos($old_name, $subform_name . '-') === 0) {

            // Remove the subform element name prefix.
            $new_name = substr($old_name, strlen($subform_name . '-'));
            foreach (array(
              'name',
              'type',
              'tmp_name',
              'error',
              'size',
            ) as $key) {
              $_FILES['files'][$key][$new_name] = $prior_files['files'][$key][$old_name];
            }
          }
        }
      }
      break;
    case 'end':
      $_FILES = array_pop($files_stack);
      break;
  }
}

/**
 * Submits a subform.
 *
 * @param $form_id
 *   The unique string identifying the current form.
 * @param $form
 *   An associative array containing the structure of the form.
 * @param $form_state
 *   A keyed array containing the current state of the form.
 *
 * @see drupal_process_form()
 *   Contains code that is duplicated here.
 */
function subform_submit_subform($form_id, &$form, &$form_state) {
  if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild'] && !$form_state['executed']) {

    // Execute form submit handlers.
    subform_execute_submit_handlers($form, $form_state);

    // Set a flag to indicate the the form has been processed and executed.
    $form_state['executed'] = TRUE;
  }

  // Don't rebuild or cache form submissions invoked via drupal_form_submit().
  if (!empty($form_state['programmed'])) {
    return;
  }

  // Rebuild subform if neccesary. The rebuild indicator is propagated up to the
  // wrapper form which will take care of the actual rebuild.
  if (!$form_state['executed'] && !form_get_errors()) {
    $form_state['rebuild'] = TRUE;
  }
}

/**
 * A helper function used to execute submission handlers for a given subform.
 *
 * @see form_execute_handlers()
 *   Contains code that is duplicated here.
 */
function subform_execute_submit_handlers(&$form, &$form_state) {

  // If there was a button pressed, use its handlers.
  if (isset($form_state['submit_handlers'])) {
    $handlers = $form_state['submit_handlers'];
  }
  elseif (isset($form['#submit'])) {
    $handlers = $form['#submit'];
  }
  else {
    $handlers = array();
  }
  foreach ($handlers as $function) {

    // Check if a previous _submit handler has set a batch, but make sure we
    // do not react to a batch that is already being processed (for instance
    // if a batch operation performs a drupal_form_submit()).
    if (($batch =& batch_get()) && !isset($batch['id'])) {

      // Some previous submit handler has set a batch. To ensure correct
      // execution order, store the call in a special 'control' batch set.
      // We don't have to set $batch['has_form_submits'] as
      // subform_parent_after_submit() is always registered for forms containing
      // subforms. So form_execute_handlers() will set $batch['has_form_submits']
      // for the root form.
      batch_set(array(
        'operations' => array(
          array(
            'subform_batch_execute_subform_submit_handler',
            array(
              $form_state['subform_name'],
              $function,
            ),
          ),
        ),
      ));
    }
    else {
      $function($form, $form_state);
    }
  }
}

/**
 * Batch operation that executes a submit handler for a subform.
 *
 * @see subform_execute_submit_handlers()
 *   Registers this callback.
 */
function subform_batch_execute_subform_submit_handler($subform_name, $submit_handler) {
  $batch =& batch_get();

  // TODO nested subforms aren't stored in root form's state.
  $subform_state = subform_get_state($subform_name, $batch['form_state']);
  $submit_handler($subform_state['complete form'], $subform_state);
}

/**
 * Returns HTML for a subform.
 *
 * @param $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #attributes, #subform
 *
 * @ingroup themeable
 */
function theme_subform($variables) {
  $element = $variables['element'];
  element_set_attributes($element, array(
    'id',
  ));
  $element['#attributes']['class'][] = 'subform';
  if (!isset($element['#subform_has_errors'])) {
    $element['#subform_errors'] = array();
  }
  subform_buffer_errors('start', $element);
  $element['#children'] = drupal_render($element['#subform']);
  subform_buffer_errors('end', $element);
  return '<div' . drupal_attributes($element['#attributes']) . '>' . $element['#children'] . '</div>';
}

Functions

Namesort descending Description
subform_array_get_nested_value Copy of drupal_array_get_nested_value() supporting return-by-reference.
subform_batch_execute_subform_submit_handler Batch operation that executes a submit handler for a subform.
subform_buffer Buffers errors and uploads seperating them per (sub)form.
subform_buffer_errors Buffers errors seperating them per (sub)form.
subform_buffer_files Buffers uploads seperating them per (sub)form.
subform_element_info Implements hook_element_info().
subform_element_process Processes a subform element.
subform_element_rebuild Constructs a new $subform from the information in $subform_state.
subform_element_submit Submit handler for submitting a single subform.
subform_element_validate Validation handler for the subform element.
subform_element_value Value callback for the subform element.
subform_execute_submit_handlers A helper function used to execute submission handlers for a given subform.
subform_form_after_build After-build callback for form elements within subform elements.
subform_form_after_build_traverse_children Helper function to set a #pre_render callback on all elements within a subform.
subform_form_alter Implements hook_form_alter().
subform_form_child_pre_render Pre-render callback for elements within subforms.
subform_form_pre_render Pre-render callback for subforms.
subform_form_process Process callback for form elements within subform elements.
subform_get_parent_state Returns the form state of a subform's parent form.
subform_get_state Returns a reference on a subform's state.
subform_init Implements hook_init().
subform_parent_after_build After-build callback for form elements containing subform elements.
subform_parent_after_submit Submit handler to be run after all other submit handlers.
subform_parent_after_validate Validation handler to be run after all other validation handlers.
subform_set_triggering_element Helper function to manually set the triggering element of a subform.
subform_submit_all Submit handler for submitting all subforms.
subform_submit_subform Submits a subform.
subform_theme Implements hook_theme().
theme_subform Returns HTML for a subform.