Extending core class with sfSympalExtendClass

Posted by weaverryan on March 29, 2010

This week has been a busy one with bug fixes and major steps towards the separation of Sympal into well-defined components. This has meant that things are being moved around and leaned up. For a good review, see our coverage in A week of symfony #169 (22->28 March 2010).

Dependency injection in Sympal? Yes! I'm excited to report that Sympal is implementing symfony's dependency injection component. I'll talk in detail about the new service container (94c1a08, 821971c, b63a695) next week and introduce the first of the Sympal components.

This post is an introduction to symfony's method_not_found event type (which is not new) and how Sympal's sfSympalExtendClass eases its use.

The problem of "extending" core classes

One of the most important requirements when designing any reusable library is that it be extendable and configurable without modifying the library itself. Using dependency injection and a service container goes a long way towards accomplishing this goal by allowing you to override core classes and their constructor arguments.

But in an environment with many plugins (like in Sympal), this methodology can fall short. Suppose, for example, that two plugins, sfAlphaPlugin and sfBetaPlugin, both need to add methods to symfony's core request factory (sfWebRequest). To satisfy the requirements of sfAlphaPlugin, you could modify your factories.yml file to use its request class, sfRequestAlpha, which extends sfWebRequest:

---
request:
  class:        sfRequestAlpha

Symfony will now use sfRequestAlpha as it's request object - allowing it to add any additional methods it needs.

But what about plugin sfBetaPlugin? We can't simply replace sfRequestAlpha with sfRequestBeta - this would remove any added functionality from sfRequestAlpha. What can we do?

Enter symfony's "method_not_found" events

In symfony, many of the core classes introduce a "method_not_found" event, which effectively allows multiple "subjects" to add methods to that core class? It's not magic, but rather an intelligent design pattern implemented by these core classes. Here's how it works.

Image we have some class, say, sfSympalConfiguration. This could be any class, but as I've actually just implemented this strategy into sfSympalConfiguration, it serves as a good example.

<?php
 
class sfSympalConfiguration
{
  // ... Lot's of methods
 
  public function __call($method, $arguments)
  {
    $event = $this->_dispatcher->notifyUntil(new sfEvent(
      $this,
      'sympal.configuration.method_not_found',
      array('method' => $method, 'arguments' => $arguments)
    ));
 
    if (!$event->isProcessed())
    {
      throw new sfException(sprintf(
        'Call to undefined method %s::%s.',
        get_class($this),
        $method
      ));
    }
 
    return $event->getReturnValue();
  }
}
 
?>

What does this accomplish? By using the magic call method, we "catch" any unknown methods called on the object. Then, by notifying the sympal.configuration.method_not_found event, we're essentially asking if anyone else is available to process this method before we throw an exception.

As a concrete example, let's show how we can now add methods to sfSympalConfiguration. First, we'll introduce a new class, sfSympalCustomConfiguration. This class will "add" myCustomMethod() to sfSympalConfiguration. Notice that this class doesn't extend sfSympalConfiguration - it could extend any class, be completely static or not even be a class at all (just a callback).

<?php
 
class sfSympalCustomConfiguration
{
  public function myCustomMethod(sfSympalConfiguration $configuration, $arguments)
  {
    // do something custom
  }
}
 
?>

But, we still haven't told sfSympalConfiguration about our new method. To do this, we need to register a "listener" on the sympal.configuration.method_not_found event. This is generally done in either an application configuration (e.g. appNameConfiguration) or a plugin configuration (e.g. myPluginConfiguration). In this example, we'll setup the functionality in sfAlphaPluginConfiguration since the functionality needs to be added by that plugin:

<?php
 
class sfAlphaPluginConfiguration extends sfPluginConfiguration
{
  // called automatically when the plugin is loaded
  public function initialize()
  {
    $this->dispatcher->connect(
      'sympal.configuration.method_not_found',
      array($this, 'listenConfigurationMethodNotFound')
    )
  }
 
  // listens to the event, decides if we're able to process the called method
  public function listenConfigurationMethodNotFound(sfEvent $event)
  {
    if ($event['method'] == 'myCustomMethod')
    {
      $configuration = new sfSympalCustomConfiguration();
      $result = $configuration->myCustomMethod(
        $event->getSubject(),
        $event['arguments']
      );
      $event->setReturnValue($result);
 
      return true;
    }
 
    return false;
  }
}
 
