Injection de dépendance et Repository Pattern
<p><strong>Injection de dépendance et Repository Pattern</strong></p>
<p>Les design pattern (littéralement patron de conception), sont des modèles afin de répondre à certaine problématique.</p>
<p>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.</p>
<p>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.</p>
<p>Dans le cas de Laravel, nous aurons un contrôleur qui fera appel au repository qui lui-même fera appel au model.</p>
<p>Cette conception permet :</p>
<p>- de séparer la couche de données</p>
<p>- de tester plus facilement l'accès aux données</p>
<p>- d'ajouter facilement de nouveaux composants </p>
<p>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</p>
<pre class="language-php"><code>$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();
//....</code></pre>
<p>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.</p>
<p>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</p>
<pre class="language-bash"><code>php artisan make:model Post -mcsf</code></pre>
<p> </p>
<p><strong>Les données</strong></p>
<p>Je créé le model Post et Laravel va créer la migration -m, le controler -c, le seeder -s, et la factory -f<br>Commençons par la migration</p>
<pre class="language-php"><code>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();
});</code></pre>
<p>Puis le model Post, on ajoute la liste des champs qui sont modifiables et la méthode permettant d'accéder a l'auteur</p>
<pre class="language-php"><code>protected $fillable = ['title', 'body', 'is_published', 'user_id'];
public function user() {
return $this->bellongsTo(User::class);
}</code></pre>
<p>Enfin le model User, on ajoute la méthode permettant d'accéder a ses articles</p>
<pre class="language-php"><code>public function posts() {
return $this->hasMany(Post::class);
}</code></pre>
<p>Maintenant, il nous faut des données pour tester, on va passer à la factory et aux seeders, PostFactory</p>
<p>On récupère en premier aléatoirement un auteur, puis grâce à Faker, on rempli avec des données</p>
<pre class="language-php"><code>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
];
}</code></pre>
<p>Ensuite on rempli, UserSeeder</p>
<pre class="language-php"><code>public function run(): void
{
User::factory(5)->create();
}</code></pre>
<p>PostSeeder</p>
<pre class="language-php"><code>public function run(): void
{
Post::factory(10)->create();
}</code></pre>
<p>Ne reste plus qu'a insérer les données dans la base</p>
<pre class="language-bash"><code>php artisan migrate
php artisan db:seed --class=UserSeeder
php artisan db:seed --class=PostSeeder</code></pre>
<p>a présent, nous avons dans la base 5 auteurs et 10 articles, nous pouvons vérifier dans phpMyAdmin ou via tinker</p>
<pre class="language-php"><code>php artisan tinker
> App\Models\User::count()
= 5
> App\Models\Post::count()
= 10</code></pre>
<p> </p>
<p><strong>Le repository</strong></p>
<p>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.</p>
<p>Dans le dossier \App, créons le dossier Repositories : App\Repositories</p>
<p>Dans le dossier \App\Repositories, créons le dossier Interfaces : App\Repositories\Interfaces</p>
<p>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</p>
<pre class="language-php"><code>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);
}</code></pre>
<p>J'ai besoin :</p>
<p>- de récupérer un post par son id</p>
<p>- de récupérer tous les posts</p>
<p>- de récupérer tous les posts d'un auteur</p>
<p>- de créer un post</p>
<p>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 (<a href="https://the-blob.io/posts/la-validation" target="_blank" rel="noopener">voir l'article sur la validation</a>)</p>
<pre class="language-bash"><code>php artisan make:request PostRequest</code></pre>
<pre class="language-php"><code>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',
];
}
}</code></pre>
<p>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 <strong>$request->validated()</strong></p>
<p>Maintenant que l'interface est faite, on va pouvoir implémenter les différentes méthodes dans App\Repositories\PostRepository.php</p>
<pre class="language-php"><code>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());
}
}</code></pre>
<p>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</p>
<p>getById, getAll, getAllByUser, ce sont de simples méthodes</p>
<p>store : on récupère les données et un utilisateur, on aurait pu écrire</p>
<pre class="language-php"><code>return Post::create([
'title' => $request->title,
'body' => $request->body,
'is_published' => $request->is_published,
'user' => $user->id,
]);</code></pre>
<p>Mais la relation Posts de $user, nous permet d'injecter directement user_id a la création</p>
<pre class="language-php"><code>return $user->posts()->create($request->validated());</code></pre>
<p> </p>
<p><strong>Le contrôleur</strong></p>
<p>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".</p>
<p>À 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. <a href="https://medium.com/@soulaimaneyh/laravel-service-container-488c52ecd627" target="_blank" rel="noopener">Consulter l'article sur les services contener</a></p>
<p>Dans le fichier App\Providers\AppServiceProvider, dans la méthode register, on utilise la méthode bind, qui va faire correspondre des fichiers</p>
<pre class="language-php"><code>public function register(): void
{
$this->app->bind(PostRepositoryInterface::class, PostRepository::class);
}</code></pre>
<p>Dans le contrôleur, il faut injecter cette dépendance, avant php 8, on aurait écrit</p>
<pre class="language-php"><code>protected $postRepository,
public function __construct(PostRepositoryInterface $postRepository) {
$this->postRepository = $postRepository;
}</code></pre>
<p>Avec php 8, on peut écrire </p>
<pre class="language-php"><code>public function __construct(protected PostRepositoryInterface $postRepository)
{
}</code></pre>
<pre class="language-php"><code>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');
}
}</code></pre>
<p>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). </p>
De Jérôme Borg
Le 7 juillet 2023
Temps de lecture : 15 min
Le 7 juillet 2023
Temps de lecture : 15 min