Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Faking method attributes in PHP?

Is it possible to use the equivalent for .NET method attributes in PHP, or in some way simulate these?

Context

We have an in-house URL routing class that we like a lot. The way it works today is that we first have to register all the routes with a central route manager, like so:

$oRouteManager->RegisterRoute('admin/test/', array('CAdmin', 'SomeMethod'));
$oRouteManager->RegisterRoute('admin/foo/', array('CAdmin', 'SomeOtherMethod'));
$oRouteManager->RegisterRoute('test/', array('CTest', 'SomeMethod'));

Whenever a route is encountered, the callback method (in the cases above they are static class methods) is called. However, this separates the route from the method, at least in code.

I am looking for some method to put the route closer to the method, as you could have done in C#:

<Route Path="admin/test/">
public static void SomeMethod() { /* implementation */ }

My options as I see them now, are either to create some sort of phpDoc extension that allows me to something like this:

/**
 * @route admin/test/
 */
public static function SomeMethod() { /* implementation */ }

But that would require writing/reusing a parser for phpDoc, and will most likely be rather slow.

The other option would be to separate each route into it's own class, and have methods like the following:

class CAdminTest extends CRoute
{
    public static function Invoke() { /* implementation */ }
    public static function GetRoute() { return "admin/test/"; }
}

However, this would still require registering every single class, and there would be a great number of classes like this (not to mention the amount of extra code).

So what are my options here? What would be the best way to keep the route close to the method it invokes?

like image 206
Vegard Larsen Avatar asked Nov 30 '09 08:11

Vegard Larsen


2 Answers

This is how I ended up solving this. The article provided by Kevin was a huge help. By using ReflectionClass and ReflectionMethod::getDocComment, I can walk through the phpDoc comments very easily. A small regular expression finds any @route, and is registered to the method.

Reflection is not that quick (in our case, about 2,5 times as slow as having hard-coded calls to RegiserRoute in a separate function), and since we have a lot of routes, we had to cache the finished list of routes in Memcached, so reflection is unnecessary on every page load. In total we ended up going from taking 7ms to register the routes to 1,7ms on average when cached (reflection on every page load used 18ms on average.

The code to do this, which can be overridden in a subclass if you need manual registration, is as follows:

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));
            }
        }
    }
}

Thanks to everyone for pointing me in the right direction, there were lots of good suggestions here! We went with this approach simply because it allows us to keep the route close to the code it invokes:

class CSomeRoutable extends CRoutable
{
    /**
     * @route /foo/bar
     * @route /for/baz
     */
    public static function SomeRoute($SomeUnsafeParameter)
    {
        // this is accessible through two different routes
        echo (int)$SomeUnsafeParameter;
    }
}
like image 160
Vegard Larsen Avatar answered Sep 28 '22 09:09

Vegard Larsen


Using PHP 5.3, you could use closures or "Anonymous functions" to tie the code to the route.

For example:

<?php
class Router
{
    protected $routes;
    public function __construct(){
        $this->routes = array();
    }

    public function RegisterRoute($route, $callback) {
       $this->routes[$route] = $callback;
    }

    public function CallRoute($route)
    {
        if(array_key_exists($route, $this->routes)) {
            $this->routes[$route]();
        }
    }
}


$router = new Router();

$router->RegisterRoute('admin/test/', function() {
    echo "Somebody called the Admin Test thingie!";
});

$router->CallRoute('admin/test/');
// Outputs: Somebody called the Admin Test thingie!
?>
like image 33
Atli Avatar answered Sep 28 '22 11:09

Atli