PHP伪造方法属性?

时间:2009-11-30 08:31:26

标签: php attributes methods url-routing decorator

是否可以在PHP中使用.NET方法属性的等价物,或以某种方式模拟这些?

上下文

我们有一个我们非常喜欢的内部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/"; }
}

然而,这仍然需要注册每一个类,并且会有很多这样的类(更不用说额外代码的数量)。

那么我的选择是什么?保持路线接近它调用的方法的最佳方法是什么?

6 个答案:

答案 0 :(得分:10)

这就是我最终解决这个问题的方法。 article provided by Kevin是一个巨大的帮助。通过使用ReflectionClass和ReflectionMethod :: getDocComment,我可以非常轻松地浏览phpDoc注释。一个小的正则表达式找到任何@route,并注册到该方法。

反射不是那么快(在我们的例子中,在单独的函数中对RegiserRoute进行硬编码调用的速度大约是2.5倍),并且因为我们有很多路由,所以我们必须缓存完成的列表Memcached中的路由,因此每次加载页面都不需要反射。总的来说,当缓存时,我们最终从7ms开始将路由注册到1,7ms(每页加载的反射平均使用18ms。

如果您需要手动注册,可以在子类中重写的代码如下:

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

答案 1 :(得分:5)

使用PHP 5.3,您可以使用闭包 "Anonymous functions" 将代码绑定到路由。

例如:

<?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 :(得分:2)

这是一种可以满足您需求的方法。每个包含路由的类必须实现一个接口,然后循环遍历所有定义的类,这些类实现该接口以收集路由列表。该接口包含一个方法,该方法需要返回一组UrlRoute对象。然后使用现有的URL路由类注册它们。

编辑:我只是想,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));  
        }
    }
}

答案 3 :(得分:1)

我会使用接口和单例类的组合来动态注册路由。

我会使用命名路由器类的约定,如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";
    }
}

答案 4 :(得分:1)

最接近你可以把你的路径放到函数定义(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() {}
}

答案 5 :(得分:0)

对此有一个建议,但被拒绝了。在此处查看RFC: Attributes RFC at php.net 我对这种愿望的解决方案是这样的:

abstract class MyAttributableBase{
  protected static $_methodAttributes=[];
  public static function getMethodAtributes(string $method):?array{
    if( isset(self::$_methodAttributes[$method])){
      return self::$_methodAttributes[$method];
    }
    return null;
  }

  protected static function setMethodAttributes(string $method, array $attrs):void{
    self::$_methodAttributes[$method] = $attrs;
  }
}

class MyController extends MyAttributableBase{
  protected static function getMethodAtributes(string $method):?array{
    switch( $method ){
      case 'myAction':
        return ['attrOne'=>'value1'];
      default:
        return parent::getMethodAttributes($method);
    }
  }
}

用法:

$c = new MyController();
print $c::getMethodAttributes('myAction')['attrOne'];

当然,在这种情况下,您当然可以从基类方法中使用它来进行“路由”操作,也可以在“ MyAttributableBase”对象上运行的路由类中使用它,或者在您要检查此附加元数据的其他任何地方使用它任何目的。与使用phpDoc相比,我更喜欢这种“代码内”解决方案。请注意,我没有尝试测试此确切的代码,但是它是从有效的解决方案中复制而来的。如果由于某些小原因而无法编译,则应该易于修复和使用。我还没有找到一种将属性干净地放在方法定义附近的方法。在基础中使用此实现,您可以将方法(在本例中为myAction)中的属性设置为要执行的第一个代码,但它不是静态属性,它将在每次调用时重置。您可以添加代码以另外确保仅设置一次,但这只是要执行的额外代码,可能并不更好。覆盖get方法可以使您一次设置信息并对其进行一次引用,即使该信息与方法定义不太接近也是如此。如果存在在运行时添加或更改元数据的情况,则将静态数组保留在基础中确实可以提供一定的灵活性。当创建第一个类来填充静态元数据数组时,可以使用类似phpDoc和静态构造函数的解析方法。我还没有找到很棒的解决方案,但是我使用的解决方案就足够了。