Symfony World blog is not maintained anymore. Check new sys.exit() programming blog.

symfony range filter

In symfony/doctrine, only the Timestampable behavior provides ranged filters (a combination of from and to values that are queried just like MySQL BETWEEN statement). But how about ranged values for other data types?

I thought it'd be both cool and very useful, so I set off to search the web, but found no solution. Hence, I've made one myself. We will create three new classes and modify two more files to make it work in the basic version.

new widget/validator classes

The first class to be added is the form widget, lib/widget/sfWidgetFormInputRange.class.php:

class sfWidgetFormInputRange extends sfWidgetForm
{
  protected function configure($options = array(), $attributes = array())
  {
    $this->addRequiredOption('from_value');
    $this->addRequiredOption('to_value');
 
    $this->addOption('template', 'from %from_value% to %to_value%');
  }
 
  /**
   * Renders the widget.
   *
   * @param  string $name        The element name
   * @param  string $value       The value displayed in this widget
   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes
   * @param  array  $errors      An array of errors for the field
   *
   * @return string An HTML tag string
   *
   * @see sfWidgetForm
   */
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $value = array_merge(array('from' => '', 'to' => ''), is_array($value) ? $value : array());
 
    return strtr($this->translate($this->getOption('template')), array(
      '%from_value%' => $this->getOption('from_value')->render($name.'[from]', $value['from']),
      '%to_value%'   => $this->getOption('to_value')->render($name.'[to]', $value['to']),
    ));
  }
 
  /**
   * Gets the stylesheet paths associated with the widget.
   *
   * @return array An array of stylesheet paths
   */
  public function getStylesheets()
  {
    return array_unique(array_merge(
      $this->getOption('from_value')->getStylesheets(),
      $this->getOption('to_value')->getStylesheets()));
  }
 
  /**
   * Gets the JavaScript paths associated with the widget.
   *
   * @return array An array of JavaScript paths
   */
  public function getJavaScripts()
  {
    return array_unique(array_merge(
      $this->getOption('from_value')->getJavaScripts(),
      $this->getOption('to_value')->getJavaScripts()));
  }
}
The second one is the form filter widget, lib/widget/sfWidgetFormFilterInputRange.class.php:
class sfWidgetFormFilterInputRange extends sfWidgetFormInputRange
{
  /**
   * Configures the current widget.
   *
   * Available options:
   *
   *  * with_empty:      Whether to add the empty checkbox (true by default)
   *  * empty_label:     The label to use when using an empty checkbox
   *  * template:        The template used for from value and to value
   *                     Available placeholders: %from_value%, %to_value%
   *  * filter_template: The template to use to render the widget
   *                     Available placeholders: %value_range%, %empty_checkbox%, %empty_label%
   *
   * @param array $options     An array of options
   * @param array $attributes  An array of default HTML attributes
   *
   * @see sfWidgetForm
   */
  protected function configure($options = array(), $attributes = array())
  {
    parent::configure($options, $attributes);
 
    $this->addOption('with_empty', true);
    $this->addOption('empty_label', 'is empty');
    $this->addOption('template', 'from %from_value%<br />to %to_value%');
    $this->addOption('filter_template', '%value_range%<br />%empty_checkbox% %empty_label%');
  }
 
  /**
   * Renders the widget.
   *
   * @param  string $name        The element name
   * @param  string $value       The value displayed in this widget
   * @param  array  $attributes  An array of HTML attributes to be merged with the default HTML attributes
   * @param  array  $errors      An array of errors for the field
   *
   * @return string An HTML tag string
   *
   * @see sfWidgetForm
   */
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $values = array_merge(array('is_empty' => ''), is_array($value) ? $value : array());
 
