Créer un service réutilisable ne consiste pas seulement à écrire une classe qui fonctionne. L'objectif est surtout de réfléchir à la surface minimale dont l'application a besoin, pour pouvoir remplacer facilement l'implémentation plus tard.
Pour illustrer cette démarche, on va partir d'un formulaire de contact que l'on souhaite protéger avec un CAPTCHA, sans se lier définitivement à Cloudflare Turnstile, Google reCAPTCHA ou hCaptcha. La logique repose sur quelques briques Laravel : un Service Provider pour câbler les classes, un contrat pour définir l'interface commune, un composant Blade pour afficher le CAPTCHA et une règle de validation pour vérifier la réponse.
Partir du besoin réel
Avant de créer les classes, le plus important est d'observer le fonctionnement des différents systèmes que l'on veut supporter. Dans le cas des CAPTCHA, la structure est globalement similaire :
- on ajoute un morceau de HTML dans le formulaire pour afficher le CAPTCHA.
- on récupère une valeur soumise avec le formulaire.
- on envoie cette valeur à une API pour savoir si la vérification est valide.
À partir de là, on peut identifier deux besoins principaux :
- rendre le HTML nécessaire dans le formulaire.
- vérifier les données soumises côté serveur.
Cette étape permet d'éviter de commencer directement par une implémentation spécifique. Si on code tout autour de Turnstile dès le départ, il sera plus difficile de remplacer ce système par hCaptcha ou reCAPTCHA ensuite.
Organiser le code par domaine
Comme ce système concerne la lutte contre le spam, on peut regrouper le code dans un dossier dédié :
L'idée est de garder tout ce qui concerne cette fonctionnalité au même endroit. Cela rend le code plus facile à déplacer d'un projet à l'autre, et plus simple à comprendre quand on revient dessus plus tard.
Le SpamServiceProvider sert uniquement à initialiser et connecter les éléments entre eux. Il contiendra les 2 méthodes classiques : register() pour enregistrer les services dans le container, et une méthode boot() pour initialiser ce qui touche à Blade ou aux vues.
Il faudra ensuite enregistrer ce provider dans la configuration générale de l'application, par exemple dans bootstrap/providers.php.
Créer un contrat minimal
Le contrat est la pièce la plus importante du système. C'est lui qui décrit ce que tous les providers de CAPTCHA devront être capables de faire.
Au départ, le composant Blade a seulement besoin de rendre une vue. On peut donc créer une première méthode render() :
Ensuite, lorsque l'on met en place la validation, on découvre un nouveau besoin : vérifier une valeur reçue depuis le formulaire. On ajoute alors une méthode verify() :
Enfin, comme le nom du champ varie selon les services (cf-turnstile-response pour Turnstile, autre chose pour hCaptcha ou reCAPTCHA), le contrat doit aussi permettre de récupérer ce nom :
Le point important est de ne pas surcharger ce contrat. On pourrait être tenté d'y ajouter directement une méthode pour générer toutes les règles de validation, mais ce n'est pas indispensable au fonctionnement du CAPTCHA lui-même. Plus le contrat reste petit, plus il sera facile de créer de nouvelles implémentations.
Afficher le CAPTCHA avec un composant Blade
Pour rendre le CAPTCHA dans les formulaires, on peut créer un composant Blade Captcha. Ce composant ne connaît pas Turnstile, hCaptcha ou reCAPTCHA. Il dépend seulement du contrat.
Le composant délègue le rendu au service configuré. Cela permet ensuite d'utiliser la même syntaxe dans tous les formulaires :
Le provider se charge d'enregistrer le composant :
Valider les données avec une règle dédiée
La validation doit aussi passer par le contrat. On crée donc une règle Laravel qui reçoit le service CAPTCHA et appelle sa méthode verify().
Le message pourrait être remplacé par une clé de traduction si l'application est multilingue. L'objectif reste le même : la règle ne sait pas comment Turnstile ou hCaptcha fonctionnent, elle sait seulement qu'un service compatible avec CaptchaContract peut vérifier une valeur.
Générer les règles avec une factory
Comme le nom du champ dépend du provider utilisé, on peut créer une petite factory dédiée aux règles de validation. Elle évite de dupliquer la logique dans chaque FormRequest.
On peut ensuite l'utiliser dans une FormRequest :
On garde ainsi une signature très légère dans l'application : un composant dans le formulaire et une ligne pour ajouter les règles de validation.
Prévoir une configuration interchangeable
Pour choisir le provider utilisé, on peut s'inspirer des configurations Laravel comme le système de fichiers ou les connexions à la base de données. On définit un driver par défaut, puis une liste de providers avec leurs options.
Le driver none peut servir pendant les tests ou en développement. Il rend un champ caché et laisse toujours passer la validation.
La vue peut rester dans le dossier du domaine, par exemple domains/Spam/Resources/views/none.blade.php :
Pour que Laravel trouve ces vues, le service provider peut déclarer un namespace :
Câbler les implémentations dans le service provider
Le SpamServiceProvider fait le lien entre le contrat et l'implémentation concrète. Quand une classe demande un CaptchaContract, le container Laravel reçoit le provider correspondant à la configuration.
Pour Turnstile, on crée une implémentation dédiée qui reçoit les options nécessaires.
Ajouter de nouveaux providers
Une fois le contrat stabilisé, ajouter hCaptcha ou reCAPTCHA revient à créer de nouvelles classes qui implémentent CaptchaContract, puis à ajouter les vues et la configuration correspondantes. Le reste de l'application ne change pas : le formulaire utilise toujours <x-captcha /> et les règles de validation passent toujours par CaptchaRulesFactory.
C'est l'intérêt principal de cette architecture : chaque provider peut avoir sa propre logique interne, mais tous exposent la même interface au reste du code. On peut donc changer de service en modifiant la configuration plutôt qu'en réécrivant les formulaires et les validations.
Garder une surface d'intégration réduite
Pour qu'un service soit vraiment réutilisable, il faut limiter ce que l'application doit connaître. Ici, l'intégration se résume à deux endroits :
- ajouter le composant CAPTCHA dans le formulaire.
- ajouter les règles générées par la factory dans la validation.
Le reste est isolé dans le domaine Spam. Cette séparation rend aussi le code plus simple à faire évoluer, voire à générer partiellement avec une IA, parce que la structure est claire : un contrat, des providers, des vues et un service provider pour assembler le tout.
Il reste toujours possible d'améliorer le système, par exemple en affichant les erreurs de validation directement au niveau du rendu du CAPTCHA. Mais la base est solide : le contrat reste simple, les implémentations sont interchangeables et le code spécifique à l'application reste succinct.