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:
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.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.Before you go ahead implementing this, you should also know that there are some caveats to this method:
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.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