The More with symfony book

Custom Widgets and Validators

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


About

You are currently reading "The More with symfony book" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.

Master symfony

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Books on symfony

Learn more about symfony with the official guides.
books.sensiolabs.com

L'audit Qualité par SensioLabs

200 points de contrôle de votre applicatif web.
audit.sensiolabs.com

Chapter Content

Widget and Validator Internals

sfWidgetForm Internals

sfValidatorBase Internals

The options Attribute

Building a Simple Widget and Validator

The Google Address Map Widget

sfWidgetFormGMapAddress Widget

sfValidatorGMapAddress Validator

Testing

Final Thoughts

symfony training
Be trained by symfony experts
Feb 21: Köln (Getting Started with Symfony2 - English)
Feb 27: Köln (Mastering Symfony2 - English)
Mar 05: Köln (Web Development with Symfony2 - Deutsch)
Mar 05: Montreal (Web Development with Symfony2 - English)
Mar 05: Montreal (Getting Started with Symfony2 - English)
and more...

Search


powered by google
You are currently browsing "The More with symfony book" in English for the 1.4 version - Switch to language:
Creative Commons License This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.
More with Symfony
Support symfony!
Buy this book
or donate.
Buy More with Symfony from amazon.com

by Thomas Rabaix

This chapter explains how to build a custom widget and validator for use in the form framework. It will explain the internals of sfWidgetForm and sfValidator, as well as how to build both a simple and complex widget.

Widget and Validator Internals

sfWidgetForm Internals

An object of the sfWidgetForm class represents the visual implementation of how related data will be edited. A string value, for example, might be edited with a simple text box or an advanced WYSIWYG editor. In order to be fully configurable, the sfWidgetForm class has two important properties: options and attributes.

Additionally, the sfWidgetForm class implements two important methods:

An sfWidgetForm object does not know anything about its name or its value. The component is responsible only for rendering the widget. The name and the value are managed by an sfFormFieldSchema object, which is the link between the data and the widgets.

sfValidatorBase Internals

The sfValidatorBase class is the base class of each validator. The sfValidatorBase::clean() method is the most important method of this class as it checks if the value is valid depending on the provided options.

Internally, the clean() method perform several different actions:

The doClean() method is the method which implements the main validation logic. It is not good practice to override the clean() method. Instead, always perform any custom logic via the doClean() method.

A validator can also be used as a standalone component to check input integrity. For instance, the sfValidatorEmail validator will check if the email is valid:

$v = new sfValidatorEmail();
 
try
{
  $v->clean($request->getParameter("email"));
}
catch(sfValidatorError $e)
{
  $this->forward404();
}

When a form is bound to the request values, the sfForm object keeps references to the original (dirty) values and the validated (clean) values. The original values are used when the form is redrawn, while the cleaned values are used by the application (e.g. to save the object).

The options Attribute

Both the sfWidgetForm and sfValidatorBase objects have a variety of options: some are optional while others are mandatory. These options are defined inside each class's configure() method via:

These two methods are very convenient as they ensure that dependency values are correctly passed to the validator or the widget.

Building a Simple Widget and Validator

This section will explain how to build a simple widget. This particular widget will be called a "Trilean" widget. The widget will display a select box with three choices: No, Yes and Null.

class sfWidgetFormTrilean extends sfWidgetForm
{
  public function configure($options = array(), $attributes = array())
  {
 
    $this->addOption('choices', array(
      0 => 'No',
      1 => 'Yes',
      'null' => 'Null'
    ));
  }
 
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $value = $value === null ? 'null' : $value;
 
    $options = array();
    foreach ($this->getOption('choices') as $key => $option)
    {
      $attributes = array('value' => self::escapeOnce($key));
      if ($key == $value)
      {
        $attributes['selected'] = 'selected';
      }
 
      $options[] = $this->renderContentTag(
        'option',
        self::escapeOnce($option),
        $attributes
      );
    }
 
    return $this->renderContentTag(
      'select',
      "\n".implode("\n", $options)."\n",
      array_merge(array('name' => $name), $attributes
    ));
  }
}

