La semaine dernière, j'ai écrit , mais j'ai promis de parler de la façon dont on peut l'utiliser partiellement à bon escient. Pour cela, je vais essayer d'analyser comment ce modèle est généralement utilisé dans les projets. L'ensemble minimal de méthodes pour un repository est:
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
}Cependant, dans les projets réels, si l'utilisation des repositories a été décidée, ils sont souvent dotés de méthodes pour récupérer des enregistrements :
<?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 mises en œuvre via des scopes Eloquent, mais surcharger les classes d'entités avec la responsabilité de récupérer elles-mêmes leurs données n'est pas la meilleure idée, et transférer cette responsabilité aux classes des repositories semble logique. Est-ce vraiment le cas ? J'ai délibérément séparé 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 :
- la construction d'un nouvel objet et l'appel de PostRepository::save
- PostRepository::getById, la manipulation de l'entité et l'appel PostRepository::save
- l'appel PostRepository::delete
Dans les opérations d'écriture, il n'y a pas d'utilisation des méthodes de récupération. En revanche, dans les opérations de lecture, seules les méthodes get* sont utilisées. Si l'on lit sur le Principe de Ségrégation des Interfaces (la lettre I dans SOLID), il devient clair que notre interface est devenue trop grande et remplit au moins deux responsabilités différentes. Il est temps de la diviser en deux. La méthode getById est nécessaire dans les deux, mais avec la complexité croissante de l'application, ses implémentations seront différentes. Nous le verrons un peu plus tard. J'ai déjà écrit précédemment sur l'inutilité de la partie écriture, donc dans cet article, je vais simplement l'omettre.
La partie lecture me semble en revanche moins inutile, car même pour Eloquent, il peut y avoir plusieurs implémentations. Quel nom donner à la classe ? On peut dire ReadPostRepository, mais par rapport au patron Repository , elle est déjà peu pertinente. On peut simplement dire PostQueries:
<?php
interface PostQueries
{
public function getById($id): Post;
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}Sa réalisation avec Eloquent est assez simple :
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 reliée à l'implémentation, par exemple dans AppServiceProvider:
app->bind(PostQueries::class,
EloquentPostQueries::class);
}
}Cette classe est déjà utile. Elle remplit sa responsabilité, déchargeant ainsi soit les contrôleurs, soit la classe d'entité. Dans le contrôleur, elle peut être utilisée comme suit :
$postQueries->getLastPosts(),
]);
}
} Méthode PostsController::lastPosts demande simplement à avoir une implémentation quelconque PostsQueries et travaille avec elle. Dans le fournisseur, nous avons lié PostQueries à la classe EloquentPostQueries et dans le contrôleur, cette classe sera insérée.
Imaginons que notre application soit devenue très populaire. Des milliers d'utilisateurs ouvrent par minute la page des 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 une telle charge, c'est pourquoi elles utilisent une solution standard : le cache. En plus de la base de données, un certain instantané des 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 complexe, mais l'implémenter dans EloquentPostQueries n'est pas très correct (ne serait-ce que pour le Single Responsibility Principle). Il est beaucoup plus naturel d'utiliser le patron Décorateur et d'implémenter le cache en tant que décoration de l'action principale :
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();
});
}
// d'autres méthodes sont pratiquement les mêmes
}Ne vous préoccupez pas de l'interface Repository dans le constructeur. Pour une raison inexplicable, c'est ainsi que l'interface de mise en cache a été nommée dans Laravel.
Classe CachedPostQueries implémente uniquement la mise en cache. $this->cache->remember vérifie s'il n'y a pas d'entrée dans le cache et, si ce n'est pas le cas, appelle le callback et enregistre la valeur retournée dans le cache. Il ne reste plus qu'à intégrer cette classe dans l'application. Nous devons faire en sorte que toutes les classes qui demandent l'implémentation de l'interface PostQueries reçoivent une instance de la classe CachedPostQueries. Pourtant, lui-même CachedPostQueries comme paramètre dans le constructeur doit recevoir la classe EloquentPostQueries, car il ne peut pas fonctionner sans une véritable implémentation. Changeons AppServiceProvider:
app->bind(PostQueries::class,
CachedPostQueries::class);
$this->app->when(CachedPostQueries::class)
->needs(PostQueries::class)
->give(EloquentPostQueries::class);
}
}Tous mes souhaits sont décrits de manière assez naturelle dans le fournisseur. Ainsi, nous avons réalisé la mise en cache de nos requêtes simplement en écrivant une classe et en changeant la configuration du conteneur. Le reste du code de l'application n'a pas changé.
Bien sûr, pour une mise en cache complète, il est également nécessaire de mettre en œuvre l'invalidation afin qu'un article supprimé ne reste pas sur le site pendant un certain temps mais soit supprimé immédiatement. Mais ce n'est déjà que des détails.
En résumé : nous avons utilisé non pas un, mais deux modèles. Le modèle Command Query Responsibility Segregation (CQRS) propose de complètement séparer les opérations de lecture et d'écriture au niveau des interfaces. J'y suis arrivé par Principe de Ségrégation des Interfaces, ce qui montre que je manie habilement les modèles et les principes, en déduisant l'un de l'autre comme un théorème 🙂 Bien sûr, tous les projets n'ont pas besoin d'une telle abstraction pour les récupérations d'entités, mais je vais partager une astuce. À la phase initiale du développement de l'application, vous pouvez simplement créer une classe PostQueries avec une implémentation normale via Eloquent :
<?php
final class PostQueries
{
public function getById($id): Post
{
return Post::findOrFail($id);
}
// autres méthodes
}Lorsque la nécessité de mise en cache se fera sentir, il suffit de créer un interface (ou une classe abstraite) à la place de cette classe PostQueries, copier son implémentation dans le classe EloquentPostQueries et de passer au schéma que j'ai décrit précédemment. Le reste du code de l'application n'a pas besoin d'être modifié.
Tous ces trucs avec les classes, interfaces, Dependency Injection et CQRS sont décrits en détail dans . C'est là que réside le mystère de pourquoi toutes mes classes dans les exemples de cet article sont marquées comme final.
Source : habr.com
