A post validator to define dependencies between fields

0 gravatar By Grégoire Marchal - 04/18/2011

Today, I share a post validator that allows to define dependencies between fields. For example, it allows to define that the postal_code is only required if the country field value is FR.

So here is the class:

/**
 * sfValidatorSchemaDependency allows to define dependency between fields
 */
class sfValidatorSchemaDependency extends sfValidatorSchema
{
  
  /**
   * Constructor.
   *
   * Available options:
   *
   *  * affected_field:  The field that is required or not
   *  * expected_values: A key/value array that represents the fields and their value
   *                     that must be matched to set the affected_field as required
   *
   * @param string $affected_field   The field that is required or not
   * @param array  $expected_values  A key/value array that represents the fields and their value
   *                                 that must be matched to set the affected_field as required
   * @param array  $options          An array of options
   * @param array  $messages         An array of error messages
   *
   * @see sfValidatorBase
   */
  public function __construct($affected_field, $expected_values, $options = array(), $messages = array())
  {
    $this->addOption('affected_field', $affected_field);
    $this->addOption('expected_values', $expected_values);

    parent::__construct(null, $options, $messages);
  }

  /**
   * @see sfValidatorBase
   */
  protected function doClean($values)
  {
    if (null === $values)
    {
      $values = array();
    }

    if (!is_array($values))
    {
      throw new InvalidArgumentException('You must pass an array parameter to the clean() method');
    }

    $affected_field = isset($values[$this->getOption('affected_field')]) ? $values[$this->getOption('affected_field')] : null;
    
    $bAllValuesMatched = true;
    foreach ($this->getOption('expected_values') as $field => $value)
    {
      // if a field has not the expected value
      if (!isset($values[$field]) || $values[$field] != $value)
      {
        $bAllValuesMatched = false;
        break;
      }
    }

    // if every field has the expected value, and affected_field not defined
    if ($bAllValuesMatched && !$affected_field)
    {
      // "affected_field is required" error
      throw new sfValidatorErrorSchema($this, array(
        $this->getOption('affected_field') => new sfValidatorError($this, 'required')
      ));
    }

    return $values;
  }
}

And here is how to use it for our introduction example:

  /**
   * @see sfForm::configure()
   */
  public function configure()
  {
    // ...
    $this->mergePostValidator(new sfValidatorSchemaDependency('postal_code', array('country' => 'FR')));
  }

Then, when the form will be validated, if the country field value is FR and if the postal_code field is empty, a required sfValidatorError will be attached to the postal_code field.

Note that you can define several dependencies, by passing several elements in the second parameter of the sfValidatorSchemaDependency constructor.

Creation of a panel for the Web Debug Toolbar and usage of the Event Dispatcher

0 gravatar By Grégoire Marchal - 02/09/2011

Let's write a small tutorial today. We'll create a panel for the WDT (Web Debug Toolbar) that will display the list of the SOAP WebService requests (but feel free to adapt it to whatever) executed on the current page, in the style of the Doctrine/Propel panels displaying SQL queries. Its content will be fed by events managed by the Event Dispatcher.

For the creation of the panel skeleton of sfWebDebugPanelSoapClient, I let you follow the official tutorial. When it's done, we'll fill this panel using the Event Dispatcher, by generating events. So let's go to the MySoapClient class, my class that generates the things I want to log.

First I create the method that creates and dispatches the event:

  /**
   * Dispatch a 'soapclient.log' event (used by web debug panel)
   *
   * @param SoapCommand $oCommand  The executed command
   * @param int         $iDuration The duration of the soap call
   */
  protected function dispatchEvent($oCommand, $iDuration)
  {
    $this->context->getEventDispatcher()->notify(new sfEvent($this, 'soapclient.log', array(
      'command'  => $oCommand,
      'duration' => $iDuration,
    )));
  }

As you can see, I create an event which type is "soapclient.log", about the current object ($this), and I provide some additional parameters that I'll display later in my panel... Then I use the Event Dispatcher to notify the objects that listen the "soapclient.log" events that there's a new one!

Then we have to call this method. In my case, it's just after the WebService call.

    $iTime = microtime(true);
    // do your stuff...
    $this->dispatchEvent($oCommand, microtime(true) - $iTime);

Now let's go back to the sfWebDebugPanelSoapClient class, to tell it to listen to that kind of event. So we'll override its constructor to connect a method to that event.

  protected $aEvents = array();

  public function __construct(sfWebDebug $webDebug)
  {
    parent::__construct($webDebug);
    $this->webDebug->getEventDispatcher()->connect('soapclient.log', array($this, 'listenForSoapClientLogs'));
  }

  /**
   * Listens to soapclient.log event and record them
   *
   * @param sfEvent $event
   */
  public function listenForSoapClientLogs(sfEvent $event)
  {
    $this->aEvents[] = $event;
  }

In the constructor, we ask the Event Dispatcher to call the "listenForSoapClientLogs" method of our class as soon as it receive a "soapclient.log" event. In this method, we just store each event in an array of our class.

So we have everything we need now. We just have to display the informations in our panel.

  /**
   * Get the html content of the panel
   *
   * @return string $html
   */
  public function getPanelContent()
  {
    return '
    <div id="sfWebDebugSoapClientLogs">
      <ol>'.implode("\n", $this->getSoapClientLogs()).'</ol>
    </div>
    ';
  }

  /**
   * Retrieves events as html
   *
   * @return string
   */
  protected function getSoapClientLogs()
  {
    $aRet = array();
    $i = 1;
    foreach ($this->aEvents as $oEvent)
    {
      $aParams = $oEvent->getParameters();

      $oClient = $oEvent->getSubject();
      $oCommand = $aParams['command'];

      $sHtml = '
        <li>
          ' . get_class($oCommand) . ' (' . round(($aParams['duration'] * 1000), 2) . ' ms)
          ...
        </li>
      ';

      $aRet[] = $sHtml;
      $i++;
    }
    return $aRet;
  }

That's all folks! Now we have a smart panel to debug a SOAP client, or whatever you want!

Soap Debug Panel

ValidatorEmailList : validate a list of emails

0 gravatar By Grégoire Marchal - 01/25/2011

Here is a short and simple validator that validates a list of email addresses from a "textarea" field.

class ValidatorEmailList extends sfValidatorBase
{
  public function doClean($value)
  {
    $aValues = explode("\n", $value);
    $aValues = array_map('trim', $aValues);
    $aEmails = array();
    
    $oEmailValidator = new sfValidatorEmail();

    foreach ($aValues as $sEmail)
    {
      // ignore empty lines
      if ($sEmail != '')
      {
        // verify email syntax using sfValidatorEmail
        // sfValidatorError exception will be thrown if invalid
        $oEmailValidator->clean($sEmail);
        $aEmails[] = $sEmail;
      }
    }

    return $aEmails;
  }
}

By the way, we could improve it by passing the validator (here "new sfValidatorEmail()") as an option of ValidatorEmailList, then we could validate lists of whatever actually... Well, you can do it!