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).

force to log message SVN python

Scene from "It Happened One Night" by Frank Capra (1934)

Another blog post about Subversion. Few days ago someone asked me if I know how to block commiting a subversion revision that has no log message defined. I didn't, but I thought it's a good idea, because it could help to track bugs, changes, etc. There's no such configuration, but it can be achieved using SVN hooks.

tried to do this in PHP...

There are many resources in the web giving examples to do this using a windows batch file - but I'm a linux user. At first, I wanted to do this in PHP. Although my final solution is not PHP, maybe someone will find following code snippets useful.

So I started searching through many pages on the web to find PHP shell scripts that could block committing a revision with no log message. The first step is, of course, to create an executable pre-commit file in the hooks directory of the repository. Then make the shell use the PHP parser to execute our script. Next, define path to svnlook binary and execute it:

#!/usr/local/bin/php -q
<?php
 
$SVNLOOK = '/usr/bin/svnlook';
exec("$SVNLOOK log -t {$argv[2]} {$argv[1]}", $output, $result);
We have the output and the result of the command available. So we may just check if the message (which revision a user is just trying to commit) is empty (I guess there is no other way than to use svnlook). If the log message is empty, the revision shall be stopped - and this happens, when exit status code is different than 0 (of course, a message explaining the cause is appreciated):
if (empty($result))
{
  fwrite(STDERR, "Commit blocked, need to pass the log message.");
  exit(1);
}
else
  exit(0);
This all should work, but 99,9% of all servers will have the exec function disabled by default (in both CLI and web server PHP configuration; along with other system functions):
disable_functions = exec,system,passthru,shell_exec,escapeshellarg,escapeshellcmd,proc_close,proc_open,dl,popen,show_source
Even if you manage to remove those functions from the configuration, you lower the server security, which may risk dangerous script injection. So I forgot about using PHP...

...and finally moved to python

Finally, I managed to succeed using python:

#!/usr/bin/python
 
import sys, os, string
 
SVNLOOK='/usr/bin/svnlook'
 
def main(repos, txn):
  log_cmd = '%s log -t "%s" "%s"' % (SVNLOOK, txn, repos)
  log_msg = os.popen(log_cmd, 'r').readline().rstrip('n')
  if len(log_msg) < 10:
    sys.stderr.write ("\n> Revision comment must be defined\n")
    sys.exit(1)
  else:
    sys.exit(0)
 
if __name__ == '__main__':
  if len(sys.argv) < 3:
    sys.stderr.write("\n> Usage: %s REPOS TXN\n" % (sys.argv[0]))
  else:
    main(sys.argv[1], sys.argv[2])