Explorons une implémentation du pattern Repository au sein d’une application Laravel.
Définition
Les design pattern, ou patrons de conception en français, sont des solutions communément utilisées pour répondre à une problématique donnée.
Ce sont des méthodes, des guides que chacun peut s'approprier à sa manière.
Parlons du Repository, son essence reside dans le fait qu'il permet de faire abstraction de la couche de donnée, que ce soit une base de donnée ou une API par exemple.
L'autre avantage à l'utiliser est qu'il permet de factoriser de la logique en son sein. Ce qui offre la possibilité d’utiliser ses méthodes dans divers contextes comme la création d'un article via un formulaire classique ou bien en ligne de commande.
Cas d'usage
Quoi de mieux qu’invoquer la Force pour illustrer ce design pattern. La fonctionnalité va consister à afficher, sur une page dédiée, le nom d’un personnage de Star Wars.
Afin d’illustrer l’implémentation voici un diagramme que nous allons détailler juste après :
Une première implémentation consistera à aller chercher la donnée en base, c’est la classe EloquentCharacterRepository
, la seconde aura pour source l’API Swapi dont la classe est ApiCharacterRepository
.
Toute deux auront pour contrat une interface CharacterRepository
qui sera le seul élément de notre implémentation connu par le reste de notre application.
Ce qui veut dire deux choses :
- Si une ou les deux implémentations évoluent en leur sein, le reste du code ne changera pas.
- Si nous produisons une nouvelle implémentation, le reste du code ne changera pas.
C’est le principe du design pattern, il est donc nécessaire de veiller à ce que les entrées et les sorties de toutes les implémentations soient les mêmes ainsi que les exceptions.
Définition des entrées et des sorties
Avant de partir sur nos implémentations, nous allons constituer ce que seront les entrées et les sorties de celles-ci. Et pour ça, rien que de mieux que le DTO !
Le premier consistera à représenter un identifiant car oui celui-ci peut-être de plusieurs natures, donc autant s’en prémunir :
1<?php 2 3namespace App\DataTransferObjects; 4 5class Identifier 6{ 7 public function __construct( 8 public mixed $value 9 ) {10 }11}
Ici rien de bien complexe, notre classe à une propriété qui contiendra la valeur de l’id quel que soit son type : un entier, un uuid, peu importe.
L’autre DTO que nous allons créer est le CharacterDto
qui représentera un personnage de Star Wars :
1<?php 2 3namespace App\DataTransferObjects; 4 5class CharacterDto 6{ 7 public function __construct( 8 public Identifier $id, 9 public string $name,10 ) {11 }12}
Celle classe récupère notre identifiant en tant que propriété, ainsi qu’une autre qui contiendra le nom du personnage.
Afin d’être complet dans cette section, nous allons d’ors et déjà implémenter la seule exception qui aura le droit d’être émise depuis notre Repository :
1<?php2 3namespace App\Exceptions;4 5use Exception;6 7class CharacterNotFoundException extends Exception8{9}
Comme son nom l’indique, celle-ci sera émise quand on ne trouvera pas notre personnage.
Nous verrons, un peu plus loin, dans le contrôleur comme en bénéficier afin de générer une page 404.
Implémentation depuis un modèle Eloquent
Nous sommes presque prêts à attaquer notre première implémentation, il nous manque pour cela son contrat que voilà :
1<?php 2 3namespace App\Repositories; 4 5use App\DataTransferObjects\CharacterDto; 6use App\DataTransferObjects\Identifier; 7 8interface CharacterRepository 9{10 public function find(Identifier $id): CharacterDto;11}
Cette interface définit les directives que doivent suivre nos implémentations et nous pouvons noter que les entrées/sorties de la méthode find
sont bien nos DTO.
Nous voilà prêt à attaquer notre première implémentation, celle dont la source de donnée est notre base. On utilisera pour cela la pleine puissance de l’ORM Eloquent :
1<?php 2 3namespace App\Repositories; 4 5use App\DataTransferObjects\CharacterDto; 6use App\DataTransferObjects\Identifier; 7use App\Exceptions\CharacterNotFoundException; 8use App\Models\Character; 9 10class EloquentCharacterRepository implements CharacterRepository11{12 public function find(Identifier $id): CharacterDto13 {14 $character = Character::where('id', $id->value)->first();15 16 if ( ! $character) {17 throw new CharacterNotFoundException();18 }19 20 return new CharacterDto($id, $character->name);21 }22}
Pour décrire notre méthode, nous récupérons notre personnage depuis son identifiant via notre ORM puis si aucun n’est trouvé, une exception est levée, sinon une instance de notre DTO est construite et retournée.
Implémentation depuis une API
Comme indiqué en introduction, nous allons utiliser SWAPI comme source de donnée en tant qu’API. Afin de faire l’appel, on utilisera le client http fournit par Laravel :
1<?php 2 3namespace App\Repositories; 4 5use App\DataTransferObjects\CharacterDto; 6use App\DataTransferObjects\Identifier; 7use App\Exceptions\CharacterNotFoundException; 8use Illuminate\Http\Client\Factory; 9 10class ApiCharacterRepository implements CharacterRepository11{12 public function __construct(13 private Factory $http,14 ) {15 }16 17 public function find(Identifier $id): CharacterDto18 {19 $response = $this->http->get('https://swapi.dev/api/people/' . $id->value);20 21 if ( ! $response->successful()) {22 throw new CharacterNotFoundException();23 }24 25 $body = json_decode($response->body());26 27 return new CharacterDto($id, $body->name);28 }29}
Afin d’effectuer l’appel à l’API, nous utilisons la librairie Http de Laravel.
Si la réponse n’est pas un succès, l’exception CharacterNotFoundException
est levée sinon la réponse est traitée pour en faire un CharacterDto
.
Utiliser nos implémentations
Afin de rendre notre exemple plus visuel, nous allons créer une route qui, depuis l’identifiant d’un personnage, rend une vue qui affiche le nom du personnage, tout cela en passant par un contrôleur qui utilisera notre Repository. Voici la route :
1Route::get(2 'characters/{id}',3 \App\Http\Controllers\CharacterController::class4);
Ainsi que le contenu de notre vue characters.show
qui recevra dans $character
une instance de CharacterDto
:
1@extends('layouts.app')2 3@section('content')4 <div class="h-screen flex items-center">5 <div class="w-full text-center text-9xl font-semibold text-gray-800">6 <h1>{{ $character->name }}</h1> 7 </div>8 </div>9@endsection
Maintenant, nous allons voir comment utiliser notre Repository au sein du contrôleur.
Il est important qu’à partir de maintenant, pour le reste de l’application qui va utiliser notre Repository, elle n’ai connaissance uniquement de l’interface
CharacterRepository
.
Visualisons cela via notre contrôleur CharacterController
:
1<?php 2 3namespace App\Http\Controllers; 4 5use App\DataTransferObjects\Identifier; 6use App\Exceptions\CharacterNotFoundException; 7use App\Repositories\CharacterRepository; 8use Illuminate\View\View; 9 10class CharacterController extends Controller11{12 public function __construct(13 private CharacterRepository $characterRepository,14 ) {15 }16 17 public function __invoke(Identifier $id): View18 {19 try {20 $character = $this->characterRepository->find($id);21 22 return view('characters.show', ['character' => $character]);23 } catch (CharacterNotFoundException $e) {24 abort(404);25 }26 }27}
Notre interface CharacterRepository
est injectée dans le constructeur du contrôleur.
A la méthode __invoke
, une instance Identifier
est passée, point sur lequel nous reviendrons un peu plus loin.
Au sein de cette méthode, nous allons rechercher le personnage ciblé depuis la méthode find
de notre Repository. Le CharacterDto
retourné est passé à la vue et si l’exception CharacterNotFoundException
est levée, elle est capturée et provoque une 404.
Et voilà, notre contrôleur, n’a aucune connaissance de l’implémentation utilisée, il n’a connaissance uniquement de notre Repository !
Bon, à partir d’ici, il nous reste quand même une étape et demie à réaliser pour que cela fonctionne :
- Indiquer à Laravel quelle implémentation utiliser
- Le bonus : utiliser l’auto-binding de Laravel pour instancier
Identifier
Pour spécifier à Laravel quelle implémentation nous souhaitons utiliser, nous allons créer un nouveau Service Provider intitulé RepositoryServiceProvider
via la commande suivante :
1php artisan make:provider RepositoryServiceProvider
Puis complétons la méthode register
ainsi :
1public function register()2{3 $this->app->bind(4 CharacterRepository::class,5 EloquentCharacterRepository::class,6 );7}
Ici, nous utilisons la méthode bind
qui permet d’indiquer à Laravel, via son système d’injection de dépendances, que lorsqu’il rencontrera une utilisation de l’interface CharacterRepository
, alors il devra instancier la classe concrète EloquentCharacterRepository
.
Si par la suite, nous souhaitons utiliser l’implémentation via API, il nous suffira de modifier uniquement cet appel comme suit :
1public function register()2{3 $this->app->bind(4 CharacterRepository::class,5 EloquentCharacterRepository::class, 6 ApiCharacterRepository::class, 7 );8}
Passons au petit bonus, l’instanciation d’un Identifier
via le système d’auto-binding de Laravel.
Toujours dans le RepositoryServiceProvider
mais cette fois cela se passe dans la méthode boot
:
1public function boot()2{3 Route::bind('id', function (mixed $value) {4 return new Identifier(value: $value);5 });6}
Nous indiquons au service Route
, que lorsqu’il rencontre une paramètre id
stipuler dans l’url, alors, il instancie un Identifier
qui reprend la valeur depuis l’url.
Rédiger des tests fonctionnels
Voilà une épreuve qui peut être fatidique : comment effectuer des tests sur le fonctionnement de notre contrôleur ayant pour dépendance une interface de notre Repository ?
Tout simplement, en le mockant ! On va pour cela utiliser la méthode instance
fournis par Laravel qui nous permet de définir notre dépendance, comme au sein du ServiceProvider, sauf qu’ici, nous sommes dans le contexte des tests.
Nous allons donc, pour notre dépendance CharacterRepository
, généré un mock, qui n’est finalement qu’une énième implémentation du repository :
1<?php 2 3namespace Tests\Feature; 4 5use App\DataTransferObjects\CharacterDto; 6use App\DataTransferObjects\Identifier; 7use App\Exceptions\CharacterNotFoundException; 8use App\Repositories\CharacterRepository; 9use Mockery;10use Mockery\MockInterface;11use Tests\TestCase;12 13class CharacterControllerTest extends TestCase14{15 /** @test */16 public function can_show_character()17 {18 $this->instance(19 CharacterRepository::class,20 Mockery::mock(CharacterRepository::class, function (MockInterface $mock) {21 $mock->shouldReceive('find')22 ->andReturn(23 new CharacterDto(new Identifier(66), 'Palpatine')24 );25 })26 );27 28 $response = $this->get('/characters/66');29 30 $response->assertSuccessful();31 $response->assertSee('Palpatine');32 }33}
On peut aller un peu plus loin et provoquer notre exception dédiée à travers le mock et vérifier que l’on obtient la page 404 souhaitée :
1/** @test */ 2public function cant_show_unknown_character() 3{ 4 $this->instance( 5 CharacterRepository::class, 6 Mockery::mock(CharacterRepository::class, function (MockInterface $mock) { 7 $mock->shouldReceive('find') 8 ->andThrow(CharacterNotFoundException::class); 9 })10 );11 12 $response = $this->get('/characters/66');13 14 $response->assertNotFound();15}
Oui, mais, est-ce pertinent ?
Bien entendu, c’est à vous de positionner le curseur entre le niveau de complexité à apporter et la livraison de votre fonctionnalité, en d’autres termes, rester pragmatique.
Le but ici étant surtout de prendre du recul sur la notion de Repository et d’identifier les conséquences potentielles au choix que l’on fait comme par exemple retourner un modèle Eloquent, à la place d’un DTO, qui fera implicitement d’autres appels à la base au sein du contrôleur ou même la vue. Autant de code à potentiellement modifier si vous changez de source de donnée.
Ceci étant dit, je n’ai personnellement que très rarement vu ce genre de changement au cours de ma carrière, assez anecdotique pour me questionner sur la nécessité d’apporter cette complexité en plus.
Enfin et afin de parfaire notre vision du pattern Repository, je vous conseil la très bonne conférence d’Arnaud Langlade qui vous fera voir les galinettes sous un autre angle.
Source : https://github.com/laravel-fr/support-pattern-repository
A lire
Autres articles de la même catégorie
Le pattern Pipeline
Laravel dispose d'un puissant service de Pipeline méconnu de la plupart des développeurs, explorons ensemble les possibilités que propose ce pattern !
Mathieu De Gracia
Traquer un utilisateur dans les logs
Laravel offre la possibilité d'ajouter un contexte unique à chaque ligne de log, voyons comment utiliser cette feature pour traquer les erreurs d'un utilisateur !
Mathieu De Gracia
#1 découverte FilamentPHP : installation & listing
Premier article de cette série de tutoriels, découvrons les bases de FilamentPHP en créant un listing des utilisateurs !
Mathieu De Gracia