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.

symfony forward through LAN

Scene from "Disney's Adventures of the Gummi Bears" by Jymn Magon & Art Vitello (introduced in 1985)

Ever thought of creating an action that would run different code depending on whether the client machine is inside or outside the LAN? It's really easy yet useful. I used it few times in one of my backend apps so far. For people outside the LAN, both links are hidden (use comparison below) and the redirected links are inaccessible. Thanks to it, company employees working from outside the office may not see inaccessible links. Below is the auxiliary method:

  /**
   * Checks if the client computer is inside the LAN with a
   * specific address. If so, redirects to a local address.
   *
   * @param String $ip - IP of the webpage to be forwarded to
   * @param String $port - port of the webpage to be forwarded to
   */
  protected function forwardThroughLan($ip, $port)
  {
    if ($_SERVER['REMOTE_ADDR'] == sfConfig::get('path_server_ip'))
      $this->redirect('http://'.$ip.':'.$port);
    else
      $this->setTemplate ('forwardThroughLanError');
  }
and here is direct usage:
  /**
   * Executes forward TRAC through LAN action. Forwards to TRAC webpage
   * inside LAN in the office building.
   *
   * @param sfWebRequest $request
   */
  public function executeForwardTracThroughLan(sfWebRequest $request)
  {
    $this->forwardThroughLan('192.168.1.99', '8080');
  }

SVN repository auto update

Scene from "Brazil" by Terry Gilliam (1985)

I found an article (in Polish) describing a subversion hook that will automatically update a server working copy. This seems perfect - after commiting a new revision, all changes are available online (entire development team can test the most up to date version, your boss may see the newest results, etc).

Anyway, I don't see any reason to use C here, so I've made my own version updating the repository using shell command:

svn up /home/website/domains/dev.website.com/website
  --no-auth-cache --username user --password pass
The --no-auth-cache option is important here - authorization data is not going to be remembered here. Other two options define the authorization data.

troubleshooting

You may have problems with file permissions, if the user which holds the repository is different than the user which hold the working copy (this was in my case). The problem I got was permission denied for .../.svn/lock file, which is a temporary file, existing only when the revision is being checked/committed. Oops... There is no one good solution for this - it depends on the server configuration. It can be adding one user to the group of another, changing the owner of the entire working copy files (no matter in which /home subdirectory it is, as long as web server can handle requests, etc).