Implementing metadata-based programming in PHP

Sunday December 27, 2009

After we implemented a URL routing system for PHP, we found that the spatial distance between the declaration of the route and the method was prohibitively large. The code we had was something like this, but with a lot more method declarations:

class CFooRoute extends CRoutable
{
    public static function RegisterRoutes($oRouteManager)
    {
        $oRouteManager->RegisterRoute('/test/route', array('CFooRoute', 'TestRoute'));
    }

    public static function TestRoute($aParam)
    {
         // glaring XSS bug on purpose
         echo "Foo is the new $aParam";
     }
}

After thinking about this for a while, I posted a question on Stack Overflow, and tried to find ways around this. My idea was to somehow parse phpDoc comments so that the code above could be turned into much more readable code:

class CFooRoute extends CRoutable
{
    /**
     * @route /test/route
     */
    public static function TestRoute($aParam)
    {
         // glaring XSS bug on purpose
         echo "Foo is the new $aParam";
     }
}

The problem in my mind was that parsing the PHP files simply to get at the comments would be a large overhead. Then someone pointed out an article from InterfaceLab regarding metadata-based attributes in PHP.

In short, you use PHP’s built-in reflection classes to look at the phpDoc-comment associated with each method. This made it possible for us to implement the exact syntax above, using simple reflection. We implemented the previously abstract CRoutable::RegisterRoutes() to automatically scan through the class for comments.

public static function RegisterRoutes()
{
    $sClass = get_called_class(); // unavailable in PHP < 5.3.0
    $rflClass = new ReflectionClass($sClass);
    foreach ($rflClass->getMethods() as $rflMethod)
    {
        $sComment = $rflMethod->getDocComment();
        if (preg_match_all('%^\s*\*\s*@route\s+(?P<route>/?(?:[a-z0-9]+/?)+)\s*$%im', 
            $sComment, $result, PREG_PATTERN_ORDER)) 
        {
            foreach ($result[1] as $sRoute)
            {
                $sMethod = $rflMethod->GetName();
                $oRouteManager->RegisterRoute($sRoute, array($sClass, $sMethod));
            }
        }
    }
}

While the code is fairly straightforward, there are a few comments:

  1. get_called_class() isn’t available in PHP versions less than 5.3.0. Since we are running 5.2, we had to implement a custom version of one found in the comments at PHP.net. This probably won’t work for anything else, so I’m not posting it here.
  2. The regular expression is kinda hairy, but should still be readable. Since ReflectionMethod::getDocComment() returns the comment as it is in code – including the spaces and asterisks at the beginning of lines – and looks for the route name. If no route is found for a method, it is simply ignored.
  3. This method supports having multiple routes for one method. Kinda handy if you need to support legacy URLs.

Before you go ahead implementing this, you should also know that there are some caveats to this method:

  1. You will probably still have to manually register the subclasses of CRoutable if you are using any form of autoloading. Otherwise the reflection would have to look at, and autoload, every class in your project to find the valid paths. Therefore we manually register all the classes with routes with our route manager.
  2. We ended up caching the list of routes in memcached. This was quite a bit faster than using reflection on every page load. Reflection in PHP really isn’t that slow, but for a service with some traffic this can be significant.
  3. We can still simply overload the RegisterRoutes()-method in a subclass if we want to control routes “manually”. We only do this in a few places, and in those cases we call parent::RegisterRoutes() to use the phpDoc-comments for the methods where that is possible.

At the moment we have no further use for metadata-based programming in PHP, but this has proven to us that this really is feasible in PHP, at least for this problem.

Comments

Commenting is closed for this article.