Symfony Mixins – how to extend symfony core classes without inheritance

Symfony is very powerful PHP framework, one of the most popular in the PHP world. In big projects you should probably noticed, that there isn’t easy way to extend core classes, which aren’t returned by factories, for example actions classes. Every generated module has actions.class.php, which inherits from sfActions. What, if we want to have same method in few actions, let’s call it getCurrentDay to achieve effect listed bellow?

class defaultActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
    $year = $request->getParameter('year', date('Y'));
    $this->day   = $this->getCurrentDay($year);
    $this->month = $this->getCurrentMonth($year);
  }
}
Current day is <strong>Monday 2010</strong>
The month is <strong>February</strong>

We can do it, without changing parent class. Symfony provides the events mechanism and some lifesaving built-in events. Events, which are based on the observer pattern, are very simple: in one place you are notifying eventDispatcher with sfEvent instance, in config you connecting events with callbacks, and in callback you are processing event. Symfony core offers some built-in events, you can find them in the book: http://www.symfony-project.org/book/1_2/17-Extending-Symfony#chapter_17_sub_built_in_events. In our case, we need event “component.method_not_found” (sfActions inherits from sfComponent). We connecting this event in app config class (in configure method):

require_once(dirname(__FILE__). '/../../../lib/myMixins.class.php');
$this->dispatcher->connect('component.method_not_found', array(new myMixins(), 'listener'));

Please notice, that we must require mixin class here, because autoloader doesn’t works in this moment.
We create myMixins class in lib/, where we’ll extend actions:

/**
 * simple mixin class
 * this class is built with listener method and two mixin methods
 *
 * @package default
 * @author Wojciech Sznapka 
 * @license GPL
 */
class myMixins
{
  /**
   * invoker action instance
   *
   * @var sfActions
   * @access private
   */
  private $actionInstance = NULL;

  /**
   * simple listener, it checks whether method from method_not_found event exists in this class
   *
   * @param sfEvent $event
   * @access public
   * @return boolean
   */
  public function listener(sfEvent $event)
  {
    // check if not found method exists in this mixin
    if (method_exists($this, $event['method'])) {
      // store action instance, it's passed in the event subject
      $this->actionInstance = $event->getSubject();
      // call mixin method with given arguments
      $result = call_user_func_array(array($this, $event['method']), $event['arguments']);
      // set result to the event, so it's accessible in the action
      $event->setReturnValue($result);
      // stop notify chain
      return true;
    }
  }

  /**
   * gets current day with year; it also sets title of the page
   *
   * @param int $year
   * @access private
   * @return string
   */
  private function getCurrentDay($year)
  {
    $date = strftime('%A %Y', strtotime($year . date('-m-d')));
    $this->actionInstance->getResponse()->setTitle(sprintf("It's %s!", $date));
    return $date;
  }

  /**
   * gets current month
   *
   * @param int $year
   * @access private
   * @return string
   */
  private function getCurrentMonth($year)
  {
    return strftime('%B', strtotime($year . date('-m-d')));
  }
}

The listener checks if method_not_found is available in his mixin, if so, it calls it, set return value, and returns true, to stop notify chain (it is notifyUntill event). We can also access action instance ($this from defaultActions). This instance is passed as an event subject. We can, for example, set title in it, and it will be visible in the action.

With this simple technique symfony projects can be more flexible and readable, and you doesn’t have to repeat yourself in many places :-) You can also achieve mixins in other core objects: configuration, controller, response, request, user, view, by listening to thier method_not_found event.

Sources of an example project for this blog post is available on the GitHub for symfony 1.3: http://github.com/wowo/symfonyMixins

If you want a real life usage of mixins, you should look at the sfSslRequirementPlugin for symfony 1.2


Tagi: , , ,

Podobne wpisy

One Response to “Symfony Mixins – how to extend symfony core classes without inheritance”

  1. Centralniak Says:
    Marzec 11th, 2010 at 14:56

    Dzięki, pomogło mi sporo w osadzaniu tych samych walidatorów w kilku podobnych formularzach.

    Zastanawiam się trochę nad zasadnością przyłączania Mixina już w ApplicationConfiguration. Niby nie trzeba tego potem robić w każdej klasie z osobna, ale tak jak piszesz, na tym etapie wykonywaniu kodu autoloader Sf jeszcze nie funkcjonuje i musimy użyć require/include. Ja osobiście zdecydowałem się na podpięcie mixina znacznie wyżej, w klasie formularza w configure(). Jest to chyba dość naturalne, że deklarujemy Mixin tylko dla klas do którego rzeczywiście chcemy go dołączyć?

    P.S.:

    “Please notice, that we must require mixin class here, because autoloader doesn’t works in this moment.”

    Dla mnie jest to trochę mylące; czy oznacza to że autoloader nie działa w obecnej (1.4.3) wersji Sf, czy że nie działa w tym momencie wykonywania kodu bo nie został jeszcze wykonany kod z ProjectConfiguration ;)

Skomentuj