Rolf B: Was gibt es beim Schreiben eines PHP-Routers zu beachten?

Beitrag lesen

Hallo borisbaer,

ja, ungefähr.

function addRouteByAttribute( array $controllers )

Das Problem bei PHP ist, dass man Klassen zumeist on-demand lädt, mit einem Autoloader. Wenn Deine PHP Anwendung 17 Controller hat, und Du nur einen davon brauchst, ist es Zeitverschwendung, alle 17 Controller zu laden und darin nach Routendeklarationen zu suchen.

Vor allem ist es auch oft so, dass eine Routendeklaration mehr als einen Controller ermitteln kann. Eine Generalroute wie /{controller}/{action} kann man nicht in einem einzelnen Controller per Attribut festlegen.

Deswegen findet die Deklaration der eigentlichen Routen besser ohne Reflection auf die Controller statt, so dass Du Controller- und Action-Name weißt, ohne etwas zu reflektieren.

Sobald dein Router den Controller kennt, kann er ihn laden (was beim new ReflectionClass automatisch passieren dürfte) und über die Methoden reflektieren, um zu schauen, welche davon zur gefundenen Action passt.

Aber: Die Controllerklassen sollten auf jeden Fall eine Markierung tragen, dass es sich um Controller handelt. Entweder müssen sie in einem speziell für Controller definierten Namespace stehen, oder Du gibst den Controllerklassen ein Klassen-Attribut "Controller" und prüfst, ob dieses Attribut vorhanden ist, bevor der Router eine Klasse als Controller akzeptiert. Keinesfalls darf der Router beliebige Klassennamen als Controller akzeptieren.

Wenn Du einen gültigen Controller hast, kannst Du darin die Methoden inspizieren. Aber nicht so:

$method->getAttributes( \Core\Attributes\Router::class,
                        \ReflectionAttribute::IS_INSTANCEOF );
$route = $attribute -> newInstance();

Da hast Du einiges falsch verstanden. Deine Attribute sind keine Instanzen der Routerklasse. Es sei denn, du hast tatsächlich eine Klasse GET geschrieben, die von \Core\Attributes\Router abgeleitet ist. Hast Du das? Wenn ja - ich finde es suboptimal.

Ich würde es Dir so vorschlagen.

Definiere eine einzige Attributklasse, mit der Du die Methoden ausstattest, die Actions implementieren. Diese Action-Attributklasse besitzt alle Propertys für die benötigten MVC-Informationen.

namespace Core\Attributes
{
  #[\Attribute]
  class Action {
    public readonly string $method;
    public readonly string $name;

    public function __construct(string $method = "GET", string $actionName = null) {
      $this->method = $method;
      $this->name   = $actionName;
	}
}

Es ist wichtig, dass die Attributklasse selbst ein Attribut bekommt. Das Attribut \\Attribute wird von PHP verlangt, damit später der newInstance Aufruf funktioniert. Das \ ist nötig, weil Attribute im globalen Namespace deklariert ist und ActionAttribute in \Core\Attributes steht.

Die readonly-Deklaration benötigt übrigens PHP 8.1, für ältere PHPs musst Du das weglassen.

Jetzt kannst Du getAttributes aufrufen und auf ActionAttribute filtern:

   $actionAttributes = $method->getAttributes(
               \Core\Attributes\Action::class,
               \ReflectionAttribute::IS_INSTANCEOF );
   if (count($actionAttributes) > 0)
   {
      $action = $actionAttributes[0] -> newInstance();
      if (!$action->name)
         $action->name = $method->name;
   }

Es ist eine Design-Entscheidung, ob Du zulassen willst, dass eine Methode aufgerufen werden kann, die kein Action-Attribut hat. Ich würde das aus Sicherheitsgründen verbieten, genau wie auf Controller-Ebene.

Mit dem Action-Attribut kannst Du nun abfragen, für welche HTTP Methoden und welche Routenaktionen die gefundene Methode im Controller geeignet ist. Wenn Du Glück hast, kommt am Ende genau eine Methode 'raus. Andernfalls muss der Router leider den Aufruf zurückweisen.

Es gibt auch eine Alternative ohne Action-Attributklassen. Wenn Du ein Attribut GET verwendest und keine PHP Klasse GET definierst,, kannst Du auf den Methoden der Klasse auch getAttributes("GET") aufrufen und würdest das Attribut bekommen. Der Aufruf von newInstance geht nun nicht, aber Du kannst getArguments() aufrufen und bekommst die Parameter für das Attribut. Ich meine aber, dass es sauberer ist, mit Attributklassen zu arbeiten und die Interpretation der Attributparameter in dieser Klasse vorzunehmen.

Rolf

--
sumpsi - posui - obstruxi