The configure() method defines the option values list via the choices option. This array can be redefined (i.e. to change the associated label of each value). There is no limit to the number of option a widget can define. The base widget class, however, declares a few standard options, which function like de-facto reserved options:

The render() method generates the corresponding HTML for a select box. The method calls the built-in renderContentTag() function to help render HTML tags.

The widget is now complete. Let's create the corresponding validator:

class sfValidatorTrilean extends sfValidatorBase
{
  protected function configure($options = array(), $messages = array())
  {
    $this->addOption('true_values', array('true', 't', 'yes', 'y', 'on', '1'));
    $this->addOption('false_values', array('false', 'f', 'no', 'n', 'off', '0'));
    $this->addOption('null_values', array('null', null));
  }
 
  protected function doClean($value)
  {
    if (in_array($value, $this->getOption('true_values')))
    {
      return true;
    }
 
    if (in_array($value, $this->getOption('false_values')))
    {
      return false;
    }
 
    if (in_array($value, $this->getOption('null_values')))
    {
      return null;
    }
 
    throw new sfValidatorError($this, 'invalid', array('value' => $value));
  }
 
  public function isEmpty($value)
  {
    return false;
  }
}

The sfValidatorTrilean validator defines three options in the configure() method. Each option is a set of valid values. As these are defined as options, the developer can customize the values depending on the specification.

The doClean() method checks if the value matches a set a valid values and returns the cleaned value. If no value is matched, the method will raise an sfValidatorError which is the standard validation error in the form framework.

The last method, isEmpty(), is overridden as the default behavior of this method is to return true if null is provided. As the current widget allows null as a valid value, the method must always return false.

If isEmpty() returns true, the doClean() method will never be called.

While this widget was fairly straightforward, it introduced some important base features that will be needed as we go further. The next section will create a more complex widget with multiple fields and JavaScript interaction.

The Google Address Map Widget

In this section, we are going to build a complex widget. New methods will be introduced and the widget will have some JavaScript interaction as well. The widget will be called "GMAW": "Google Map Address Widget".

What do we want to achieve? The widget should provide an easy way for the end user to add an address. By using an input text field and with google's map services we can achieve this goal.

"Google Map Address Widget" mashup

Use case 1:

Use case 2:

The following fields need to be posted and handled by the form:

The widget's functional specifications have just been defined, now let's define the technical tools and their scopes:

sfWidgetFormGMapAddress Widget

As a widget is the visual representation of data, the configure() method of the widget must have different options to tweak the Google map or modify the styles of each element. One of the most important options is the template.html option, which defines how all elements are ordered. When building a widget it is very important to think about reusability and extensibility.

Another important thing is the external assets definition. An sfWidgetForm class can implement two specific methods:

The current widget only requires some JavaScript to work so no stylesheet is needed. In this case, however, the widget will not handle the initialization of the Google JavaScript, though the widget will make use of the Google geocoding and map services. Instead, it will be the developer's responsibility to include it on the page. The reason behind this is that Google's services may be used by other elements on the page, and not only by the widget.

Let's jump to the code:

