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

Injection de dépendance et Repository Pattern

Injection de dépendance et Repository Pattern

<p><strong>Injection de d&eacute;pendance et Repository Pattern</strong></p> <p>Les design pattern (litt&eacute;ralement patron de conception), sont des mod&egrave;les afin de r&eacute;pondre &agrave; certaine probl&eacute;matique.</p> <p>Le repository pattern est un mod&egrave;le de conception qui fournit une couche d'abstraction entre l'application et la couche de persistance des donn&eacute;es. Il s&eacute;pare la logique applicative de la couche d'acc&egrave;s aux donn&eacute;es, ce qui permet aux d&eacute;veloppeurs de remplacer la couche d'acc&egrave;s aux donn&eacute;es sans affecter les fonctionnalit&eacute;s de l'application.</p> <p>Dans Laravel, le repository pattern est couramment utilis&eacute; pour g&eacute;rer les acc&egrave;s aux mod&egrave;les (bases de donn&eacute;es). Au lieu d'acc&eacute;der directement aux mod&egrave;les, les d&eacute;veloppeurs cr&eacute;ent un repository pattern qui sert d'interface entre l'application et la base de donn&eacute;es. Le repository pattern g&egrave;re toutes les op&eacute;rations de la base de donn&eacute;es et renvoie les donn&eacute;es &agrave; l'application.</p> <p>Dans le cas de Laravel, nous aurons un contr&ocirc;leur qui fera appel au repository qui lui-m&ecirc;me fera appel au model.</p> <p>Cette conception permet :</p> <p>- de s&eacute;parer la couche de donn&eacute;es</p> <p>- de tester plus facilement l'acc&egrave;s aux donn&eacute;es</p> <p>- d'ajouter facilement de nouveaux composants&nbsp;</p> <p>Imaginons, que nous devons afficher une liste d'article, un article sp&eacute;cifique, et cr&eacute;er un article. Pour qu'un article soit visible, il faut qu'il soit publi&eacute;, voici les requ&ecirc;tes que nous pourrions avoir dans nos contr&ocirc;leurs</p> <pre class="language-php"><code>$posts = Post::where('is_published', true)-&gt;count(); $posts = Post::where('is_published', true)-&gt;get(); $posts = Post::where('is_published', true)-&gt;paginate(); $post = Post::where('user_id', $user_id)-&gt;where('is_published', true)-&gt;first(); $prevPost = Post::where('id', '&lt;' $post-&gt;id)-&gt;where('user_id', $user_id)-&gt;where('is_published', true)-&gt;latest('id')-&gt;first(); $nextPost = Post::where('id', '&gt;' $post-&gt;id)-&gt;where('user_id', $user_id)-&gt;where('is_published', true)-&gt;oldest('id')-&gt;first(); //....</code></pre> <p>A pr&eacute;sent, le d&eacute;veloppeur change d'avis, et change is_published par is_validated, il va falloir intervenir dans toutes les m&eacute;thodes de tous les contr&ocirc;leurs, alors que si la couche de donn&eacute;es est externe au contr&ocirc;leurs, il suffira de modifier notre repository.</p> <p>Prenons un exemple avec nos posts, pour faire tr&egrave;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>&nbsp;</p> <p><strong>Les donn&eacute;es</strong></p> <p>Je cr&eacute;&eacute; le model Post et Laravel va cr&eacute;er la migration -m, le controler -c, le seeder -s, et la factory -f<br>Commen&ccedil;ons par la migration</p> <pre class="language-php"><code>Schema::create('posts', function (Blueprint $table) { $table-&gt;id(); $table-&gt;string('title', 255); $table-&gt;text('body'); $table-&gt;boolean('is_published')-&gt;default(false); $table-&gt;foreignUlid('user_id')-&gt;nullable()-&gt;constrained()-&gt;onDelete('cascade'); $table-&gt;timestamps(); });</code></pre> <p>Puis le model Post, on ajoute la liste des champs qui sont modifiables et la m&eacute;thode permettant d'acc&eacute;der a l'auteur</p> <pre class="language-php"><code>protected $fillable = ['title', 'body', 'is_published', 'user_id']; public function user() { return $this-&gt;bellongsTo(User::class); }</code></pre> <p>Enfin le model User, on ajoute la m&eacute;thode permettant d'acc&eacute;der a ses articles</p> <pre class="language-php"><code>public function posts() { return $this-&gt;hasMany(Post::class); }</code></pre> <p>Maintenant, il nous faut des donn&eacute;es pour tester, on va passer &agrave; la factory et aux seeders, PostFactory</p> <p>On r&eacute;cup&egrave;re en premier al&eacute;atoirement un auteur, puis gr&acirc;ce &agrave; Faker, on rempli avec des donn&eacute;es</p> <pre class="language-php"><code>public function definition(): array { $user = User::inRandomOrder()-&gt;first(); return [ 'title' =&gt; fake()-&gt;sentence($nbWords = 6, $variableNbWords = true), 'body' =&gt; fake()-&gt;paragraph($nbSentences = 3, $variableNbSentences = true), 'is_published' =&gt; rand(0,1), 'user_id' =&gt; $user-&gt;id ]; }</code></pre> <p>Ensuite on rempli, UserSeeder</p> <pre class="language-php"><code>public function run(): void { User::factory(5)-&gt;create(); }</code></pre> <p>PostSeeder</p> <pre class="language-php"><code>public function run(): void { Post::factory(10)-&gt;create(); }</code></pre> <p>Ne reste plus qu'a ins&eacute;rer les donn&eacute;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&eacute;sent, nous avons dans la base 5 auteurs et 10 articles, nous pouvons v&eacute;rifier dans phpMyAdmin ou via tinker</p> <pre class="language-php"><code>php artisan tinker &gt; App\Models\User::count() = 5 &gt; App\Models\Post::count() = 10</code></pre> <p>&nbsp;</p> <p><strong>Le repository</strong></p> <p>Laravel ne poss&egrave;de pas de commande sp&eacute;cifique pour cr&eacute;er un repository et une interface, mais c'est du php standard, commen&ccedil;ons par cr&eacute;er les dossiers.</p> <p>Dans le dossier \App, cr&eacute;ons le dossier Repositories : App\Repositories</p> <p>Dans le dossier \App\Repositories, cr&eacute;ons le dossier Interfaces : App\Repositories\Interfaces</p> <p>Commen&ccedil;ons par cr&eacute;er un fichier pour l'interface&nbsp; App\Repositories\Interfaces\PostRepositoryInterface.php dans lequel on d&eacute;fini les prototypes des m&eacute;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&eacute;cup&eacute;rer un post par son id</p> <p>- de r&eacute;cup&eacute;rer tous les posts</p> <p>- de r&eacute;cup&eacute;rer tous les posts d'un auteur</p> <p>- de cr&eacute;er un post</p> <p>Comme vous avez pu le constater pour la m&eacute;thode store, on injecte un Form Request, ce sont des classes de validation de donn&eacute;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' =&gt; 'required|string|max:255', 'body' =&gt; 'sometimes', 'is_published' =&gt; 'required', ]; } }</code></pre> <p>PostRequest va nous permettre de v&eacute;rifier que les champs n&eacute;cessaires respectent certaines r&egrave;gles, comme par exemple le titre ne peux d&eacute;passer 255 caract&egrave;res, si ces r&egrave;gles sont respect&eacute;es, laravel renverra ces donn&eacute;es aux contr&ocirc;leurs, et nous pourrons les r&eacute;cup&eacute;rer avec&nbsp;<strong>$request-&gt;validated()</strong></p> <p>Maintenant que l'interface est faite, on va pouvoir impl&eacute;menter les diff&eacute;rentes m&eacute;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-&gt;id)-&gt;all(); } public function store(PostRequest $request, User $user): Post|null { return $user-&gt;posts()-&gt;create($request-&gt;validated()); } }</code></pre> <p>Nous cr&eacute;ons la classe PostRepository qui faire r&eacute;f&eacute;rence a l'interface PostRepositoryInterface (implements), on doit donc respecter le contrat d'impl&eacute;menter toutes les d&eacute;finitions de m&eacute;thode de l'interface</p> <p>getById, getAll, getAllByUser, ce sont de simples m&eacute;thodes</p> <p>store : on r&eacute;cup&egrave;re les donn&eacute;es et un utilisateur, on aurait pu &eacute;crire</p> <pre class="language-php"><code>return Post::create([ 'title' =&gt; $request-&gt;title, 'body' =&gt; $request-&gt;body, 'is_published' =&gt; $request-&gt;is_published, 'user' =&gt; $user-&gt;id, ]);</code></pre> <p>Mais la relation Posts de $user, nous permet d'injecter directement user_id a la cr&eacute;ation</p> <pre class="language-php"><code>return $user-&gt;posts()-&gt;create($request-&gt;validated());</code></pre> <p>&nbsp;</p> <p><strong>Le contr&ocirc;leur</strong></p> <p>Passons &agrave; pr&eacute;sent &agrave; notre contr&ocirc;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>&Agrave; la base, le conteneur de services de Laravel est un conteneur d'injection de d&eacute;pendances (DI) qui g&egrave;re l'instanciation des objets et de leurs d&eacute;pendances. En termes plus simples, le conteneur de services est un conteneur qui contient et g&egrave;re tous les objets et services dont votre application a besoin pour fonctionner. Le conteneur de services est responsable de la cr&eacute;ation de ces objets et de leurs d&eacute;pendances, puis de leur injection dans le code de votre application lorsqu'ils sont n&eacute;cessaires.&nbsp;<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&eacute;thode register, on utilise la m&eacute;thode bind, qui va faire correspondre des fichiers</p> <pre class="language-php"><code>public function register(): void { $this-&gt;app-&gt;bind(PostRepositoryInterface::class, PostRepository::class); }</code></pre> <p>Dans le contr&ocirc;leur, il faut injecter cette d&eacute;pendance, avant php 8, on aurait &eacute;crit</p> <pre class="language-php"><code>protected $postRepository, public function __construct(PostRepositoryInterface $postRepository) { $this-&gt;postRepository = $postRepository; }</code></pre> <p>Avec php 8, on peut &eacute;crire&nbsp;</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-&gt;postRepository-&gt;getAll(); return view('posts.index', compact('posts')); } public function show($id) { $post = $this-&gt;postRepository-&gt;getById(); return view('posts.show', compact('post')); } public function indexAuthor($user) { $post = $this-&gt;postRepository-&gt;getAllByUser($user); return view('posts.author', compact('user','posts')); } public function store(PostRequest $request, User $user) { $post = $this-&gt;postRepository-&gt;store($request, $user); return redirect()-&gt;route('posts.index'); } }</code></pre> <p>Grace a l'injection de d&eacute;pendance, la class PostRepository et ses m&eacute;thodes, sont accessibles de n'importe ou dans notre contr&ocirc;leur, on va donc pouvoir utiliser toutes les m&eacute;thode de notre repository. Le contr&ocirc;leur devient plus l&eacute;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&ocirc;leurs (du front et/ou back).&nbsp;</p>
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