Nützliche Repositories mit Eloquent?

In der letzten Woche habe ich einen Artikel über die Nutzlosigkeit des Repository-Templates für Eloquent-Entitäten geschrieben. Ich habe jedoch versprochen, zu erklären, wie man es teilweise sinnvoll einsetzen kann. Dazu werde ich analysieren, wie dieses Template üblicherweise in Projekten verwendet wird. Der minimal notwendige Satz an Methoden für das Repository:<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); }

In echten Projekten, in denen sich entschieden wurde, Repositories zu verwenden, werden 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 Entitätsklassen mit der Verantwortung für ihre eigene Abfrage zu belasten; die Übertragung dieser Aufgabe auf Repository-Klassen erscheint logisch. Ist das so? Ich habe diesen Schnittstellenabschnitt absichtlich visuell in zwei Teile unterteilt. Der erste Teil der Methoden wird für Schreiboperationen verwendet.

Eine Standard-Schreiboperation ist:

Die Erstellung eines neuen Objekts und der Aufruf von

  • PostRepository::save PostRepository::save
  • PostRepository::getById, Manipulation mit Entitäten und Aufruf PostRepository::save
  • Aufruf PostRepository::delete

Bei Schreiboperationen werden keine Auswahlmethoden verwendet. Bei Leseoperationen hingegen nutzt man nur die Methoden get*. Wenn Sie mehr über das Interface Segregation Principle lesen möchten (Buchstabe I in SOLID), wird klar, dass unser Interface zu umfangreich ist und mindestens zwei unterschiedliche Verantwortlichkeiten hat. Es ist an der Zeit, es in zwei Teile zu teilen. Die Methode getById wird in beiden benötigt, jedoch werden sich bei zunehmender Komplexität der Anwendung ihre Implementierungen unterscheiden. Das werden wir später noch sehen. Über die Nützlichkeit des Write-Teils habe ich in meinem letzten Artikel geschrieben, daher werde ich in diesem darüber einfach vergessen.

Der Read-Teil hingegen scheint mir nicht völlig nutzlos, da selbst für Eloquent hier mehrere Implementierungen möglich sein können. Wie soll die Klasse genannt werden? Man könnte ReadPostRepository, aber in Bezug auf das Muster Repository hat es bereits wenig Bezug. Man könnte einfach 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:

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

    /**
    * @return Post[] | Collection
    */
    public function getLastPosts()
    {
        return Post::orderBy('created_at', 'desc')
            ->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();
    }
}

Интерфейс должен быть связан с реализацией, например в AppServiceProvider:

<?php
final class AppServiceProvider extends ServiceProvider 
{
    public function register()
    {
        $this->app->bind(PostQueries::class, 
            EloquentPostQueries::class);
    }
}

Данный класс уже полезен. Он реализует свою ответственность, разгрузив этим либо контроллеры, либо класс сущности. В контроллере он может быть использован так:

<?php
final class PostsController extends Controller
{
    public function lastPosts(PostQueries $postQueries)
    {
        return view('posts.last', [
            'posts' => $postQueries->getLastPosts(),
        ]);
    }
} 

Метод PostsController::lastPosts просто просит себе какую-нибудь реализацию PostsQueries и работает с ней. В провайдере мы связали PostQueries с классом EloquentPostQueries и в контроллер будет подставлен этот класс.

Stellen wir uns vor, unsere Anwendung ist sehr beliebt geworden. Tausende von Nutzern öffnen pro Minute die Seite mit den neuesten Veröffentlichungen. Die beliebtesten Beiträge werden ebenfalls häufig gelesen. Datenbanken bewältigen solch hohe Lasten nicht gut, weshalb eine Standardlösung — Cache — verwendet wird. Neben der Datenbank wird eine Art Snapshot der Daten in einem für bestimmte Operationen optimierten Speicher gehalten — memcached oder redis.

Die Logik des Cache sollte normalerweise nicht so kompliziert sein, jedoch ist es nicht ideal, sie in EloquentPostQueries zu implementieren (zumindest wegen des Single Responsibility Principle). Es ist viel natürlicher, das Muster Decorator zu verwenden und das Caching als Dekoration der Haupthandlung 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
}

Ignorieren Sie die Benutzeroberfläche Repository im Builder. Aus unbekannten Gründen wurde diese Schnittstelle für das Caching in Laravel so benannt.

Klasse CachedPostQueries implementiert nur das Caching. $this->cache->remember überprüft, ob der entsprechende Eintrag im Cache vorhanden ist. Wenn nicht, wird der Callback aufgerufen und der zurückgegebene Wert im Cache gespeichert. Jetzt müssen wir nur noch diese Klasse in die Anwendung integrieren. Alle Klassen, die in der Anwendung eine Implementierung der Schnittstelle anfordern, PostQueries sollten eine Instanz der Klasse erhalten CachedPostQueries. Allerdings muss der CachedPostQueries als Parameter in den Konstruktor die Klasse erhalten EloquentPostQueries, da er ohne eine "echte" Implementierung nicht arbeiten kann. Wir ändern AppServiceProvider:

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

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

Alle meine Anforderungen sind ziemlich natürlich im Provider beschrieben. So haben wir das Caching für unsere Anfragen lediglich durch das Schreiben einer Klasse und das Ändern der Konfiguration des Containers umgesetzt. Der Code der restlichen Anwendung blieb unverändert.

Natürlich ist es für die vollständige Implementierung des Caching auch notwendig, eine Invalidierung zu realisieren, damit der entfernte Artikel nicht noch eine Zeit lang auf der Website bleibt, sondern sofort gelöscht wird. Aber das sind schon Kleinigkeiten.

Fazit: Wir haben nicht nur eine, sondern gleich zwei Vorlagen verwendet. Die Vorlage Command Query Responsibility Segregation (CQRS) schlägt vor, Lese- und Schreiboperationen auf der Ebene der Schnittstellen vollständig zu trennen. Ich bin darauf gestoßen durch Interface Segregation Principle, was zeigt, dass ich geschickt mit Vorlagen und Prinzipien umgehe und eine aus der anderen ableite wie einen Theorem 🙂 Natürlich benötigt nicht jedes Projekt eine solche Abstraktion für Entitätsabfragen, aber ich teile mit Ihnen einen Trick. In der Anfangsphase der Anwendungsentwicklung können Sie einfach eine Klasse PostQueries mit einer gewöhnlichen Implementierung über Eloquent erstellen:

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

    // andere Methoden
}

Wenn die Notwendigkeit für Caching besteht, kann leicht ein Interface (oder eine abstrakte Klasse) anstelle dieser Klasse erstellt werden, PostQueriesderen Implementierung in die Klasse EloquentPostQueries kopiert werden kann, und man zu dem zuvor beschriebenen Schema übergehen kann. Der restliche Code der Anwendung muss nicht geändert werden.

All diese Tricks mit Klassen, Interfaces, Dependency Injection und CQRS detailliert beschrieben in meinem Buch „Architektur komplexer Webanwendungen“. Dort finden Sie auch die Lösung für das Rätsel, warum alle meine Klassen in den Beispielen zu diesem Artikel als final markiert sind.

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster