Атрибуты метода Faking в PHP?
Можно ли использовать эквивалент для атрибутов метода .NET в PHP или каким-то образом имитировать эти?
Контекст
У нас есть собственный класс маршрутизации URL-адресов, который нам очень нравится. То, как он работает сегодня, состоит в том, что мы сначала должны зарегистрировать все маршруты с помощью центрального диспетчера маршрутов, например:
$oRouteManager->RegisterRoute('admin/test/', array('CAdmin', 'SomeMethod'));
$oRouteManager->RegisterRoute('admin/foo/', array('CAdmin', 'SomeOtherMethod'));
$oRouteManager->RegisterRoute('test/', array('CTest', 'SomeMethod'));
Всякий раз, когда встречается маршрут, вызывается метод обратного вызова (в случаях, указанных выше, это статические методы класса). Однако это отделяет маршрут от метода, по крайней мере, в коде.
Я ищу некоторый метод, чтобы поместить маршрут ближе к методу, как вы могли бы сделать в С#:
<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/"; }
}
Однако это все равно потребует регистрации каждого отдельного класса, и было бы так много таких классов (не говоря уже о количестве дополнительного кода).
Итак, какие у меня варианты? Каким будет лучший способ сохранить маршрут близко к методу, который он вызывает?
Ответы
Ответ 1
Вот как я решил это решить. статья, предоставленная Кевином, была огромной помощью. Используя 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;
}
}
Ответ 2
Используя 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!
?>
Ответ 3
Вот способ, который может удовлетворить ваши потребности. Каждый класс, содержащий маршруты, должен реализовывать интерфейс, а затем более поздний цикл через все определенные классы, которые реализуют этот интерфейс для сбора списка маршрутов. Интерфейс содержит единственный метод, который ожидает возврата массива объектов 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));
}
}
}
Ответ 4
Я бы использовал комбинацию интерфейсов и одноэлементного класса для регистрации маршрутов на лету.
Я бы использовал соглашение об именах классов маршрутизаторов, таких как 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();
}
И это мое определение av 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;
}
}
Менеджер маршрутизатора
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";
}
}
Ответ 5
ближайший путь к определению функции (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() {}
}