Nützliche Repositories mit Eloquent?

In der vergangenen Woche habe ich einen Artikel über die Nutzlosigkeit des Repository-Templates für Eloquent-Entitäten geschrieben, habe jedoch versprochen zu erläutern, wie man es teilweise sinnvoll nutzen kann. Zu diesem Zweck werde ich versuchen zu analysieren, wie dieses Template in Projekten üblicherweise verwendet wird. Das minimal erforderliche Set an Methoden für ein Repository lautet:

<?php
interface PostRepository
{
    public function getById($id): Post;
    public function save(Post $post);
    public function delete($id);
}

In realen Projekten, in denen beschlossen wurde, Repositories zu verwenden, werden diesen oft Methoden zur Abfrage von Datensätzen hinzugefügt:

<?php
interface PostRepository
{
    public function getById($id): Post;
    public function save(Post $post);
    public function delete($id);

    public function getLastPosts();
    public function getTopPosts();
    public function getUserPosts($userId);
}

Diese Methoden könnten über Eloquent Scopes implementiert werden, aber es ist nicht die beste Idee, die Klassen von Entitäten mit der Verantwortung zu belasten, sich selbst abzufragen. Es erscheint logischer, diese Verantwortung in die Repository-Klassen auszulagern. Ist das wirklich so? Ich habe dieses Interface absichtlich visuell in zwei Teile unterteilt. Der erste Teil der Methoden wird in Schreiboperationen verwendet.

Die Standard-Schreiboperationen sind:

  • die Erstellung eines neuen Objekts und der Aufruf von PostRepository::save
  • PostRepository::getById, Manipulationen mit der Entität und der Aufruf von PostRepository::save
  • der Aufruf PostRepository::delete

In Schreiboperationen werden keine Abfragemethoden verwendet. In Leseoperationen kommen hingegen nur die Methoden get* zum Einsatz. Wenn man über Interface Segregation Principle liest (Buchstabe I in SOLID), wird klar, dass unser Interface zu groß ist und mindestens zwei unterschiedliche Verantwortlichkeiten erfüllt. Es ist Zeit, es in zwei Teile zu teilen. Die Methode getById wird in beiden benötigt, aber mit der zunehmenden Komplexität der Anwendung werden die Implementierungen unterschiedlich sein. Das werden wir gleich sehen. Über die Nutzlosigkeit des Schreibteils habe ich im vorherigen Artikel geschrieben, daher werde ich ihn hier einfach ignorieren.

Der Lese-Teil scheint mir jedoch nicht so nutzlos zu sein, da es sogar für Eloquent mehrere Implementierungen geben kann. Wie sollte die Klasse genannt werden? Man könnte sagen ReadPostRepository, aber das hat bereits wenig mit dem Template zu tun. Man könnte einfach Repository PostQueries <?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }:

Die Implementierung mit Eloquent ist recht einfach:

Die Implementierung mit Eloquent ist recht einfach:

limit(/*some limit*/)
            ->get();
    }
    /**
    * @return Post[] | Collection
    */
    public function getTopPosts()
    {
        return Post::orderBy('rating', 'desc')
            ->limit(/*some limit*/)
            ->get();
    }

    /**
    * @param int $userId
    * @return Post[] | Collection
    */
    public function getUserPosts($userId)
    {
        return Post::whereUserId($userId)
            ->orderBy('created_at', 'desc')
            ->get();
    }
}

Das Interface sollte mit einer Implementierung verknüpft werden, zum Beispiel mit AppServiceProvider:

app->bind(PostQueries::class,
            EloquentPostQueries::class);
    }
}

Diese Klasse ist bereits nützlich. Sie erfüllt ihre Aufgabe, indem sie entweder Controller oder Entitätsklasse entlastet. Im Controller kann sie folgendermaßen verwendet werden:

$postQueries->getLastPosts(),
        ]);
    }
} 

Methode PostsController::lastPosts fordert sich einfach eine beliebige Implementierung an PostsQueries und arbeitet mit ihr. Im Provider haben wir verbunden <?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); } mit der Klasse EloquentPostQueries und im Controller wird diese Klasse eingesetzt.

