Подделка атрибутов метода в PHP?
-
10-07-2019 - |
Вопрос
Можно ли использовать эквивалент для атрибутов метода .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/"; }
}
Однако для этого все равно потребовалась бы регистрация каждого отдельного класса, и было бы огромное количество подобных классов (не говоря уже о количестве дополнительного кода).
Итак, какие у меня есть варианты здесь?Каков был бы наилучший способ сохранить маршрут близким к методу, который он вызывает?
Решение
Вот как я в конечном итоге решил эту проблему.В статья предоставлена Кевином это была огромная помощь.Используя 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, вероятно, также должен содержать поле для 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));
}
}
}
Я бы использовал комбинацию интерфейсов и одноэлементного класса для регистрации маршрутов "на лету".
Я бы использовал соглашение об именовании классов маршрутизаторов, таких как FirstRouter, SecondRouter и так далее.Это позволило бы этому сработать:
foreach (get_declared_classes() as $class) {
if (preg_match('/Router$/',$class)) {
new $class;
}
}
Это позволило бы зарегистрировать все объявленные классы в моем диспетчере маршрутизаторов.
Это код для вызова метода route
$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";
}
}
самое близкое, что вы можете указать своим путем к определению функции (ИМХО), находится прямо перед определением класса.таким образом, вы бы
$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() {}
}