
Comment écrire un bon contrôleur ? L'une des erreurs les plus courantes à l'exemple de Laravel
Tout le monde apprend de ses erreurs. Dans cet article, je vais souligner une erreur fondamentale, qui mène à une situation où le contrôleur contient plusieurs centaines de lignes de code et devient illisible et très difficile à maintenir. Cependant, je vais présenter quelques solutions pour y remédier.
Lors de l'implémentation des services de développement PHP chez Droptica, j'ai rencontré cette erreur de nombreuses fois, et je l'ai même commise moi-même au début. De quoi s'agit-il ? De mettre toute la logique dans les méthodes du contrôleur sans diviser les activités individuelles en composants distincts dans l'application.
Pour éviter cela, suivez deux règles principales :
- Vous pouvez avoir un nombre illimité de contrôleurs. Il est préférable d'en avoir plusieurs qui contiennent peu de méthodes plutôt qu'un seul contrôleur qui en contient beaucoup.
- Le contrôleur est uniquement responsable de la logique générale. En suivant le principe du général au particulier, il doit y avoir une partie générale dans le contrôleur. Les particularités, en revanche, doivent être incluses dans d'autres composants de l'application. Moins il y a de code dans le contrôleur, mieux c'est.
Examinons l'exemple ci-dessous :
class CustomersController extends Controller
{
public function getActiveCustomers()
{
return Customer::where('active', 1)->get();
}
public function store()
{
$data = $this->validateRequest();
$customer = Customer::create($data);
// Envoyer un e-mail de notification à l'administrateur.
Mail::to('[email protected]')->send(new NewCustomerMail($customer));
// Envoyer un e-mail de bienvenue au client
Mail::to($customer->email)->send(new WelcomeMail($customer));
}
public function getQueryResults()
{
$customers = Customer::query();
if (request()->has('name')) {
$customers->where('name', request('name'));
}
if (request()->has('active')) {
$customers->where('active', request('active'));
}
if (request()->has('sort')) {
$customers->orderBy('name', request('sort'));
}
$customers->get();
return $customers;
}
private function validateRequest()
{
// Le code de validation de la requête se trouve ici…
}
}
À première vue, il peut sembler que tout est comme il se doit. Le contrôleur contient trois méthodes. La première de ces méthodes retourne les clients actifs, la deuxième crée un nouveau client et envoie des notifications par e-mail, tandis que la troisième est responsable du filtrage des résultats de recherche. Alors, pourquoi cet exemple est-il présenté comme incorrect ?
CRUD
Commençons par le premier exemple. Selon la documentation de Laravel, le contrôleur doit contenir les méthodes index, create, store, show, edit, update ou/et delete. Par conséquent, la méthode doit être renommée en l'une de celles-ci. Mais pour que cela ait du sens, vous devez suivre la première règle. Alors, créons un tout nouveau contrôleur – ActiveCustomersController, et déplaçons-y la méthode getActiveCustomers, tout en changeant son nom en index.
class ActiveCustomersController extends Controller
{
public function index()
{
return Customer::where('active', 1)->get();
}
}
Rappelez-vous que si votre contrôleur ne contient qu'une seule méthode, dans Laravel vous pouvez renommer cette méthode en __invoke().
class ActiveCustomersController extends Controller
{
public function __invoke()
{
return Customer::where('active', 1)->get();
}
}
Ce n'est pas la tâche du contrôleur
Maintenant, examinons la méthode suivante : store(). Son nom est correct, tout comme celui du contrôleur. Alors où se cache l'erreur ?
Dans cet exemple, après la création d'un nouveau client, deux messages e-mail sont envoyés (au client et à l'administrateur). Ce n'est pas énorme, mais imaginons ce qui se passera si à l'avenir vous voulez ajouter une notification Slack et envoyer un SMS (par exemple avec un code de vérification). La quantité de code augmentera considérablement, enfreignant ainsi la deuxième règle. Par conséquent, nous devrions chercher un autre endroit dans notre application qui sera responsable de l'envoi de notifications. Les événements sont parfaits à cet effet.
Commençons donc par déclencher l'événement qui remplacera le code responsable de l'envoi des notifications :
public function store()
{
$data = $this->validateRequest();
$customer = Customer::create($data);
event(new NewCustomerHasRegisteredEvent($customer));
}
Ensuite, créez cet événement en tapant dans la console :
php artisan make:event NewCustomerHasRegisteredEvent
Cette commande créera une nouvelle classe – NewCustomerHasRegisteredEvent dans le répertoire app/Events. Comme dans le contrôleur vous transférez le client créé à l'aide d'un paramètre, vous devez accepter ces données dans le constructeur de la classe événement.
Le tout devrait ressembler à ceci :
class NewCustomerHasRegisteredEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $customer;
public function __construct($customer)
{
$this->customer = $customer;
}
}
Ensuite, vous devez créer des écouteurs qui exécuteront l'action appropriée (dans ce cas – envoyer un message e-mail) si l'événement est déclenché. Encore une fois, tapons dans la console :
php artisan make:listener WelcomeNewCustomerListener
Cette fois, une nouvelle classe WelcomeNewCustomerListener devrait être créée dans le répertoire app/Listeners. Tout ce que vous avez à faire dans ce cas est de mettre le code responsable de l'envoi de l'e-mail dans la méthode handle(). Cette méthode prend le paramètre $event, qui contiendra les données du client transférées dans le constructeur de la classe NewCustomerHasRegisteredEvent.
class WelcomeNewCustomerListener
{
public function handle($event)
{
Mail::to($event->customer->email)->send(new WelcomeMail($event->customer));
}
}
Essayez maintenant de créer par vous-même un autre écouteur responsable de l'envoi d'un e-mail à l'administrateur, en l'appelant par exemple NewCustomerAdminNotificationListener.
Vous avez un événement et des écouteurs créés. Enfin, vous devez tout lier ensemble pour que les écouteurs appropriés soient lancés lorsque l'événement approprié est déclenché. Pour cela, ajoutez votre événement et vos écouteurs au tableau $listen dans la classe App\Providers\EventServiceProvider :
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
NewCustomerHasRegisteredEvent::class => [
WelcomeNewCustomerListener::class,
NewCustomerAdminNotificationListener::class,
],
];
}
Pipelines
Le dernier exemple pour aujourd'hui sera légèrement différent. Vous savez déjà d'après le premier exemple que dans ce cas, vous devez commencer par créer un nouveau contrôleur, par exemple CustomersQueryController, mais vous ne vous arrêterez pas là. Le code contenu dans la méthode n'est pas mauvais ; cependant, cela peut être fait de manière plus efficace. De plus, vous utiliserez une fonction qui est souvent utilisée dans Laravel, mais il n'y a pas beaucoup d'informations à ce sujet dans la documentation officielle.
Les pipelines ne sont rien d'autre qu'une série de étapes/tâches que vous souhaitez réaliser l'une après l'autre. Cela sera parfait dans le cas où, selon les données que l'utilisateur souhaite obtenir, vous devez passer par une série d'étapes pour construire une requête appropriée en utilisant Eloquent.
Commençons donc par déplacer la méthode getQueryResults vers un contrôleur séparé et en la renommant :
class CustomersQueryController extends Controller
{
public function index()
{
$customers = Customer::query();
if (request()->has('name')) {
$customers->where('name', request('name'));
}
if (request()->has('active')) {
$customers->where('active', request('active'));
}
if (request()->has('sort')) {
$customers->orderBy('name', request('sort'));
}
$customers->get();
return $customers;
}
}
Ensuite, dans le répertoire app, créez un nouveau répertoire QueryFilters et trois classes à l'intérieur : name, active, sort. Chacune pour un paramètre de recherche. Cette classe contiendra une méthode handle() qui prendra deux paramètres : $request et $next. Je vais maintenant présenter la classe active à titre d'exemple. Pour la pratique, essayez de préparer vous-même les deux autres classes.
class Active
{
public function handle($request, \Closure $next)
{
}
}
Dans la méthode handle, vous devrez mettre la condition opposée à celle qui était dans le contrôleur jusqu'à présent. Si la requête ne contient pas le paramètre 'active', alors passez à l'étape suivante dans le pipeline. Sinon, ajoutez la condition appropriée au builder et continuez :
public function handle($request, \Closure $next)
{
if (!request()->has('active')) {
return $next($request);
}
$builder = $next($request);
return $builder->where('active', request('active'));
}
Une fois que vous avez préparé les deux autres classes de manière similaire, il ne vous reste plus qu'à tout lier ensemble. Ainsi, retournons au contrôleur et passons toutes les classes susmentionnées à travers le pipeline :
public function index()
{
$customers = app(Pipeline::class)
->send(Customer::query())
->through([
\App\QueryFilters\Active::class,
\App\QueryFilters\Name::class,
\App\QueryFilters\Sort::class,
])
->thenReturn();
return $customers->get();
}
Résumé
Dans cet article, j'ai présenté trois façons d'extraire la logique d'un contrôleur. Bien sûr, vous n'avez pas à vous limiter à celles-ci – il y a beaucoup plus de façons. Vous pouvez lire sur les fonctions Laravel utiles que tout le monde ne connaît pas.
Alors, souvenez-vous de garder le moins de code possible dans le contrôleur. Ce code ne devrait contenir que la logique générale. Vous devez mettre tous les éléments plus détaillés dans un endroit distinct. Et demandez-vous toujours : Est-ce vraiment une tâche pour le contrôleur ?