Stellen wir uns vor, unsere Anwendung ist sehr populär geworden. Tausende von Benutzern öffnen pro Minute die Seite mit den neuesten Veröffentlichungen. Auch die beliebtesten Inhalte werden sehr häufig gelesen. Datenbanken bewältigen solche Lasten nicht sehr gut, weshalb eine standardmäßige Lösung — Cache — verwendet wird. Neben der Datenbank wird eine Art Datenabdruck in einem für bestimmte Operationen optimierten Speicher gehalten — memcached oder redis.

Die Logik zur Zwischenspeicherung ist normalerweise nicht so kompliziert, aber sie in EloquentPostQueries zu implementieren, wäre nicht ganz richtig (zumindest wegen des Single Responsibility Principle). Es ist viel natürlicher, das Muster Dekorator zu verwenden und das Caching als Dekorierung der Hauptaktion zu implementieren:

base = $base;
        $this->cache = $cache;
    }

    /**
    * @return Post[] | Collection
    */
    public function getLastPosts()
    {
        return $this->cache->remember('last_posts',
            self::LASTS_DURATION,
            function(){
                return $this->base->getLastPosts();
            });
    }

    // andere Methoden sind praktisch gleich
}

Achten Sie nicht auf die Benutzeroberfläche Repository im Konstruktor. Aus unerklärlichen Gründen hat man entschieden, die Benutzeroberfläche für das Caching in Laravel so zu nennen.

Klasse CachedPostQueries implementiert nur das Caching. $this->cache->remember prüft, ob dieser Eintrag im Cache vorhanden ist, und wenn nicht, ruft er den Callback auf und speichert den zurückgegebenen Wert im Cache. Nun bleibt nur noch, diese Klasse in die Anwendung zu integrieren. Wir benötigen, dass alle Klassen, die in der Anwendung eine Implementierung der Benutzeroberfläche anfordern, <?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); } eine Instanz der Klasse erhalten. CachedPostQueriesAllerdings muss diese CachedPostQueries als Parameter in den Konstruktor die Klasse EloquentPostQueriesübergeben, da sie ohne eine "echte" Implementierung nicht funktionieren kann. Ändern wir AppServiceProvider:

app->bind(PostQueries::class, 
            CachedPostQueries::class);

        $this->app->when(CachedPostQueries::class)
            ->needs(PostQueries::class)
            ->give(EloquentPostQueries::class);
    }
}

Alle meine Wünsche werden recht natürlich im Anbieter beschrieben. Auf diese Weise haben wir das Caching für unsere Anfragen nur durch das Schreiben einer Klasse und die Änderung der Containerkonfiguration implementiert. Der Code der restlichen Anwendung hat sich nicht verändert.

Natürlich benötigt eine vollständige Implementierung des Cachings auch eine Invalidierung, damit ein entferner Artikel nicht noch eine gewisse Zeit auf der Website bleibt, sondern sofort gelöscht wird. Aber das sind schon Kleinigkeiten.

Fazit: Wir haben nicht einen, sondern gleich zwei Muster verwendet. Das Muster Command Query Responsibility Segregation (CQRS) empfiehlt, Lese- und Schreiboperationen auf der Ebene der Schnittstellen vollständig zu trennen. Ich bin über Interface Segregation Principlezu ihm gekommen, was zeigt, dass ich geschickt mit Mustern und Prinzipien umgehe und eines aus dem anderen ableite wie einen Satz 🙂 Natürlich benötigt nicht jedes Projekt eine solche Abstraktion für Abfragen von Entitäten, aber ich möchte Ihnen einen Trick verraten. In der frühen Entwicklungsphase einer Anwendung kann man einfach eine Klasse <?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); } mit einer normalen Implementierung über Eloquent erstellen:

<?php
final class PostQueries
{
    public function getById($id): Post
    {
        return Post::findOrFail($id);
    }

    // andere Methoden
}

Wenn das Bedürfnis nach Caching entsteht, kann man ganz einfach an der Stelle dieser Klasse ein Interface (oder eine abstrakte Klasse) erstellen, <?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }seine Implementierung in die Klasse EloquentPostQueries kopieren und zum Schema übergehen, das ich zuvor beschrieben habe. Den restlichen Code der Anwendung muss man nicht ändern.

All diese Tricks mit Klassen, Schnittstellen, Dependency Injection und CQRS werden ausführlich in meinem Buch "Architektur komplexer Webanwendungen" beschrieben.Hier liegt das Rätsel, warum alle meine Klassen in den Beispielen zu diesem Artikel als final gekennzeichnet sind.

Quelle: habr.com

60GB SSD 8Gb DDR4