PHPでメソッド属性を偽造しますか?
-
10-07-2019 - |
質問
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/"; }
}
ただし、これにはすべてのクラスを登録する必要があり、このようなクラスが多数あります(余分なコードの量は言うまでもありません)。
では、ここで私のオプションは何ですか?呼び出すメソッドの近くにルートを保持する最良の方法は何ですか?
解決
これが私がこれを解決する方法です。 Kevinが提供する記事は大きな助けになりました。 ReflectionClassとReflectionMethod :: getDocCommentを使用することで、phpDocコメントを非常に簡単に確認できます。小さな正規表現は、 @route
を検出し、メソッドに登録されます。
反射はそれほど速くありません(この場合、別の関数でRegiserRouteをハードコーディングした場合の約2.5倍遅い)。また、ルートがたくさんあるため、完成したリストをキャッシュする必要がありました。 Memcached内のルートなので、ページを読み込むたびに反映する必要はありません。合計で、キャッシュ時に平均で1,7msにルートを登録するのに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;
}
}
他のヒント
PHP 5.3を使用すると、 closures または &quot;匿名関数&quot;を使用できます。 でコードをルートに結び付けます。
例:
<?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-&gt; RegisterRoute($ urlRoute-&gt; route、array($ className、$ urlRoute-&gt; method))
は $ oRouteManager-&gt; 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() {}
}
and
$oRouteManager->RegisterRoute('admin/test/', array('CAdmin', 'SomeMethod'));
$oRouteManager->RegisterRoute('admin/foo/', array('CAdmin', 'SomeOtherMethod'));
class CAdmin {
public static function SomeMethod() {}
public static function SomeOtherMethod() {}
}