Подделка атрибутов метода в PHP?


Можно ли использовать эквивалент атрибутов метода .NET в PHP или каким-то образом имитировать их?

Контекст

У нас есть собственный класс маршрутизации URL-адресов, который нам очень нравится. Сегодня это работает так, что сначала мы должны зарегистрировать все маршруты в центральном менеджере маршрутов, например:

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

Всякий раз, когда встречается маршрут, вызывается метод обратного вызова (в приведенных выше случаях это методы статического класса). Однако это отделяет маршрут от метод, по крайней мере, в коде.

Я ищу какой-нибудь метод, чтобы приблизить маршрут к методу, как вы могли бы сделать в C#:

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

Мои варианты, как я их вижу сейчас, заключаются в том, чтобы либо создать какое-то расширение PHPDoc, которое позволит мне сделать что-то вроде этого:

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

Но это потребует написания/повторного использования синтаксического анализатора для PHPDoc и, скорее всего, будет довольно медленным.

Другим вариантом было бы разделить каждый маршрут на свой собственный класс и использовать такие методы, как следующее:

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

Однако для этого все равно потребуется зарегистрировать каждый отдельный класс, и таких классов будет огромное количество (не говоря уже о количестве дополнительного кода).

Итак, каковы мои варианты здесь? Каков был бы наилучший способ сохранить маршрут близко к методу, который он вызывает?

Author: Vegard Larsen, 2009-11-30

5 answers

Вот как я в итоге решил эту проблему. Статья , предоставленная Кевином, оказала огромную помощь. Используя ReflectionClass и ReflectionMethod::getDocComment, я могу очень легко просматривать комментарии PHPDoc. Небольшое регулярное выражение находит любое @route и регистрируется в методе.

Отражение происходит не так быстро (в нашем случае примерно в 2,5 раза медленнее, чем при жестко запрограммированных вызовах RegiserRoute в отдельной функции), и, поскольку у нас много маршрутов, нам пришлось кэшировать готовый список маршрутов в Memcached, поэтому отражение не требуется при каждой загрузке страницы. В общей сложности мы закончили тем, что потратили 7 мс на регистрацию маршрутов в среднем до 1,7 мс при кэшировании (отражение каждой загрузки страницы в среднем занимало 18 мс.

Код для этого, который можно переопределить в подклассе, если вам нужна регистрация вручную, выглядит следующим образом:

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

Спасибо всем за то, что указали мне правильное направление, здесь было много хороших предложений! Мы пошли с этим подходите просто потому, что это позволяет нам держать маршрут близко к коду, который он вызывает:

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;
    }
}
 10
Author: Vegard Larsen, 2014-03-05 08:53:37

Используя PHP 5.3, вы можете использовать замыкания или " Анонимные функции" чтобы привязать код к маршруту.

Например:

<?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!
?>
 5
Author: Atli, 2009-11-30 09:14:45

Вот метод, который может удовлетворить ваши потребности. Каждый класс, содержащий маршруты, должен реализовать интерфейс, а затем выполнить цикл по всем определенным классам, реализующим этот интерфейс, для сбора списка маршрутов. Интерфейс содержит один метод, который ожидает, что будет возвращен массив объектов UrlRoute. Затем они регистрируются с использованием существующего класса маршрутизации URL-адресов.

Редактировать: Я просто подумал, что класс UrlRoute, вероятно, также должен содержать поле для имени класса. Тогда $oRouteManager->RegisterRoute($urlRoute->route, array($className, $urlRoute->method)) можно было бы упростить до $oRouteManager->RegisterRoute($urlRoute). Однако для этого потребуется внести изменения в существующую структуру...

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));  
        }
    }
}
 2
Author: Kevin, 2009-11-30 16:55:55

Я бы использовал комбинацию интерфейсов и одноэлементный класс для регистрации маршрутов на лету.

Я бы использовал соглашение об именовании классов маршрутизаторов, таких как FirstRouter, SecondRouter и так далее. Это позволило бы этому работать:

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

Это зарегистрировало бы все объявленные классы в моем менеджере маршрутизаторов.

Это код для вызова метода маршрута

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

Метод маршрутизатора будет выглядеть следующим образом

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

Интерфейсы

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

И это является ли мое определение маршрутом

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

Менеджер маршрутизатора

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

И суперкласс саморегистрации

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

И, наконец, класс маршрутизатора с саморегистрацией

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";
    }
}
 1
Author: Peter Lindqvist, 2009-11-30 10:16:10

Ближе всего вы можете указать свой путь к определению функции (IMHO) прямо перед определением класса. таким образом, у вас было бы

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

И

$oRouteManager->RegisterRoute('admin/test/', array('CAdmin', 'SomeMethod'));
$oRouteManager->RegisterRoute('admin/foo/', array('CAdmin', 'SomeOtherMethod'));
class CAdmin {
    public static function SomeMethod() {}
    public static function SomeOtherMethod() {}
}
 1
Author: jab11, 2009-11-30 11:05:36