views:

175

answers:

5

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?

+4  A: 

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!
?>
Atli
I can't wait until I get to use this feature in production, but right now most of the time I get to work with shared hostings that only support versions 5.1.x or 5.2.x.
Igor Zinov'yev
Anonymous functions are very interesting, but what I am worried about is readability. Imagine having 50 of these routes registered after each other. However, this is absolutely the kind of answer we are looking for, and I like the approach. :)
Vegard Larsen
Short of parsing phpDoc leading a function or evaluating code from a XML file, I can't see a pure PHP method that is clearly readable; without any extra garbage to read through. You could always make the router static and wrap the `Router::RegisterRoute` call into a stand-alone `route()` function. Should make it a little more readable. Probably even more so than your C# example :-)
Atli
+1  A: 

I'd use a combination of interfaces and a singleton class to register routes on the fly.

I would use a convention of naming the router classes like FirstRouter, SecondRouter and so on. This would enable this to work:

foreach (get_declared_classes() as $class) {
    if (preg_match('/Router$/',$class)) {
 new $class;
    }
}

That would register all declared classes with my router manager.

This is the code to call the route method

$rm = routemgr::getInstance()->route('test/test');

A router method would look like this

static public function testRoute() {
if (self::$register) {
    return 'test/test'; // path
}
echo "testRoute\n";
}

The interfaces

interface getroutes {
    public function getRoutes();
}

interface router extends getroutes {
    public function route($path);
    public function match($path);
}

interface routes {
    public function getPath();
    public function getMethod();
}

And this is my definition av a route

class route implements routes {
    public function getPath() {
    return $this->path;
    }
    public function setPath($path) {
    $this->path = $path;
    }
    public function getMethod() {
    return $this->method;
    }
    public function setMethod($class,$method) {
    $this->method = array($class,$method);
    return $this;
    }
    public function __construct($path,$method) {
    $this->path = $path;
    $this->method = $method;
    }
}

The Router manager

class routemgr implements router {
    private $routes;
    static private $instance;
    private function __construct() {
    }
    static public function getInstance() {
    if (!(self::$instance instanceof routemgr)) {
        self::$instance = new routemgr();
    }
    return self::$instance;
    }
    public function addRoute($object) {
    $this->routes[] = $object;
    }
    public function route($path) {
    foreach ($this->routes as $router) {
        if ($router->match($path)) {
     $router->route($path);
        }
    }
    }
    public function match($path) {
    foreach ($this->routes as $router) {
        if ($router->match($path)) {
     return true;
        }
    }
    }
    public function getRoutes() {
    foreach ($this->routes as $router) {
        foreach ($router->getRoutes() as $route) {
     $total[] = $route;
        }
    }
    return $total;
    }
}

And the self register super class

class selfregister implements router {
    private $routes;
    static protected $register = true;
    public function getRoutes() {
    return $this->routes;
    }
    public function __construct() {
    self::$register = true;
    foreach (get_class_methods(get_class($this)) as $name) {
        if (preg_match('/Route$/',$name)) {
     $path = call_user_method($name, $this);
     if ($path) {
         $this->routes[] = new route($path,array(get_class($this),$name));
     }
        }
    }
    self::$register = false;
    routemgr::getInstance()->addRoute($this);
    }
    public function route($path) {
    foreach ($this->routes as $route) {
        if ($route->getPath() == $path) {
     call_user_func($route->getMethod());
        }
    }
    }
    public function match($path) {
    foreach ($this->routes as $route) {
        if ($route->getPath() == $path) {
     return true;
        }
    }
    }
}

And finally the self registering router class

class aRouter extends selfregister {
    static public function testRoute() {
    if (self::$register) {
        return 'test/test';
    }
    echo "testRoute\n";
    }
    static public function test2Route() {
    if (self::$register) {
        return 'test2/test';
    }
    echo "test2Route\n";
    }
}
Peter Lindqvist
I like your use of the `self::$register` variable, that can be used during registration. However, having the method itself do two separate things isn't really desirable, and could reduce the readability of the code. Still, this does exactly what I asked for; keep the route close to the method implementation...
Vegard Larsen
Well if you want to you could just override getRoutes and return an array that you create manually. This keeps the route definition within the same class albeit not within the function. And i'm sure there are other possibilites as well.
Peter Lindqvist
+1  A: 

