How to Write a Good Controller? One of the Most Common Mistakes Using the Example of Laravel

Wie schreibt man einen guten Controller? Ein häufig gemachter Fehler am Beispiel von Laravel

Jeder lernt aus Fehlern. In diesem Artikel werde ich auf einen kardinalen Fehler hinweisen, der dazu führt, dass ein Controller mehrere hundert Codezeilen enthält, unlesbar und sehr schwer zu warten wird. Ich werde jedoch einige Wege präsentieren, wie man dieses Problem lösen kann.

Bei der Implementierung von PHP-Entwicklungsservices bei Droptica bin ich diesem Fehler viele Male begegnet und habe ihn sogar zu Beginn selbst gemacht. Wovon spreche ich? Davon, die gesamte Logik in Controller-Methoden zu setzen, ohne einzelne Aktivitäten in separate Komponenten der Anwendung zu unterteilen.

Um dies zu vermeiden, befolgen Sie zwei Hauptregeln:

  1. Sie können eine unbegrenzte Anzahl von Controllern haben. Es ist besser, viele mit wenigen Methoden zu haben als einen Controller, der viele Methoden enthält.
  2. Der Controller ist nur für die Gesamtlogik verantwortlich. Nach dem Prinzip „vom Allgemeinen zum Besonderen“ sollte ein allgemeiner Teil im Controller enthalten sein. Die Besonderheiten sollten hingegen in anderen Anwendungskomponenten enthalten sein. Je weniger Code im Controller, desto besser.

Werfen wir einen Blick auf das folgende Beispiel:

class CustomersController extends Controller
{
    public function getActiveCustomers()
    {
        return Customer::where('active', 1)->get();
    }

    public function store()
    {
        $data = $this->validateRequest();
        $customer = Customer::create($data);

        // Benachrichtigungs-E-Mail an Admin senden.
        Mail::to('[email protected]')->send(new NewCustomerMail($customer));

        // Begrüßungsmail an Kunden senden
        Mail::to($customer->email)->send(new WelcomeMail($customer));
    }

    public function getQueryResults()
    {
        $customers = Customer::query();

        if (request()->has('name')) {
            $customers->where('name', request('name'));
        }

        if (request()->has('active')) {
            $customers->where('active', request('active'));
        }

        if (request()->has('sort')) {
            $customers->orderBy('name', request('sort'));
        }

        $customers->get();

        return $customers;
    }

    private function validateRequest()
    {
        // Code zur Anfragevalidierung kommt hier hin…
    }
}

Auf den ersten Blick mag es scheinen, dass alles so ist, wie es sein sollte. Der Controller enthält drei Methoden. Die erste davon gibt die aktiven Kunden zurück, die zweite erstellt einen neuen Kunden und versendet E-Mail-Benachrichtigungen, während die dritte für das Filtern der Suchergebnisse verantwortlich ist. Warum wird dieses Beispiel also als fehlerhaft dargestellt?

CRUD

Beginnen wir mit dem ersten Beispiel. Laut der Laravel-Dokumentation sollte der Controller die Methoden index, create, store, show, edit, update oder/und delete enthalten. Daher sollte die Methode in eine der oben genannten umbenannt werden. Damit das Sinn macht, müssen Sie der ersten Regel folgen. Erstellen wir also einen komplett neuen Controller – ActiveCustomersController, und verschieben die getActiveCustomers-Methode dorthin, während wir ihren Namen in index ändern.

class ActiveCustomersController extends Controller
{
    public function index()
    {
        return Customer::where('active', 1)->get();
    }
}

Beachten Sie, dass Sie, wenn Ihr Controller nur eine Methode enthält, diese Methode in Laravel in __invoke() umbenennen können.

class ActiveCustomersController extends Controller
{
    public function __invoke()
    {
        return Customer::where('active', 1)->get();
    }
}

Das ist nicht die Aufgabe des Controllers

Betrachten wir nun die nächste Methode: store(). Ihr Name ist korrekt, ebenso der Name des Controllers. Wo also versteckt sich der Fehler?

In diesem Beispiel werden nach der Erstellung eines neuen Kunden zwei E-Mail-Nachrichten gesendet (an den Kunden und an den Administrator). Es ist nicht viel, aber lassen Sie uns versuchen, sich vorzustellen, was passieren wird, wenn Sie in Zukunft zusätzlich eine Slack-Benachrichtigung hinzufügen und eine SMS senden möchten (z.B. mit einem Bestätigungscode). Die Code-Menge wird erheblich zunehmen und die zweite Regel brechen. Daher sollten wir nach einem anderen Ort in unserer Anwendung suchen, der für das Versenden von Benachrichtigungen verantwortlich ist. Events sind dafür ideal.

Beginnen wir also damit, das Event zu starten, das den Code ersetzt, der für das Versenden von Benachrichtigungen verantwortlich ist:

public function store()
{
    $data = $this->validateRequest();
    $customer = Customer::create($data);

    event(new NewCustomerHasRegisteredEvent($customer));
}

Erstellen Sie dann dieses Event, indem Sie in die Konsole eingeben:

php artisan make:event NewCustomerHasRegisteredEvent

Dieser Befehl erstellt eine neue Klasse – NewCustomerHasRegisteredEvent im Verzeichnis app/Events. Da Sie im Controller den erstellten Kunden mithilfe eines Parameters übertragen, sollten Sie diese Daten im Konstruktor in der Event-Klasse akzeptieren.

Das Ganze sollte so aussehen:

