7 juillet 2023   |   De Jérôme Borg   |    Laravel

Injection de dépendance et Repository Pattern

Injection de dépendance et Repository Pattern

Injection de dépendance et Repository Pattern

Les design pattern (littéralement patron de conception), sont des modèles afin de répondre à certaine problématique.

Le repository pattern est un modèle de conception qui fournit une couche d'abstraction entre l'application et la couche de persistance des données. Il sépare la logique applicative de la couche d'accès aux données, ce qui permet aux développeurs de remplacer la couche d'accès aux données sans affecter les fonctionnalités de l'application.

Dans Laravel, le repository pattern est couramment utilisé pour gérer les accès aux modèles (bases de données). Au lieu d'accéder directement aux modèles, les développeurs créent un repository pattern qui sert d'interface entre l'application et la base de données. Le repository pattern gère toutes les opérations de la base de données et renvoie les données à l'application.

Dans le cas de Laravel, nous aurons un contrôleur qui fera appel au repository qui lui-même fera appel au model.

Cette conception permet :

- de séparer la couche de données

- de tester plus facilement l'accès aux données

- d'ajouter facilement de nouveaux composants 

Imaginons, que nous devons afficher une liste d'article, un article spécifique, et créer un article. Pour qu'un article soit visible, il faut qu'il soit publié, voici les requêtes que nous pourrions avoir dans nos contrôleurs

$posts = Post::where('is_published', true)->count();    
$posts = Post::where('is_published', true)->get();    
$posts = Post::where('is_published', true)->paginate();    
$post = Post::where('user_id', $user_id)->where('is_published', true)->first();    
$prevPost = Post::where('id', '<' $post->id)->where('user_id', $user_id)->where('is_published', true)->latest('id')->first();  
$nextPost = Post::where('id', '>' $post->id)->where('user_id', $user_id)->where('is_published', true)->oldest('id')->first();  
//....

A présent, le développeur change d'avis, et change is_published par is_validated, il va falloir intervenir dans toutes les méthodes de tous les contrôleurs, alors que si la couche de données est externe au contrôleurs, il suffira de modifier notre repository.

Prenons un exemple avec nos posts, pour faire très simple, un article a un titre, un contenu, une date de publication et un auteur

php artisan make:model Post -mcsf

 

Les données

Je créé le model Post et Laravel va créer la migration -m, le controler -c, le seeder -s, et la factory -f
Commençons par la migration

Schema::create('posts', function (Blueprint $table) {  
    $table->id();  
    $table->string('title', 255);  
    $table->text('body');  
    $table->boolean('is_published')->default(false);  
    $table->foreignUlid('user_id')->nullable()->constrained()->onDelete('cascade');  
    $table->timestamps();  
});

Puis le model Post, on ajoute la liste des champs qui sont modifiables et la méthode permettant d'accéder a l'auteur

protected $fillable = ['title', 'body', 'is_published', 'user_id'];  
  
    public function user() {  
        return $this->bellongsTo(User::class);  
    }

Enfin le model User, on ajoute la méthode permettant d'accéder a ses articles

public function posts() {  
        return $this->hasMany(Post::class);  
    }

Maintenant, il nous faut des données pour tester, on va passer à la factory et aux seeders, PostFactory

On récupère en premier aléatoirement un auteur, puis grâce à Faker, on rempli avec des données

public function definition(): array  
    {  
        $user = User::inRandomOrder()->first();  
        return [  
            'title' => fake()->sentence($nbWords = 6, $variableNbWords = true),  
            'body' => fake()->paragraph($nbSentences = 3, $variableNbSentences = true),  
            'is_published' => rand(0,1),  
            'user_id' => $user->id  
        ];  
    }

Ensuite on rempli, UserSeeder

public function run(): void  
    {  
        User::factory(5)->create();  
    }

PostSeeder

public function run(): void  
    {  
        Post::factory(10)->create();  
    }

Ne reste plus qu'a insérer les données dans la base

php artisan migrate  
php artisan db:seed --class=UserSeeder  
php artisan db:seed --class=PostSeeder

a présent, nous avons dans la base 5 auteurs et 10 articles, nous pouvons vérifier dans phpMyAdmin ou via tinker

php artisan tinker  
> App\Models\User::count()                                                                                                                       
= 5    
> App\Models\Post::count()                                                                                                                       
= 10

 

Le repository

Laravel ne possède pas de commande spécifique pour créer un repository et une interface, mais c'est du php standard, commençons par créer les dossiers.

Dans le dossier \App, créons le dossier Repositories : App\Repositories

Dans le dossier \App\Repositories, créons le dossier Interfaces : App\Repositories\Interfaces

Commençons par créer un fichier pour l'interface  App\Repositories\Interfaces\PostRepositoryInterface.php dans lequel on défini les prototypes des méthodes dont on aura besoin

namespace App\Repositories\Interfaces;  
  
use App\Models\User;  
use http\Client\PostRequest;  
  
interface PostRepositoryInterface {  
    public function getById($id);  
    public function getAll();  
    public function getAllByUser(User $user);  
    public function store(PostRequest $request, User $user);  
}

