Le design pattern Repository dans Laravel

Publié le 27 juin 2023 par William Suppo
Couverture de l'article Le design pattern Repository dans Laravel

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 :

Diagramme

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 :

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<?php
2 
3namespace App\Exceptions;
4 
5use Exception;
6 
7class CharacterNotFoundException extends Exception
8{
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 CharacterRepository
11{
12 public function find(Identifier $id): CharacterDto
13 {
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 CharacterRepository
11{
12 public function __construct(
13 private Factory $http,
14 ) {
15 }
16 
17 public function find(Identifier $id): CharacterDto
18 {
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::class
4);

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 Controller
11{
12 public function __construct(
13 private CharacterRepository $characterRepository,
14 ) {
15 }
16 
17 public function __invoke(Identifier $id): View
18 {
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 :

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 TestCase
14{
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
William Suppo avatar
William Suppo
Je mange du PHP au p'tit dej', et vous ?

A lire

Autres articles de la même catégorie