    return strtr($this->getOption('filter_template'), array(
      '%value_range%'     => parent::render($name, $value, $attributes, $errors),
      '%empty_checkbox%' => $this->getOption('with_empty') ? $this->renderTag('input', array('type' => 'checkbox', 'name' => $name.'[is_empty]', 'checked' => $values['is_empty'] ? 'checked' : '')) : '',
      '%empty_label%'    => $this->getOption('with_empty') ? $this->renderContentTag('label', $this->translate($this->getOption('empty_label')), array('for' => $this->generateId($name.'[is_empty]'))) : '',
    ));
  }
}
Finally, the last class is the widget validator, lib/validator/sfValidatorInputRange.class.php:
class sfValidatorInputRange extends sfValidatorBase
{
  /**
   * Configures the current validator.
   *
   * Available options:
   *
   *  * from_value:   The from value validator (required)
   *  * to_value:     The to value validator (required)
   *  * from_field:  The name of the "from" value field (optional, default: from)
   *  * to_field:    The name of the "to" value field (optional, default: to)
   *
   * @param array $options    An array of options
   * @param array $messages   An array of error messages
   *
   * @see sfValidatorBase
   */
  protected function configure($options = array(), $messages = array())
  {
    $this->setMessage('invalid', 'The begin value must be before the end value.');
 
    $this->addRequiredOption('from_value');
    $this->addRequiredOption('to_value');
    $this->addOption('from_field', 'from');
    $this->addOption('to_field', 'to');
  }
 
  /**
   * @see sfValidatorBase
   */
  protected function doClean($value)
  {
    $fromField = $this->getOption('from_field');
    $toField   = $this->getOption('to_field');
 
    $value[$fromField] = $this->getOption('from_value')->clean(isset($value[$fromField]) ? array('text' => $value[$fromField]) : null);
    $value[$toField]   = $this->getOption('to_value')->clean(isset($value[$toField]) ? array('text' => $value[$toField]) : null);
 
    if ($value[$fromField]['text'] && $value[$toField]['text'])
    {
      $v = new sfValidatorSchemaCompare($fromField, sfValidatorSchemaCompare::LESS_THAN_EQUAL, $toField, array('throw_global_error' => true), array('invalid' => ('invalid')));
      $v->clean($value);
    }
 
    $value[$fromField] = $value[$fromField]['text'];
    $value[$toField] = $value[$toField]['text'];
 
    return $value;
  }
}

accustoming solution to a example project

For the purposes of this article, let's say we have an Offer model. We will add the offer.recommendations integer value filter in the offer admin module. The lib/filter/doctrine/OfferFormFilter.class.php shall be improved as below:
class OfferFormFilter extends BaseOfferFormFilter
{
  public function configure()
  {
    parent::configure();
 
    $this->setWidget('recommendations_range', new sfWidgetFormFilterInputRange(array(
      'from_value' => new sfWidgetFormInput(),
      'to_value' => new sfWidgetFormInput(),
      'with_empty' => false
    )));
 
    $this->setValidator('recommendations_range', new sfValidatorInputRange(array(
      'required' => false,
      'from_value' => new sfValidatorSchemaFilter('text', new sfValidatorNumber(array('required' => false))),
      'to_value' => new sfValidatorSchemaFilter('text', new sfValidatorNumber(array('required' => false)))
    )));
  }
 
  public function addRecommendationsRangeColumnQuery($query, $field, $value)
  {
    $rootAlias = $query->getRootAlias();
    if (isset($value['from']) && $value['from'])
      $query->andWhere($rootAlias.".recommendations >= ?", $value['from']);
    if (isset($value['to']) && $value['to'])
      $query->andWhere($rootAlias.".recommendations <= ?", $value['to']);
  }
}
Finally, add the filter inside the generator.yml file of the offer module:
  config:
    fields:
      recommendations_range:
        label: Liczba poleceń        
    filter:
      display: [ ..., recommendations_range]

result

Now you can use ranged filters anywhere in your project. Moreover, you can modify this mechanism to accustom it to your project needs.

5 comments:

  1. best tuto i have seen thank a lot :)

    ReplyDelete
  2. clean and straight forward. simply brilliant. thanks for this widget.

    ReplyDelete
  3. how to separate from_date and to_date in template (templatenameeditSuccess.php) ?

    Tanks

    ReplyDelete
    Replies
    1. I'm afraid I don't understand your question, but maybe taking a look at lib/widget/sfWidgetFormInputRange::render() function will help you with templating the range widget.

      Delete