Des référentiels utiles avec Eloquent ?

La semaine dernière, j'ai écrit article sur l'inutilité du modèle Repository pour les entités Eloquents, cependant, il a promis de me dire comment l'utiliser en partie à son avantage. Pour ce faire, je vais essayer d’analyser comment ce modèle est habituellement utilisé dans les projets. L'ensemble minimum de méthodes requis pour un référentiel :

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

Cependant, dans les projets réels, s'il était décidé d'utiliser des référentiels, des méthodes de récupération d'enregistrements y sont souvent ajoutées :

<?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);
}

Ces méthodes pourraient être implémentées via des portées Eloquent, mais surcharger les classes d'entités avec la responsabilité de se récupérer elles-mêmes n'est pas la meilleure idée, et déplacer cette responsabilité vers les classes du référentiel semble logique. Est-ce ainsi ? J'ai spécifiquement divisé visuellement cette interface en deux parties. La première partie des méthodes sera utilisée dans les opérations d'écriture.

L'opération d'écriture standard est la suivante :

  • construction d'un nouvel objet et défi PostRepository ::enregistrer
  • PostRepository :: getById, manipulation et invocation d'entités PostRepository ::enregistrer
  • défi PostRepository :: supprimer

Les opérations d'écriture n'utilisent pas de méthodes de récupération. Dans les opérations de lecture, seules les méthodes get* sont utilisées. Si vous lisez à propos Principe de séparation des interfaces (lettre I в SOLIDE), alors il deviendra clair que notre interface est trop volumineuse et remplit au moins deux responsabilités différentes. Il est temps de le diviser par deux. Méthode getById est nécessaire dans les deux cas, mais à mesure que l'application devient plus complexe, ses implémentations différeront. Nous verrons cela un peu plus tard. J'ai parlé de l'inutilité de la partie écriture dans un article précédent, donc dans celui-ci, je vais tout simplement l'oublier.

La partie Read ne me semble pas si inutile, puisque même pour Eloquent il peut y avoir plusieurs implémentations ici. Comment nommer la classe ? Peut ReadPostRepository, mais au modèle Dépôt il a déjà peu de pertinence. Tu peux juste Post-requêtes:

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

L'implémenter avec Eloquent est assez simple :

<?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();
    }
}

L'interface doit être associée à l'implémentation, par exemple dans Fournisseur de services d'application:

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

Cette classe est déjà utile. Il prend conscience de sa responsabilité en relevant soit les contrôleurs, soit la classe d'entité. Dans un contrôleur, il peut être utilisé comme ceci :

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

méthode PostsController :: lastPosts je demande juste une mise en œuvre PostsRequêtes et travaille avec. Dans le fournisseur que nous avons lié Post-requêtes avec classe EloquentPostQueries et cette classe sera remplacée dans le contrôleur.

Imaginons que notre application soit devenue très populaire. Des milliers d'utilisateurs par minute ouvrent la page avec les dernières publications. Les publications les plus populaires sont également lues très souvent. Les bases de données ne gèrent pas très bien de telles charges, elles utilisent donc une solution standard : un cache. En plus de la base de données, un certain instantané de données est stocké dans un stockage optimisé pour certaines opérations - Memcached ou Redis.

La logique de mise en cache n'est généralement pas si compliquée, mais son implémentation dans EloquentPostQueries n'est pas très correcte (ne serait-ce que parce que Principe de responsabilité unique). Il est beaucoup plus naturel d'utiliser un modèle Décorateur et implémentez la mise en cache comme décoration pour l'action principale :

<?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();
            });
    }

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

Ignorer l'interface Dépôt dans le constructeur. Pour une raison inconnue, ils ont décidé de nommer ainsi l'interface de mise en cache dans Laravel.

classe CachedPostQueries implémente uniquement la mise en cache. $this->cache->souvenir vérifie si cette entrée est dans le cache et sinon, appelle le rappel et écrit la valeur renvoyée dans le cache. Il ne reste plus qu'à implémenter cette classe dans l'application. Nous avons besoin de toutes les classes de l'application pour demander une implémentation de l'interface Post-requêtes a commencé à recevoir une instance de la classe CachedPostQueries. Cependant, lui-même CachedPostQueries le constructeur doit recevoir une classe en paramètre EloquentPostQueriescar cela ne peut pas fonctionner sans une « vraie » implémentation. Nous changeons Fournisseur de services d'application:

<?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);
    }
}

Tous mes souhaits sont tout naturellement décrits chez le prestataire. Ainsi, nous avons implémenté la mise en cache de nos requêtes uniquement en écrivant une classe et en modifiant la configuration du conteneur. Le reste du code de l'application n'a pas changé.

Bien entendu, pour mettre pleinement en œuvre la mise en cache, il est également nécessaire de mettre en œuvre l'invalidation afin que l'article supprimé ne reste pas bloqué sur le site pendant un certain temps, mais soit immédiatement supprimé. Mais ce sont des choses mineures.

En résumé : nous avons utilisé non pas un, mais deux modèles. Échantillon Ségrégation des responsabilités des requêtes de commande (CQRS) propose de séparer complètement les opérations de lecture et d'écriture au niveau de l'interface. Je suis venu vers lui par Principe de séparation des interfaces, ce qui suggère que je manipule habilement des modèles et des principes et que je dérive les uns des autres sous forme de théorème :) Bien sûr, tous les projets n'ont pas besoin d'une telle abstraction pour sélectionner des entités, mais je partagerai l'astuce avec vous. développement, vous pouvez simplement créer une classe Post-requêtes avec l'implémentation habituelle via Eloquent :

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

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

Lorsque le besoin de mise en cache se fait sentir, d'un simple geste vous pouvez créer une interface (ou classe abstraite) à la place de cette classe Post-requêtes, copiez son implémentation dans la classe EloquentPostQueries et allez au schéma que j'ai décrit plus tôt. Le reste du code de l'application n'a pas besoin d'être modifié.

Toutes ces astuces avec des classes, des interfaces, Injection de dépendance и CQRS décrit en détail dans mon livre « Architecture des applications Web complexes ». Il existe également une solution à l'énigme pourquoi tous mes cours dans les exemples de cet article sont marqués comme finaux.

Source: habr.com

Ajouter un commentaire