Jun 13, 2014

Building a form using a route path with a wildcard

The Optimizely module provides a combined Add/Update form that is used both to add a new project as well as edit an existing one.

In D7 this is implemented in  hook_menu() by defining two different paths for bringing up the form. One path is for the "add" version with the fields blank. The other path includes a positive integer that is a unique id for a project. The project id is used to prepopulate the fields. Note the wildcard % in the second path below.

$items['admin/config/system/optimizely/add_update'] = array(
  'title' => 'Add Project',
  'page callback' => 'drupal_get_form',
  'page arguments' => array('optimizely_add_update_form'),

  'type' => MENU_LOCAL_TASK,
  // ...
);

$items['admin/config/system/optimizely/add_update/%'] = array(
  'title' => 'Edit Project',
  'page callback' => 'optimizely_add_update_update',
  'page arguments' => array(5),

  // ...
);

For the "edit" version, function   optimizely_add_update_update() acts as an intermediary to accept the project id as an argument and pass it on as an optional parameter to function  optimizely_add_update_form(), which does the actual form building.

function optimizely_add_update_update($target_project) { 
  return drupal_get_form('optimizely_add_update_form', 

                         $target_project);
}


For conversion purposes, I wanted to keep this path structure intact. This turned out to be a little harder to do in Drupal 8 than in D7.

First, as usual, it's necessary to define a route in the routing file. My initial attempt was to do the following, using the AddUpdateForm class that I had previously created for the "add" form.

optimizely.add_update.oid:
  path: /admin/config/system/optimizely/add_update/{oid}
  defaults: 

    _form: \Drupal\optimizely\AddUpdateForm
    _title: Optimizely Edit Project
  requirements:
    _permission: administer optimizely


My thinking was that there would be some way to pass the value of the {oid} wildcard to the AddUpdateForm::buildForm() method. Unfortunately, I remained stymied in finding a way to add such an optional parameter.

Eventually, I decided to use an intermediary class and method, changing the route definition as follows.

optimizely.add_update.oid:
  path: /admin/config/system/optimizely/add_update/{oid}
  defaults: 

    _content: \Drupal\optimizely\DoUpdate::buildUpdateForm
    _title: Optimizely Edit Project
  requirements:
    _permission: administer optimizely


The key difference is the use of the  _content  property rather than  _form. The class definition is simply

class DoUpdate {
  public static function buildUpdateForm($oid) {
    return \Drupal::formBuilder()->getForm(new AddUpdateForm($oid));
  }
}


By using the  _content  property there was no problem in passing the value of {oid} to the specified method, which is done automatically. That value is then passed along as a parameter to a constructor for class AddUpdateForm, which also has a new class variable in addition to the constructor.

class AddUpdateForm extends FormBase {

  // A positive integer which is a unique Optimizely project id.
  private $oid = NULL;

  public function __construct($oid = NULL) {
    $this->oid = $oid;
  }


  // ... the rest of the class definition, as before.
}

The  buildForm() method then uses the value of the class variable to fetch the existing values of the project from the database.

The final piece was to add the wildcard path to hook_help() so that the same help message could be displayed on the edit page. This also turned out to be a little tricky.

My first attempt was to do the following in the switch statement comprising the body of the function, adding the second case to the same help text.

case 'admin/config/system/optimizely/add_update':
case 'admin/config/system/optimizely/add_update/%':
  return t('Add or edit specific project entries. ...');


That wildcard did not work. The path that was passed to  hook_help()  had the actual project id, not a wildcard. So at the beginning of  hook_menu I inserted this code to fake the wildcard.

function optimizely_help($path, $arg) {

  // Look for paths for updating particular projects and
  // fix them to match in the switch statement below.
  // The replacement string includes % as a kind of wildcard.


  $pattern = ':^admin/config/system/optimizely/add_update/\d+$:';
  $replacement = 'admin/config/system/optimizely/add_update/%';
  $path = preg_replace($pattern, $replacement, $path);



Update: see this newer post for a better solution  http://optimizely-to-drupal-8.blogspot.com/2014/06/take-two-building-form-using-route-path.html

Update: see  http://optimizely-to-drupal-8.blogspot.com/2014/12/beta-3-beta-4-routes-use-controller.html



Sources.

Drupal 8 using Drupal::formbuilder()->getForm();
http://domainpiranha.com/Drupal-8-formbuilder-reusing-forms

Parameter upcasting in routes
https://drupal.org/node/2122223

2 comments:

  1. Check out the following to get around using _content to pass in a parameter to the form:

    Form API in Drupal 8
    https://drupal.org/node/2117411

    $extra = '612-123-4567';
    $form = \Drupal::formBuilder()->getForm('Drupal\mymodule\Form\ExampleForm', $extra);

    ReplyDelete
    Replies
    1. Dee, you spotted something in the documentation that I overlooked. Thanks a lot! I changed my solution to make it simpler and cleaner and wrote about that in a follow up post at http://optimizely-to-drupal-8.blogspot.com/2014/06/take-two-building-form-using-route-path.html

      Delete