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

svn blame: who made the bug?

Scene from "12 Angry Men" by Sidney Lumet (1957)

In bigger projects it happens quite often that an unknown team member has made a modification that caused a lot of trouble. And no one wants to confess... It is quite uncomfortable to analyse distinct revisions and changes of a file to find who caused the problem, as it is time consuming. This is where svn blame is the perfect tool - you execute it upon a chosen target to display revision number, team member and content line by line. You can easily find who is the author of a bug and when he committed it. It's intereting, that despite being very useful, this tool is not very known.

blame me if you can find me quickly!

The usage is very simple:

tducin@workshop:/var/www/admin/config/doctrine$ svn blame schema.yml
and you have the entire file analysed:
   367     dennis connection: master
   362     tducin
   362     tducin options:
   362     tducin   charset: utf8
   362     tducin   collate: utf8_general_ci
   362     tducin
  3755     george Status:
  3755     george   comment: "Statusy"
   491     dennis   actAs:
  1980     dennis     SoftDelete: ~
  1793     harold     Timestampable: ~
  1793     harold     Signable: ~
   492     tducin   columns:
   492     tducin     name:
  3755     george       type: string(64)
   492     tducin       notnull: true
  2628     tducin       comment: "nazwa"
  3755     george     notify_mail:
  3755     george       type: boolean
  3755     george       notnull: true
  3755     george       default: false
  3755     george       comment: "klient ma być powiadamiany mailowo"
  1541     dennis     stock_mode:
  1541     dennis       type: enum
  1541     dennis       values: [ reserved, sent, cancelled ]
  1541     dennis       default: reserved
  2628     dennis       comment: "tryb magazynowy: reserved, sent, cancelled"
  3755     george     force_reminder:
   892     tducin       type: boolean
   878     tducin       notnull: true
  3755     george       comment: "Wymuszenie dodania wpisu w kalendarzu"
  3755     george     block_update:
  3755     george       type: boolean
  3755     george       notnull: true
  3755     george       comment: "Blokada zmiany statusu"
  4078     harold   indexes:
  4078     harold     softdelete_idx:
  4078     harold       fields: [ deleted_at ]

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');
  }