?>

The first method, initialize, tells the event dispatcher to notify listenConfigurationMethodNotFound whenever an unknown method is called on sfSympalConfiguration. If the method being called is myCustomMethod, we pass it along to sfSympalCustomConfiguration and return true. Returning true tells the dispatcher that the event has been processed. If we haven't processed the event, we return false, which tells the dispatcher to continue asking any other listeners if they can handle the unknown method.

The end goal is that we can call myCustomMethod on sfSympalConfiguration as if that method actually existed in the class:

<?php
 
// $configuration is of type sfSympalConfiguration
$configuration->myCustomMethod('arg1', 'arg2');
 
?>

Making things easier in Sympal

The above strategy is powerful, but it has a few practical drawbacks:

  • It requires a lot of code to add one method to a class
  • The myCustomMethod is called with unnatural arguments. Instead of passing it the exact arguments passed to the original method, we pass it an sfSympalConfiguration instance and an array of the arguments. If we didn't pass it the sfSympalConfiguration instance, then the method wouldn't have access to the class that it's supposedly extending.

To make things simpler, Sympal uses a utility class called sfSympalExtendClass class. This class exists solely to eliminate the need for the boiler-plate code needed when extending a class via a "method_not_found" event. It doesn't add any functionality - it just makes things easier and centralizes logic to be less error-prone.

Making a class extendable in Sympal

First, let's use sfSympalExtendClass to allow a class to be extended:

<?php
 
class sfSympalConfiguration
{
  // ... Lot's of methods
 
  public function __call($method, $arguments)
  {
    return sfSympalExtendClass::extendEvent($this, $method, $arguments);
  }
}
 
?>

That was easy! This will throw a "method_not_found" an event whose name is based on the "tableized" name of your class. In this case, the event will be named sf_sympal_configuration.method_not_found. If a listener is found that can handle this event, its value will be returned. Otherwise, a "method not found" exception message will be thrown.

Extending a class in Sympal

While this is great, most of the time you'll be more interested in the other side of the transaction. Namely, how can I easily add methods to a class (e.g. sfSympalConfiguration)? First, we add a listener on the method_not_found event:

<?php
 
class sfAlphaPluginConfiguration extends sfPluginConfiguration
{
  public function initialize()
  {
    $configuration = new sfSympalCustomConfiguration();
    $this->dispatcher->connect(
      'sf_sympal_configuration.method_not_found',
      array($configuration, 'extend')
    )
  }
}
 
?>

This setup will always be the same: create an instance of your new object, then connect the appropriate "method_not_found" event to the extend method of the object.

This works because your class should new extend sfSympalExtendClass, which supplies an extend method containing the requisite boilerplate "listener" code. The sfSympalCustomConfiguration class itself is now much more natural:

<?php
 
class sfSympalCustomConfiguration extends sfSympalExtendClass
{
  public function myCustomMethod($arg1, $arg2)
  {
    // do something
    // $this->_subject is the original sfSympalConfiguration instance
  }
}
 
?>

This has several advantages over the initial implementation:

  • myCustomMethod() receives the true arguments that were called on the method. It can be used naturally - as if it were really an extension of sfSympalConfiguration. The only difference is that the sfSympalConfiguration instance is available via $this->_subject instead of simply $this.
  • If you want to add another method to sfSympalConfiguration, you need only create another public function inside sfSympalCustomConfiguration. All public functions inside sfSympalCustomConfiguration automatically act like extensions of the original class (sfSympalConfiguration).

The good and bad of extending classes

Fortunately, symfony employs its event dispatcher and the method_not_found event on several core classes to allow you to add methods to these classes. Sympal has introduced this same pattern to sfSympalPluginConfiguration b63a695.

While this is great, it only allow you to add NEW methods. If you'd like to override an existing method, this pattern will not work. In those case, your best hope is that the library was designed in such a way that you can achieve your goal without being too obtrusive. The service container helps: core objects can be subclassed and their dependencies can be equally configured. If this still doesn't help, it's time to revisit your strategy (why is it so demanding?) or start getting creative.

Comments (512) Add a comment


Markdown syntax is enabled.