class sfWidgetFormGMapAddress extends sfWidgetForm
{
  public function configure($options = array(), $attributes = array())
  {
    $this->addOption('address.options', array('style' => 'width:400px'));
 
    $this->setOption('default', array(
      'address' => '',
      'longitude' => '2.294359',
      'latitude' => '48.858205'
    ));
 
    $this->addOption('div.class', 'sf-gmap-widget');
    $this->addOption('map.height', '300px');
    $this->addOption('map.width', '500px');
    $this->addOption('map.style', "");
    $this->addOption('lookup.name', "Lookup");
 
    $this->addOption('template.html', '
      <div id="{div.id}" class="{div.class}">
        {input.search} <input type="submit" value="{input.lookup.name}"  id="{input.lookup.id}" /> <br />
        {input.longitude}
        {input.latitude}
        <div id="{map.id}" style="width:{map.width};height:{map.height};{map.style}"></div>
      </div>
    ');
 
     $this->addOption('template.javascript', '
      <script type="text/javascript">
        jQuery(window).bind("load", function() {
          new sfGmapWidgetWidget({
            longitude: "{input.longitude.id}",
            latitude: "{input.latitude.id}",
            address: "{input.address.id}",
            lookup: "{input.lookup.id}",
            map: "{map.id}"
          });
        })
      </script>
    ');
  }
 
  public function getJavascripts()
  {
    return array(
      '/sfFormExtraPlugin/js/sf_widget_gmap_address.js'
    );
  }
 
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    // define main template variables
    $template_vars = array(
      '{div.id}'             => $this->generateId($name),
      '{div.class}'          => $this->getOption('div.class'),
      '{map.id}'             => $this->generateId($name.'[map]'),
      '{map.style}'          => $this->getOption('map.style'),
      '{map.height}'         => $this->getOption('map.height'),
      '{map.width}'          => $this->getOption('map.width'),
      '{input.lookup.id}'    => $this->generateId($name.'[lookup]'),
      '{input.lookup.name}'  => $this->getOption('lookup.name'),
      '{input.address.id}'   => $this->generateId($name.'[address]'),
      '{input.latitude.id}'  => $this->generateId($name.'[latitude]'),
      '{input.longitude.id}' => $this->generateId($name.'[longitude]'),
    );
 
    // avoid any notice errors to invalid $value format
    $value = !is_array($value) ? array() : $value;
    $value['address']   = isset($value['address'])   ? $value['address'] : '';
    $value['longitude'] = isset($value['longitude']) ? $value['longitude'] : '';
    $value['latitude']  = isset($value['latitude'])  ? $value['latitude'] : '';
 
    // define the address widget
    $address = new sfWidgetFormInputText(array(), $this->getOption('address.options'));
    $template_vars['{input.search}'] = $address->render($name.'[address]', $value['address']);
 
    // define the longitude and latitude fields
    $hidden = new sfWidgetFormInputHidden;
    $template_vars['{input.longitude}'] = $hidden->render($name.'[longitude]', $value['longitude']);
    $template_vars['{input.latitude}']  = $hidden->render($name.'[latitude]', $value['latitude']);
 
    // merge templates and variables
    return strtr(
      $this->getOption('template.html').$this->getOption('template.javascript'),
      $template_vars
    );
  }
}

The widget uses the generateId() method to generate the id of each element. The $name variable is defined by the sfFormFieldSchema, so the $name variable is composed of the name form, any nested widget schema names and the name of the widget as defined in the form configure().

For instance, if the form name is user, the nested schema name is location and the widget name is address, the final name will be user[location][address] and the id will be user_location_address. In other words, $this->generateId($name.'[latitude]') will generate a valid and unique id for the latitude field.

The different element id attributes are quite important as there are passed to the JavaScript block (via the template.js variable), so the JavaScript can properly handle the different elements.

The render() method also instantiates two inner widgets: an sfWidgetFormInputText widget, which is used to render the address field, and an sfWidgetFormInputHidden widget, which is used to render the hidden fields.

The widget can be quickly tested with this small piece of code:

$widget = new sfWidgetFormGMapAddress();
echo $widget->render('user[location][address]', array(
  'address' => '151 Rue montmartre, 75002 Paris',
  'longitude' => '2.294359',
  'latitude' => '48.858205'
));

The output result is:

<div id="user_location_address" class="sf-gmap-widget">
  <input style="width:400px" type="text" name="user[location][address][address]" value="151 Rue montmartre, 75002 Paris" id="user_location_address_address" />
  <input type="submit" value="Lookup"  id="user_location_address_lookup" /> <br />
  <input type="hidden" name="user[location][address][longitude]" value="2.294359" id="user_location_address_longitude" />
  <input type="hidden" name="user[location][address][latitude]" value="48.858205" id="user_location_address_latitude" />
  <div id="user_location_address_map" style="width:500px;height:300px;"></div>
</div>
 
<script type="text/javascript">
  jQuery(window).bind("load", function() {
    new sfGmapWidgetWidget({
      longitude: "user_location_address_longitude",
      latitude: "user_location_address_latitude",
      address: "user_location_address_address",
      lookup: "user_location_address_lookup",
      map: "user_location_address_map"
    });
  })
</script>

The JavaScript part of the widget takes the different id attributes and binds jQuery listeners to them so that certain JavaScript is triggered when actions are performed. The JavaScript updates the hidden fields with the longitude and latitude provided by the google geocoding service.

The JavaScript object has a few interesting methods:

The final JavaScript code can be viewed in Appendix A.

Please refer to the Google map documentation for more details on the functionality of the Google maps API.

sfValidatorGMapAddress Validator

The sfValidatorGMapAddress class extends sfValidatorBase which already performs one validation: specifically, if the field is set as required then the value cannot be null. Thus, sfValidatorGMapAddress need only validate the different values: latitude, longitude and address. The $value variable should be an array, but as the user input should not be trusted, the validator checks for the presence of all keys so that the inner validators are passed valid values.

class sfValidatorGMapAddress extends sfValidatorBase
{
  protected function doClean($value)
  {
    if (!is_array($value))
    {
      throw new sfValidatorError($this, 'invalid');
    }
 
    try
    {
      $latitude = new sfValidatorNumber(array( 'min' => -90, 'max' => 90, 'required' => true ));
      $value['latitude'] = $latitude->clean(isset($value['latitude']) ? $value['latitude'] : null);
 
      $longitude = new sfValidatorNumber(array( 'min' => -180, 'max' => 180, 'required' => true ));
      $value['longitude'] = $longitude->clean(isset($value['longitude']) ? $value['longitude'] : null);
 
      $address = new sfValidatorString(array( 'min_length' => 10, 'max_length' => 255, 'required' => true ));
      $value['address'] = $address->clean(isset($value['address']) ? $value['address'] : null);
    }
    catch(sfValidatorError $e)
    {
      throw new sfValidatorError($this, 'invalid');
    }
 
    return $value;
  }
}

A validator always raises an sfValidatorError exception when a value is not valid. That's why the validation is surrounded by a try/catch block. In this validator, the validator re-throws a new invalid exception, which equates to an invalid validation error on the sfValidatorGMapAddress validator.

Testing

Why is testing important? The validator is the glue between the user input and the application. If the validator is flawed, the application is vulnerable. Fortunately, symfony comes with lime which is a testing library that is very easy to use.

How can we test the validator? As stated before, a validator raises an exception on a validation error. The test can send valid and invalid values to the validator and check to see that the exception is thrown in the correct circumstances.

$t = new lime_test(7, new lime_output_color());
 
$tests = array(
  array(false, '', 'empty value'),
  array(false, 'string value', 'string value'),
  array(false, array(), 'empty array'),
  array(false, array('address' => 'my awesome address'), 'incomplete address'),
  array(false, array('address' => 'my awesome address', 'latitude' => 'String', 'longitude' => 23), 'invalid values'),
  array(false, array('address' => 'my awesome address', 'latitude' => 200, 'longitude' => 23), 'invalid values'),
  array(true, array('address' => 'my awesome address', 'latitude' => '2.294359', 'longitude' => '48.858205'), 'valid value')
);
 
$v = new sfValidatorGMapAddress;
 
$t->diag("Testing sfValidatorGMapAddress");
 
foreach($tests as $test)
{
  list($validity, $value, $message) = $test;
 
  try
  {
    $v->clean($value);
    $catched = false;
  }
  catch(sfValidatorError $e)
  {
    $catched = true;
  }
 
  $t->ok($validity != $catched, '::clean() '.$message);
}

When the sfForm::bind() method is called, the form executes the clean() method of each validator. This test reproduces this behavior by instantiating the sfValidatorGMapAddress validator directly and testing different values.

Final Thoughts

The most common mistake when creating a widget is to be overly focused on how the information will be stored in the database. The form framework is simply a data container and validation framework. Therefore, a widget must only manage its related information. If the data is valid then the different cleaned values can then be used by the model or in the controller.

Advanced Forms »
« Emails

Questions & Feedback

If you find a typo or an error, please register and open a ticket.

If you need support or have a technical question, please post to the official user mailing-list.