Подделка атрибутов метода в 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/"; }
}
Однако для этого все равно потребуется зарегистрировать каждый отдельный класс, и таких классов будет огромное количество (не говоря уже о количестве дополнительного кода).
Итак, каковы мои варианты здесь? Каков был бы наилучший способ сохранить маршрут близко к методу, который он вызывает?
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;
}
}
Используя 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!
?>
Вот метод, который может удовлетворить ваши потребности. Каждый класс, содержащий маршруты, должен реализовать интерфейс, а затем выполнить цикл по всем определенным классам, реализующим этот интерфейс, для сбора списка маршрутов. Интерфейс содержит один метод, который ожидает, что будет возвращен массив объектов 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));
}
}
}
Я бы использовал комбинацию интерфейсов и одноэлементный класс для регистрации маршрутов на лету.
Я бы использовал соглашение об именовании классов маршрутизаторов, таких как 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";
}
}
Ближе всего вы можете указать свой путь к определению функции (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() {}
}