Nützliche Repositories mit Eloquent?

Letzte Woche habe ich geschrieben Artikel über die Nutzlosigkeit der Repository-Vorlage für Eloquent-EntitätenEr versprach mir jedoch, mir zu sagen, wie ich es teilweise zu seinem Vorteil nutzen könnte. Dazu versuche ich zu analysieren, wie diese Vorlage üblicherweise in Projekten verwendet wird. Der mindestens erforderliche Satz an Methoden für ein Repository:

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

Wenn in realen Projekten jedoch die Verwendung von Repositorys beschlossen wurde, werden diesen häufig Methoden zum Abrufen 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 durch Eloquent-Bereiche implementiert werden, aber die Überlastung von Entitätsklassen mit der Verantwortung, sich selbst abzurufen, ist nicht die beste Idee, und diese Verantwortung auf Repository-Klassen zu verlagern erscheint logisch. Ist es so? Ich habe diese Schnittstelle speziell visuell in zwei Teile unterteilt. Der erste Teil der Methoden wird in Schreiboperationen verwendet.

Der Standard-Schreibvorgang ist:

  • Bau eines neuen Objekts und Herausforderung PostRepository::save
  • PostRepository::getById, Entitätsmanipulation und Beschwörung PostRepository::save
  • вызов PostRepository::delete

Schreibvorgänge verwenden keine Abrufmethoden. Bei Lesevorgängen werden nur get*-Methoden verwendet. Wenn Sie darüber lesen Prinzip der Schnittstellentrennung (Buchstabe I в SOLIDE), dann wird klar, dass unsere Schnittstelle zu groß ist und mindestens zwei verschiedene Aufgaben erfüllt. Es ist Zeit, es durch zwei zu teilen. Methode getById ist in beiden Fällen erforderlich, aber je komplexer die Anwendung wird, desto unterschiedlicher werden ihre Implementierungen sein. Das werden wir etwas später sehen. Ich habe in einem früheren Artikel über die Nutzlosigkeit des Schreibteils geschrieben, daher werde ich ihn in diesem Artikel einfach vergessen.

Der Read-Teil erscheint mir nicht so nutzlos, da es hier auch für Eloquent mehrere Implementierungen geben kann. Wie soll die Klasse benannt werden? Kann ReadPostRepository, sondern zur Vorlage Dokumente er hat schon wenig Relevanz. Du kannst einfach PostQueries:

<?php
interface PostQueries
{
    public function getById($id): Post;
    public function getLastPosts();
    public function getTopPosts();
    public function getUserPosts($userId);
}

Die Umsetzung mit Eloquent ist ganz 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();
    }
}

Die Schnittstelle muss mit der Implementierung verknüpft sein, zum Beispiel in AppServiceProvider:

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

Diese Klasse ist bereits nützlich. Er nimmt seine Verantwortung wahr, indem er entweder die Controller oder die Entity-Klasse entlastet. In einem Controller kann es so verwendet werden:

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

Verfahren PostsController::lastPosts Ich bitte nur um eine Umsetzung BeiträgeAbfragen und arbeitet damit. Bei dem von uns verlinkten Anbieter PostQueries mit Klasse EloquentPostQueries und diese Klasse wird in den Controller eingesetzt.

Stellen wir uns vor, dass unsere Anwendung sehr beliebt geworden ist. Tausende Benutzer pro Minute öffnen die Seite mit den neuesten Veröffentlichungen. Auch die beliebtesten Publikationen werden sehr oft gelesen. Da Datenbanken solche Belastungen nicht besonders gut bewältigen, verwenden sie eine Standardlösung – einen Cache. Zusätzlich zur Datenbank wird ein bestimmter Daten-Snapshot in einem für bestimmte Vorgänge optimierten Speicher gespeichert – memcached oder wieder auf.

Die Caching-Logik ist normalerweise nicht so kompliziert, aber ihre Implementierung in EloquentPostQueries ist nicht sehr korrekt (schon allein deshalb, weil Prinzip der Einzelverantwortung). Es ist viel natürlicher, eine Vorlage zu verwenden Dekorateur und implementieren Sie Caching als Dekoration für die Hauptaktion:

<?php
use IlluminateContractsCacheRepository;

final class CachedPostQueries implements PostQueries
{
    const LASTS_DURATION = 10;

    /** @var PostQueries */
    private $base;

    /** @var Repository */
    private $cache;

    public function __construct(
        PostQueries $base, Repository $cache) 
    {
        $this->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();
            });
    }

    // другие методы практически такие же
}

Ignorieren Sie die Schnittstelle Dokumente im Konstruktor. Aus einem unbekannten Grund haben sie beschlossen, die Schnittstelle für das Caching in Laravel so zu benennen.

Klasse CachedPostQueries implementiert nur Caching. $this->cache->merken prüft, ob sich dieser Eintrag im Cache befindet und wenn nicht, ruft er den Callback auf und schreibt den zurückgegebenen Wert in den Cache. Jetzt muss nur noch diese Klasse in die Anwendung implementiert werden. Wir benötigen alle Klassen, die in der Anwendung vorhanden sind, um eine Implementierung der Schnittstelle anzufordern PostQueries begann eine Instanz der Klasse zu empfangen CachedPostQueries. Allerdings er selbst CachedPostQueries Der Konstruktor muss eine Klasse als Parameter erhalten EloquentPostQueriesda es ohne eine „echte“ Implementierung nicht funktionieren kann. Wir verändern uns AppServiceProvider:

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

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

Alle meine Wünsche sind im Anbieter ganz selbstverständlich beschrieben. Daher haben wir das Caching für unsere Anfragen nur implementiert, indem wir eine Klasse geschrieben und die Containerkonfiguration geändert haben. Der Rest des Anwendungscodes hat sich nicht geändert.

Um das Caching vollständig zu implementieren, ist es natürlich auch notwendig, die Invalidierung zu implementieren, damit der gelöschte Artikel nicht für einige Zeit auf der Site hängen bleibt, sondern sofort gelöscht wird. Aber das sind Kleinigkeiten.

Fazit: Wir haben nicht eine, sondern zwei Vorlagen verwendet. Probe Verantwortungstrennung für Befehlsabfragen (CQRS) schlägt vor, Lese- und Schreibvorgänge auf Schnittstellenebene vollständig zu trennen. Ich bin durch ihn zu ihm gekommen Prinzip der Schnittstellentrennung, was darauf hindeutet, dass ich Muster und Prinzipien geschickt manipuliere und sie als Theorem voneinander ableite :) Natürlich benötigt nicht jedes Projekt eine solche Abstraktion zur Auswahl von Entitäten, aber ich werde den Trick mit Ihnen teilen. In der Anfangsphase der Anwendung Entwicklung können Sie einfach eine Klasse erstellen PostQueries mit der üblichen Implementierung über Eloquent:

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

    // другие методы
}

Wenn Caching erforderlich ist, können Sie mit einem einfachen Schritt anstelle dieser Klasse eine Schnittstelle (oder abstrakte Klasse) erstellen PostQueries, kopieren Sie seine Implementierung in die Klasse EloquentPostQueries und gehen Sie zu dem Schema, das ich zuvor beschrieben habe. Der restliche Anwendungscode muss nicht geändert werden.

All diese Tricks mit Klassen, Schnittstellen, Abhängigkeitsspritze и CQRS ausführlich beschrieben in mein Buch „Architecture of Complex Web Applications“. Es gibt auch eine Lösung für das Rätsel, warum alle meine Klassen in den Beispielen für diesen Artikel als endgültig markiert sind.

Source: habr.com

Kommentar hinzufügen