the closest you can put your path to the function definition (IMHO) is right before the class definition. so you would have

$oRouteManager->RegisterRoute('test/', array('CTest', 'SomeMethod'));
class CTest {
    public static function SomeMethod() {}
}

and

$oRouteManager->RegisterRoute('admin/test/', array('CAdmin', 'SomeMethod'));
$oRouteManager->RegisterRoute('admin/foo/', array('CAdmin', 'SomeOtherMethod'));
class CAdmin {
    public static function SomeMethod() {}
    public static function SomeOtherMethod() {}
}
jab11
...this is very close to what I am doing at the moment. However, consider that there might be 50 routes there, and some of the methods are 10-20 lines. The distance is then a bit too long, according to my definition...
Vegard Larsen
well you want some code right before the function definition which would register that function to some url. but since that's part of the class there's no way to execute that code. so you can either do this, or some registerRoutes() function inside every class which is really uncomfortable, or just use paths as /class/function - this way you don't have to register anything just apply some validation to check which functions can be called directly like this.
jab11
+2  A: 

Here's a method which may suit your needs. Each class that contains routes must implement an interface and then later loop through all defined classes which implement that interface to collect a list of routes. The interface contains a single method which expects an array of UrlRoute objects to be returned. These are then registered using your existing URL routing class.

Edit: I was just thinking, the UrlRoute class should probably also contain a field for ClassName. Then $oRouteManager->RegisterRoute($urlRoute->route, array($className, $urlRoute->method)) could be simplified to $oRouteManager->RegisterRoute($urlRoute). However, this would require a change to your existing framework...

interface IUrlRoute
{
    public static function GetRoutes();
}

class UrlRoute
{
    var $route;
    var $method;

    public function __construct($route, $method)
    {
     $this->route = $route;
     $this->method = $method;
    }
}

class Page1 implements IUrlRoute
{
    public static function GetRoutes()
    {
     return array(
      new UrlRoute('page1/test/', 'test')
     );
    }

    public function test()
    {
    }
}

class Page2 implements IUrlRoute
{
    public static function GetRoutes()
    {
     return array(
      new UrlRoute('page2/someroute/', 'test3'),
      new UrlRoute('page2/anotherpage/', 'anotherpage')
     );
    }

    public function test3()
    {
    }

    public function anotherpage()
    {
    }
}

$classes = get_declared_classes();
foreach($classes as $className)
{
    $c = new ReflectionClass($className);
    if( $c->implementsInterface('IUrlRoute') )
    {
     $fnRoute = $c->getMethod('GetRoutes');
     $listRoutes = $fnRoute->invoke(null);

     foreach($listRoutes as $urlRoute)
     {
      $oRouteManager->RegisterRoute($urlRoute->route, array($className, $urlRoute->method)); 
     }
    }
}
Kevin
This is very close to what we have currently. The "problem" is as before, that the actual route can be way separated from the implementation of the code, and the readability suffers. It is good to see that someone else thought similar to what we did, though... :)
Vegard Larsen
Haha, oh well. The other thought I had was using something like AttributeReader (http://interfacelab.com/metadataattributes-in-php/) and create a command line script to cache all routes to a single file. May strike the balance between speed and readability, though you have to run the script whenever a route changes...
Kevin
The article you linked to is awesome, if I had found that myself I might not have posed the question. Depending on how this performs, I might just end up using this to parse phpDoc-comments as outlined in my question, and find some way of caching this so I don't have to use reflection on every page load. Great find! :)
Vegard Larsen
+3  A: 

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 wen't 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;
    }
}
Vegard Larsen
Very cool. You got me interested in doing something similar to this for future projects.
Kevin