class NewCustomerHasRegisteredEvent
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $customer;

    public function __construct($customer)
    {
        $this->customer = $customer;
    }
}

Dann müssen Sie Listener erstellen, die die entsprechende Aktion ausführen (in diesem Fall – das Senden einer E-Mail-Nachricht), wenn das Event ausgelöst wird. Geben Sie erneut in die Konsole ein:

php artisan make:listener WelcomeNewCustomerListener

Diesmal sollte eine neue WelcomeNewCustomerListener-Klasse im Verzeichnis app/Listeners erstellt werden. Alles, was Sie in diesem Fall tun müssen, ist, den Code, der für das Senden der E-Mail verantwortlich ist, in die handle() Methode zu setzen. Diese Methode nimmt den $event-Parameter, der die Kundendaten enthält, die im Konstruktor der NewCustomerHasRegisteredEvent-Klasse übermittelt wurden.

class WelcomeNewCustomerListener
{
    public function handle($event)
    {
        Mail::to($event->customer->email)->send(new WelcomeMail($event->customer));
    }
}

Versuchen Sie nun, selbst einen weiteren Listener zu erstellen, der für das Senden einer E-Mail an den Administrator verantwortlich ist, und nennen Sie ihn zum Beispiel NewCustomerAdminNotificationListener.

Sie haben ein Event und Listener erstellt. Schließlich müssen Sie alles miteinander verbinden, damit die entsprechenden Listener gestartet werden, wenn das entsprechende Event ausgelöst wird. Fügen Sie dazu Ihr Event und die Listener dem $listen-Array in der App\Providers\EventServiceProvider-Klasse hinzu:

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        NewCustomerHasRegisteredEvent::class => [
            WelcomeNewCustomerListener::class,
            NewCustomerAdminNotificationListener::class,
        ],
    ];
}

Pipelines

Das letzte, dritte Beispiel für heute wird etwas anders sein. Sie wissen bereits aus dem ersten Beispiel, dass Sie in diesem Fall damit beginnen sollten, einen neuen Controller zu erstellen, z. B. CustomersQueryController, aber damit werden Sie nicht aufhören. Der im Code enthaltene Methode ist nicht schlecht; sie kann jedoch auf bessere Weise implementiert werden. Zudem werden Sie eine Funktion verwenden, die in Laravel recht häufig verwendet wird, über die jedoch nicht viel in der offiziellen Dokumentation zu finden ist.

Pipelines sind nichts anderes als eine Reihe von Schritten/Aufgaben, die Sie nacheinander ausführen möchten. Das wird perfekt im Fall, in dem Sie je nach den Daten, die der Benutzer erhalten möchte, eine Reihe von Schritten durchlaufen müssen, um eine entsprechende Abfrage mit Eloquent zu erstellen.

Beginnen wir also damit, die Methode getQueryResults in einen separaten Controller zu verschieben und umzubenennen:

class CustomersQueryController extends Controller
{
    public function index()
    {
        $customers = Customer::query();

        if (request()->has('name')) {
            $customers->where('name', request('name'));
        }

        if (request()->has('active')) {
            $customers->where('active', request('active'));
        }

        if (request()->has('sort')) {
            $customers->orderBy('name', request('sort'));
        }

        $customers->get();

        return $customers;
    }
}

Erstellen Sie anschließend im App-Verzeichnis ein neues Verzeichnis namens QueryFilters und darin drei Klassen: name, active, sort. Jede für einen Suchparameter. Diese Klasse wird eine handle() Methode enthalten, die zwei Parameter nimmt: $request und $next. Jetzt werde ich die Active-Klasse als Beispiel präsentieren. Versuchen Sie, die anderen beiden Klassen selbst vorzubereiten.

class Active
{
    public function handle($request, \Closure $next)
    {

    }
}

In der Handle-Methode sollten Sie die Bedingung entgegensetzen, die bisher im Controller war. Wenn die Anfrage keinen 'active' enthält, wechseln Sie zum nächsten Schritt in der Pipeline. Andernfalls fügen Sie dem Builder die entsprechende Bedingung hinzu und fahren fort:

public function handle($request, \Closure $next)
{
    if (!request()->has('active')) {
        return $next($request);
    }

    $builder = $next($request);

    return $builder->where('active', request('active'));
}

Sobald Sie die anderen beiden Klassen auf ähnliche Weise vorbereitet haben, müssen Sie nur noch alles zusammenfügen. Gehen Sie zurück zum Controller und geben Sie alle oben genannten Klassen durch die Pipeline:

public function index()
{
    $customers = app(Pipeline::class)
        ->send(Customer::query())
        ->through([
            \App\QueryFilters\Active::class,
            \App\QueryFilters\Name::class,
            \App\QueryFilters\Sort::class,
        ])
        ->thenReturn();

    return $customers->get();
}

Zusammenfassung

In diesem Artikel habe ich drei Wege vorgestellt, wie man Logik aus einem Controller herausziehen kann. Natürlich müssen Sie sich nicht auf diese beschränken – es gibt noch viele weitere Möglichkeiten. Sie können über nützliche Laravel-Funktionen, die nicht jeder kennt lesen.

Denken Sie also daran, so wenig Code wie möglich im Controller zu lassen. Dieser Code sollte nur allgemeine Logik enthalten. Alle detaillierteren und verwirrenden Elemente sollten Sie an einem separaten Ort unterbringen. Und fragen Sie sich immer: Ist das wirklich eine Aufgabe für den Controller?

3. Best practices for software development teams