Symfony Mixins – how to extend symfony core classes without inheritance
2010-02-02Symfony 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 don’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