La semaine dernière, j'ai écrit un article sur l'inutilité du modèle de référentiel pour les entités Eloquent , mais j'ai promis de dire comment l'utiliser partiellement avec avantage. Pour ce faire, je vais essayer d'analyser comment ce modèle est généralement utilisé dans les projets. L'ensemble minimal requis de méthodes pour le 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 sélection des enregistrements leur 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 étendues Eloquent, mais surcharger les classes d'entités avec des responsabilités pour se récupérer n'est pas une bonne idée et placer cette responsabilité dans les classes de référentiel semble logique. En est-il 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.
Les opérations d'écriture standard sont:
- construire un nouvel objet et appeler PostRepository :: save
- PostRepository :: getById , manipulation d'entité et appel à PostRepository :: save
- appeler PostRepository :: delete
Il n'y a pas de méthodes d'échantillonnage dans les opérations d'écriture. Dans les opérations de lecture, seules les méthodes get * sont utilisées. Si vous lisez le principe de séparation des interfaces (lettre I dans SOLID ), il deviendra clair que notre interface est trop grande et remplit au moins deux responsabilités différentes. Il est temps de le diviser en deux. La méthode getById est requise dans les deux, mais si l'application devient plus compliquée, son implémentation sera différente. Nous verrons cela un peu plus tard. J'ai écrit sur l'inutilité de la partie écriture dans un article précédent, donc dans ce que j'oublie.
Lire la partie ne me semble pas si inutile, car même pour Eloquent il peut y avoir plusieurs implémentations. Comment nommer une classe? Vous pouvez ReadPostRepository , mais il a déjà peu de relation avec le modèle de référentiel . Vous pouvez simplement PostQueries :
<?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Son implémentation avec Eloquent est assez simple:
<?php final class EloquentPostQueries implements PostQueries { public function getById($id): Post { return Post::findOrFail($id); } public function getLastPosts() { return Post::orderBy('created_at', 'desc') ->limit() ->get(); } public function getTopPosts() { return Post::orderBy('rating', 'desc') ->limit() ->get(); } 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 AppServiceProvider :
<?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 déchargeant soit des contrôleurs, soit une classe d'entités. Dans le 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(), ]); } }
La méthode PostsController :: lastPosts se demande simplement une certaine implémentation de PostsQueries et fonctionne avec elle. Dans le fournisseur, nous avons associé PostQueries à la 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 supportent pas très bien de telles charges, elles utilisent donc la solution standard - cache. En plus de la base de données, une certaine pépite de données est stockée dans un référentiel optimisé pour certaines opérations - memcached ou redis .
La logique de mise en cache n'est généralement pas si compliquée, mais sa mise en œuvre dans EloquentPostQueries n'est pas très correcte (du moins en raison du principe de responsabilité unique ). Il est beaucoup plus naturel d'utiliser le modèle Decorator et d'implémenter la mise en cache comme décoration de l'action principale:
<?php use Illuminate\Contracts\Cache\Repository; final class CachedPostQueries implements PostQueries { const LASTS_DURATION = 10; private $base; private $cache; public function __construct( PostQueries $base, Repository $cache) { $this->base = $base; $this->cache = $cache; } public function getLastPosts() { return $this->cache->remember('last_posts', self::LASTS_DURATION, function(){ return $this->base->getLastPosts(); }); }
Ignorez l'interface du référentiel dans le constructeur. Pour une raison quelconque, ils ont décidé d'appeler l'interface de mise en cache dans Laravel.
La classe CachedPostQueries implémente uniquement la mise en cache. $ this-> cache-> souvenez-vous de vérifier si l'entrée donnée est dans le cache et sinon, il appelle le rappel et écrit la valeur retournée dans le cache. Il ne reste plus qu'à implémenter cette classe dans l'application. Nous avons besoin de toutes les classes qui dans l'application demandent l'implémentation de l'interface PostQueries pour recevoir une instance de la classe CachedPostQueries . Cependant, CachedPostQueries lui-même devrait recevoir la classe EloquentPostQueries en tant que paramètre dans le constructeur, car elle ne peut pas fonctionner sans une "réelle" implémentation. Modifier 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); } }
Tous mes souhaits sont tout naturellement décrits dans le fournisseur. Ainsi, nous avons implémenté la mise en cache pour nos demandes uniquement en écrivant une classe et en modifiant la configuration du conteneur. Le code pour le reste de l'application n'a pas changé.
Bien sûr, pour l'implémentation complète de la mise en cache, il est toujours nécessaire d'implémenter l'invalidation afin que l'article supprimé ne reste pas sur le site pendant un certain temps, mais qu'il quitte immédiatement (récemment écrit un article sur la mise en cache , il peut aider dans les détails).
Conclusion: nous avons utilisé non pas un, mais deux modèles entiers. Le modèle CQRS (Command Query Responsibility Segregation) offre une séparation complète des opérations de lecture et d'écriture au niveau de l'interface. Je suis venu vers lui via le principe de ségrégation d'interface , ce qui signifie que je manipule habilement les modèles et les principes et que j'en déduis l'un de l'autre comme un théorème :) Bien sûr, tous les projets n'ont pas besoin d'une telle abstraction sur des échantillons d'entité, mais je partagerai le focus avec vous. Au stade initial du développement d'applications, vous pouvez simplement créer une classe PostQueries avec une implémentation régulière via Eloquent:
<?php final class PostQueries { public function getById($id): Post { return Post::findOrFail($id); }
Lorsque le besoin se fait sentir pour la mise en cache, vous pouvez facilement créer une interface (ou une classe abstraite) à la place de cette classe PostQueries , copier son implémentation dans la classe EloquentPostQueries et passer au schéma que j'ai décrit précédemment. Le reste du code d'application n'a pas besoin d'être modifié.
Cependant, il reste un problème avec l'utilisation des mêmes entités Post qui peuvent modifier les données. Ce n'est pas exactement CQRS.

Personne ne dérange pour obtenir l'entité Post de PostQueries , la modifier et enregistrer les modifications avec un simple -> save () . Et ça marchera.
Après un certain temps, l'équipe passera à la réplication maître-esclave dans la base de données et PostQueries fonctionnera avec des réplicas en lecture. Les opérations d'écriture sur les réplicas en lecture sont généralement bloquées. L'erreur s'ouvrira, mais vous devrez travailler dur pour réparer tous ces montants.
La solution évidente consiste à séparer complètement les parties de lecture et d' écriture . Vous pouvez continuer à utiliser Eloquent, mais en créant une classe pour les modèles en lecture seule. Exemple: https://github.com/adelf/freelance-example/blob/master/app/ReadModels/ReadModel.php Toutes les opérations de modification des données sont bloquées. Créez un nouveau modèle, par exemple ReadPost (vous pouvez quitter Post , mais le déplacer vers un autre espace de noms):
<?php final class ReadPost extends ReadModel { protected $table = 'posts'; } interface PostQueries { public function getById($id): ReadPost; }
Ces modèles peuvent être en lecture seule et peuvent être mis en cache en toute sécurité.

Autre option: abandonner Eloquent. Il peut y avoir plusieurs raisons à cela:
- Tous les champs de table ne sont presque jamais nécessaires. Pour la demande lastPosts , seuls les champs id , title et, par exemple, published_at sont généralement requis. Demander quelques textes lourds de publications ne fera que charger inutilement la base de données ou le cache. Eloquent ne peut sélectionner que les champs obligatoires, mais tout cela est très implicite. Les clients de PostQueries ne savent pas exactement quels champs sont sélectionnés sans entrer dans l'implémentation.
- La mise en cache utilise la sérialisation par défaut. Les classes éloquentes prennent trop de place sous forme sérialisée. Si pour les entités simples, cela n'est pas particulièrement visible, alors pour les entités complexes avec des relations, cela devient un problème (comment faites-vous même glisser des objets d'une taille de mégaoctets depuis le cache?). Sur un projet, une classe ordinaire avec des champs publics dans le cache a pris 10 fois moins d'espace que l'option Eloquent (il y avait beaucoup de petites sous-entités). Vous ne pouvez mettre en cache des attributs que lors de la mise en cache, mais cela compliquera considérablement le processus.
Un exemple simple de ce à quoi cela pourrait ressembler:
<?php final class PostHeader { public int $id; public string $title; public DateTime $publishedAt; } final class Post { public int $id; public string $title; public string $text; public DateTime $publishedAt; } interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Bien sûr, tout cela ressemble à une super-complication sauvage de la logique. "Prenez les lunettes Eloquent et tout ira bien. Pourquoi trouver tout cela?" Pour les projets plus simples, c'est correct. Il n'est absolument pas nécessaire de réinventer les portées. Mais lorsque le projet est important et que plusieurs développeurs sont impliqués dans le développement, qui change souvent (ils arrêtent et de nouveaux arrivent), les règles du jeu deviennent légèrement différentes. Le code doit être protégé par écrit afin que le nouveau développeur après quelques années ne puisse pas faire quelque chose de mal. Bien sûr, il est impossible d'exclure complètement une telle probabilité, mais sa probabilité doit être réduite. De plus, il s'agit d'une décomposition système courante. Vous pouvez collecter tous les décorateurs et classes de mise en cache pour invalider le cache dans un certain "module de mise en cache", économisant ainsi le reste de l'application des informations sur la mise en cache. J'ai dû fouiller dans de grandes demandes entourées d'appels de cache. Cela fait obstacle. Surtout si la logique de mise en cache n'est pas aussi simple que celle décrite ci-dessus.