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

Beitrag lesen

Hallo Rolf,

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.

okay, das ergibt Sinn.

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.

Variable Routen habe ich bei dem Versuch mit den Attributen der Einfachheit halber erst mal nicht berücksichtig.

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.

Also soll ich im Vorfeld angeben, auf welchen Controller und welchen Action-Namen ich es mit dieser Route abgesehen habe? Bei variablen Routen ist doch gerade der Clou, dass man vorher nicht wissen muss, was der Router finden soll. Er reagiert, wenn eine entsprechende Controller-Class gefunden wird. Oder habe ich dich da falsch verstanden?

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.

Zur gefundenen Action? Meinst du vielleicht zu der Action, die ich vorher deklariert habe?

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.

Ja, das habe ich durch den Namespace gelöst. Die Controller befinden sich bei mir im Ordner App/Controllers. Der Router ist im Core-Verzeichnis. Haben beide separate Namespaces.

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.

Ja, habe ich. Und ja, ich finde es auch suboptimal. Ich hätte gerne alles in einer einzigen Attributklasse, aber weiß nicht, wie ich das bewerkstelligen soll.

Ich würde es Dir so vorschlagen.

Definiere eine einzige Attributklasse, mit der Du die Methoden ausstattest, die Actions implementieren.

Moment, für mich waren Actions ein Sammelbegriff für eine Klasse plus die jeweilige Methode. Könntest du erklären, was du mit Methoden meinst, „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.

Momentan habe ich eine Klasse, die provisorisch ebenfalls Router heißt, sich aber in einem anderen Namespace befindet (Core\Attributes). Diese Router-Klasse ist ganz simpel aufgebaut:

<?php
declare( strict_types = 1 );
namespace Core\Attributes;
use Attribute;

#[ Attribute( Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE ) ]
class Router {
	public function __construct( public string $route, public string $httpMethod )
	{ }
}

Die GET- und POST-Klassen befinden sich ebenfalls im Namespace Core\Attributes und beerben jene Router-Klasse. Aber wie du schon sagtest: eine suboptimale Lösung.

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

Ich benutze PHP 8.1. Das mit der readonly-Deklaration muss ich mir mal anschauen, klingt interessant.

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.

Dann werde ich deiner Empfehlung folgen.

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.

Auch hier verstehe ich leider nicht wirklich die Differenzierung zwischen Aktionen und Methoden … sorry. Da muss ich mal eine Weile über deinen Code nachdenken. Abstraktes logisches Denken war nie meine Stärke.

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.

Von dem, was ich verstanden habe, klingt für mich Letzteres auch cleaner.

Dann probiere ich mal weiter rum und raufe mir die Haare.

Grüße
Boris