Поддельные атрибуты метода в PHP? - PullRequest
12 голосов
/ 30 ноября 2009

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

Context

У нас есть собственный класс маршрутизации 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/"; }
}

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

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

Ответы [ 5 ]

10 голосов
/ 04 декабря 2009

Вот так я и решил эту проблему. Статья , предоставленная Кевином , очень помогла. Используя 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;
    }
}
5 голосов
/ 30 ноября 2009

Используя 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!
?>
2 голосов
/ 30 ноября 2009

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

Edit: я просто думал, что класс UrlRoute, вероятно, должен также содержать поле для ClassName. Тогда $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));  
        }
    }
}
1 голос
/ 30 ноября 2009

ближе всего вы можете указать путь к определению функции (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 голос
/ 30 ноября 2009

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

Я бы использовал соглашение о присвоении имен классам маршрутизаторов, таким как 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";
    }
}
...