Posts Tagged ‘sfWidgetFormSchemaFormatter’

rto: Auto generating decorator classes with sfDecoratorPlugin (or: how to easily apply the decorator pattern to Symfony core classes)

Thursday, December 17th, 2009

General OO practise is to extend classes to implement new behaviour. But sometimes that’s just not possible, for instance when you want to change how a class behaves during runtime. One of the solutions in such a case is to code a decorator (see wikipedia).

Here is a short example of a decorator implemented in PHP, as a small reminder. (If you want to understand more about the decorator pattern I recommend you to read the wikipedia pages on the topic). It’s a very basic text generator for a magazine company. Sometimes the editor of the magazine wants texts to be printed in capital letters. At such a time a decorator comes in handy:

class Text
{
  protected $text = "TestText";
 
  public function get()
  {
    return $this->text;
  }
}
 
// An extension for more specific texts
class Article extends Text
{
  protected $text = "ArticleText";
}
 
// This is the actual decorator. It decorates the 
// Text class to only print in upper case
class UpperCaseText extends Text
{
  protected $text = null;
 
  public function __construct(Text $text)
  {
    $this->text = $text;
  }
 
  public function get()
  {
    return strtoupper($this->text->get());
  }
}
 
$text = new Text();
// $text->get() == "TestText"
$upText = new UpperCaseText($text);
// $upText->get() == "TESTTEXT"
 
$article = new Article();
// $article->get() == "ArticleText"
$upArticle = new UpperCaseText($article);
// $upArticle->get() == "ARTICLETEXT"

I recently used the decorator pattern to decorate the sfWidgetFormSchemaFormatter class of the symfony framework. This class is responsible for formatting forms and I wanted to change the behaviour of the ‘->generateLabel()’ method. Using a decorator modified the behaviour of the method so that it doesn’t output a label (like ‘<label>—the label—</label>’) but it outputs an input for the label (like ‘<input type=”text”>—the label—</input>’).

There’s one big problem though when coding a decorator: you need to forward the full public api of a class. When decorating core classes of a framework, this ends up to be really hard and not easily maintainable. Take for instance the sfWidgetFormSchemaFormatter class which has 27 public methods, a lot of work and very error prone!

Luckily the reflection library of PHP comes to help here. As the Reflectionclass::getMethods() is able to produce an array of all methods inside a class, it’s possible to write a decorator class by script. This is exactly what I did and is now available as a symfony plugin called sfDecoratorPlugin.

The plugin gives you a cli-task to generate a decorator class in your lib-dir and (more important) adds an autoloader which generates decorators on the fly. The autoloader watches for classes being instantiated which fit the ‘<<class to decorate>>Decorator’ string pattern. When it finds such a class, it analyses the class you want to decorate and writes a full decorator class to a cache PHP file. This allows you to easily decorate even the most complex core classes.
As a final example a snippet of my decorator to the sfWidgetFormSchemaFormatter  class:

class sfWidgetFormSchemaFormatterDecoratorArray extends sfWidgetFormSchemaFormatterDecorator
{
  public function generateLabel($name, $attributes = array())
  {
    $labelName = parent::generateLabelName();
    $input = new sfWidgetFormInputText();
    return $input->render('_key['.$name.']', $labelName, array(), $attributes);
  }
}

The autoloader of sfDecoratorPlugin recognised that extending the sfWidgetFormSchemaFormatterDecorator class meant that it had to produce a decorator. In the cache the sfWidgetFormSchemaFormatterDecorator.class.php file now contains:

/**
 * Decorator to class sfWidgetFormSchemaFormatter, automatically generated by sfDecorator
*/
abstract class sfWidgetFormSchemaFormatterDecorator extends sfWidgetFormSchemaFormatter
{
  protected
    $object = null;
 
  function __construct(sfWidgetFormSchemaFormatter $object)
  {
    $this->object = $object;
  }
 
  /** forwards to class sfWidgetFormSchemaFormatter (autocode by generate:decorator) */
  public function translate($subject, $parameters = array())
  {
    $result = $this->object->translate($subject, $parameters);
    return $result===$this->object ? $this : $result;
  }
 
  /** forwards to class sfWidgetFormSchemaFormatter (autocode by generate:decorator) */
  public function generateLabel($name, $attributes = array())
  {
    $result = $this->object->generateLabel($name, $attributes);
    return $result===$this->object ? $this : $result;
  }
 
  /** forwards to class sfWidgetFormSchemaFormatter (autocode by generate:decorator) */
  public function generateLabelName($name)
  {
    $result = $this->object->generateLabelName($name);
    return $result===$this->object ? $this : $result;
  }
 
  // ... AND SO ON for all 27 methods...
 
  function __clone()
  {
    $this->object = clone($this->object);
  }
}

Good luck decorating yourself! Thanks for reading my post.