J'ai besoin :

- de récupérer un post par son id

- de récupérer tous les posts

- de récupérer tous les posts d'un auteur

- de créer un post

Comme vous avez pu le constater pour la méthode store, on injecte un Form Request, ce sont des classes de validation de données (voir l'article sur la validation)

php artisan make:request PostRequest
class PostRequest extends FormRequest  
{  
    public function authorize(): bool  
    {  
        return true;  
    }  
  
    public function rules(): array  
    {  
        return [  
            'title' => 'required|string|max:255',  
            'body' => 'sometimes',  
            'is_published' => 'required',  
        ];  
    }  
}

PostRequest va nous permettre de vérifier que les champs nécessaires respectent certaines règles, comme par exemple le titre ne peux dépasser 255 caractères, si ces règles sont respectées, laravel renverra ces données aux contrôleurs, et nous pourrons les récupérer avec $request->validated()

Maintenant que l'interface est faite, on va pouvoir implémenter les différentes méthodes dans App\Repositories\PostRepository.php

namespace App\Repositories;  
  
use App\Models\Post;  
use App\Repositories\Interfaces\PostRepositoryInterface;  
use App\Models\User;  
use App\Http\Requests\PostRequest;  
  
class PostRepository implements PostRepositoryInterface {  
  
    public function getById($id): Post|null {  
        return Post::find($id);  
    }  
  
    public function getAll(): Post|null {  
        return Post::all();  
    }  
  
    public function getAllByUser(User $user): Post|null {  
        return Post::where('user_id', $user->id)->all();  
    }  
  
    public function store(PostRequest $request, User $user): Post|null {  
        return $user->posts()->create($request->validated());  
    }  
}

Nous créons la classe PostRepository qui faire référence a l'interface PostRepositoryInterface (implements), on doit donc respecter le contrat d'implémenter toutes les définitions de méthode de l'interface

getById, getAll, getAllByUser, ce sont de simples méthodes

store : on récupère les données et un utilisateur, on aurait pu écrire

return Post::create([  
            'title' => $request->title,  
            'body' => $request->body,  
            'is_published' => $request->is_published,  
            'user' => $user->id,  
        ]);

Mais la relation Posts de $user, nous permet d'injecter directement user_id a la création

return $user->posts()->create($request->validated());

 

Le contrôleur

Passons à présent à notre contrôleur, a fin d'utiliser notre repository, nous allons devoir l'injecter, pour cela nous allons utiliser le "conteneur de services de Laravel".

À la base, le conteneur de services de Laravel est un conteneur d'injection de dépendances (DI) qui gère l'instanciation des objets et de leurs dépendances. En termes plus simples, le conteneur de services est un conteneur qui contient et gère tous les objets et services dont votre application a besoin pour fonctionner. Le conteneur de services est responsable de la création de ces objets et de leurs dépendances, puis de leur injection dans le code de votre application lorsqu'ils sont nécessaires. Consulter l'article sur les services contener

Dans le fichier App\Providers\AppServiceProvider, dans la méthode register, on utilise la méthode bind, qui va faire correspondre des fichiers

public function register(): void  
    {  
        $this->app->bind(PostRepositoryInterface::class, PostRepository::class);  
    }

Dans le contrôleur, il faut injecter cette dépendance, avant php 8, on aurait écrit

protected $postRepository,  
    public function __construct(PostRepositoryInterface $postRepository) {  
        $this->postRepository = $postRepository;  
    }

Avec php 8, on peut écrire 

public function __construct(protected PostRepositoryInterface $postRepository) 
{  
}
class PostController extends Controller  
{  
    public function __construct(protected PostRepositoryInterface $postRepository) {  
    }  
  
    public function index() {  
        $posts = $this->postRepository->getAll();  
  
        return view('posts.index', compact('posts'));  
    }  
  
    public function show($id) {  
        $post = $this->postRepository->getById();  
  
        return view('posts.show', compact('post'));  
    }  
  
    public function indexAuthor($user) {  
        $post = $this->postRepository->getAllByUser($user);  
  
        return view('posts.author', compact('user','posts'));  
    }  
  
    public function store(PostRequest $request, User $user) {  
        $post = $this->postRepository->store($request, $user);  
          
        return redirect()->route('posts.index');  
    }  
}

Grace a l'injection de dépendance, la class PostRepository et ses méthodes, sont accessibles de n'importe ou dans notre contrôleur, on va donc pouvoir utiliser toutes les méthode de notre repository. Le contrôleur devient plus léger (moins de code), plus lisible, et en cas de modification du model Post, il n'y aura aucun impact sur le ou les contrôleurs (du front et/ou back). 

De Jérôme Borg
Le 7 juillet 2023
Temps de lecture : 15 min
Jérôme Borg
Jérôme Borg

Développeur fullstack laravel/VueJs, formateur

Tous les articles de cet auteur
Articles recommandés
La validation
La validation
Jérôme Borg De Jérôme Borg | 24 avril 2023 | Lu : 15min
Installer Laravel 12
Installer Laravel 12
Jérôme Borg De Jérôme Borg | 15 mars 2025 | Lu : 5min