Récupérez vos entités avec les repositories
Après avoir mis les contraintes de validation au chapitre précédent et envoyé votre code sur les serveurs de test, vous recevez un e-mail d’Amélie.
Merci pour les corrections, je n’ai pas l’impression qu’il reste de problème de format. Par contre je n’arrive pas à être complètement sûre. Pourriez-vous me faire les pages d’affichage des livres, auteurs et éditeurs ? Merci d’avance.
Nous avons des données en base, et les moyens d’en ajouter encore. Cependant, Amélie a raison, il serait bien de pouvoir afficher ce que nous avons.
Au début de cette seconde partie, nous avons vu l’EntityManager, qui est le principal point d’entrée vers notre base de données pour les écrits. Nous allons maintenant voir le point d’entrée en lecture. Ou plutôt, les points d’entrée, car nous allons en avoir un par entité. Il s’agit des Repositories.
Vous aviez sans doute remarqué ces fichiers qui ont été créés en même temps que nos entités et qui se trouvent dans le dossier src/Repository
. Il y en a un par entité : BookRepository
pour l’entité Book
, AuthorRepository
pour l’entité Author
, etc.
À chaque fois que vous voudrez récupérer un certain type d’entité, vous ferez donc appel au Repository correspondant. De la même manière que vous avez injecté l’EntityManager dans vos controllers, vous pouvez injecter un ou plusieurs Repositories.
Mais si je veux récupérer un objet Book, il est lié à un ou plusieurs objets Author, et à un objet Editor ! Il me faut les trois Repositories à chaque fois ?
Non, quand même pas. Lorsque vous récupérez une entité, Doctrine fait automatiquement les jointures avec les entités qui lui sont liées, et vous recevez tous vos objets. Vous pourrez donc récupérer un objet Book depuis la base de données, puis récupérer l’objet Editor correspondant par un simple $book->getEditor()
.
Découvrez les méthodes des repositories
Avant cela, il nous faut récupérer nos entités depuis la base de données. Les Repositories étendent tous la même classe de base, qui leur fournit un certain nombre de méthodes en commun. La plupart sont d’ailleurs décrites dans les commentaires au début de chaque classe de Repository. Prenez par exemple src/Repository/BookRepository.php
:
<?php
/**
* @extends ServiceEntityRepository<Book>
*
* @method Book|null find($id, $lockMode = null, $lockVersion = null)
* @method Book|null findOneBy(array $criteria, array $orderBy = null)
* @method Book[] findAll()
* @method Book[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class BookRepository extends ServiceEntityRepository
{
// …
Nous avons donc :
find
: donnez un id, recevez l’entité correspondante.findOneBy
: cette méthode prend en argument un tableau de critères pour effectuer une requêteWHERE
. Par exemplefindOneBy([‘title’ => ‘1984’])
.findAll
: cette méthode renvoie l’intégralité des entités d’un certain type. Il est fortement déconseillé de s’en servir, sauf pour des tables de référence contenant un nombre très limité de lignes.findBy
: cette méthode extrêmement polyvalente prend en paramètre le même tableau de critères quefindOneBy
, un second tableau optionnel construit de la même manière pour ajouter une clauseORDER BY
, un paramètre optionnelLIMIT
, et un paramètre optionnelOFFSET
.
À ces méthodes s’ajoute une méthode de comptage : count(array $criteria): int
.
Ouvrez le fichier src/Controller/Admin/AuthorController.php
. Nous allons commencer par afficher tous les résultats grâce à findAll
.
Mais on n’avait pas dit qu’on ne devait jamais faire ça ?
Si, tout à fait, mais nous changerons ça rapidement. En mettant notre controller à jour, cela donne donc quelque chose comme ça :
<?php
class AuthorController extends AbstractController
{
#[Route('', name: 'app_admin_author_index', methods: ['GET'])]
public function index(AuthorRepository $repository): Response
{
$authors = $repository->findAll();
return $this->render('admin/author/index.html.twig', [
'controller_name' => 'AuthorController',
'authors' => $authors,
]);
}
// …
Il ne nous reste plus qu’à afficher cette liste dans notre template.
Affichez une liste d’objets
Vous pouvez passer une variable itérable (comme un array) à vos templates Twig sans avoir besoin de gérer l'affichage de chaque ligne à la main.
Pour cela, utilisez une boucle
{% for [valeur] in [array] %}...{% endfor %}
dans votre template Twig (ou sous la forme{% for [clé], [valeur] in [array] %} … {% endfor %}
).Les boucles
for
de Twig incorporent une option{% else %}
pour gérer l'affichage dans le cas où l'array est vide.
Comprenez le fonctionnement de la boucle for
dans Twig
Pour accéder aux propriétés d’un objet ou d’un tableau en Twig, utilisez un point : object.property
. Twig se chargera tout seul de trouver le bon moyen d'accéder à la propriété. Par exemple, si vous écrivez book.title
dans Twig, ce dernier essaiera $book['title']
, puis $book->title
, puis $book->title()
, avant d'obtenir un résultat grâce à $book->getTitle()
. Cette approche fonctionne aussi si vous avez une méthode $entity->isXxx()
ou $entity->hasXxx()
.
Mettons à jour notre template templates/admin/author/index.html.twig
avec une boucle et un peu de Bootstrap ; nous n’allons cependant pas afficher toutes les informations :
block body
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
class="example-wrapper"
Liste d'auteurs
class="list-group list-group-flush"
for author in authors
class="card mb-1"
class="card-body"
class="card-title d-flex justify-content-between"
class="mb-1"{{ author.name }}
class="text-muted"Identifiant : {{ author.id }}
endfor
endblock
Retournez dans votre navigateur et observez le résultat.
Mais pourquoi est-ce qu’on n'affiche pas tout le détail de chaque auteur dans chaque carte ?
C’est une possibilité, mais ça risque de faire beaucoup d’informations dans un petit espace. Ce qui serait encore mieux, ce serait de pouvoir cliquer sur chaque élément de la liste pour accéder à une page qui en montrerait les détails.
Utilisez le routing pour récupérer une entité spécifique
Commençons par créer la page en question. Il nous faut une nouvelle méthode dans Admin\AuthorController
, et une route pour aller avec. Appelons cette méthode et le template qui ira avec show
.
Dans src/Controller/Admin/AuthorController.php
:
<?php
// …
#[Route('', name: 'app_admin_author_show', methods: ['GET'])]
public function show(): Response
{
return $this->render('admin/author/show.html.twig');
}
Et le template templates/admin/author/show.html.twig
:
extends 'base.html.twig'
block title Détail d'un auteur endblock
block body
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
class="example-wrapper"
Auteur :
endblock
Penchons-nous sur la route. Il nous faut un moyen pour indiquer quel auteur nous souhaitons afficher. Traditionnellement, dans les applications web, on passe l’identifiant de la ressource à afficher dans l’URL (par exemple /admin/author/5 pour l’auteur d’id 5). Il nous faut donc indiquer à Symfony qu’une partie du chemin associé à cette route est variable. Nous pouvons le faire grâce aux paramètres de routing. Écrivez simplement le nom de votre paramètre entre accolades dans le chemin, et réutilisez ce nom en tant qu’argument de votre méthode PHP :
<?php
#[Route('/{id}', name: 'app_admin_author_show', methods: ['GET'])]
public function show(int $id): Response
En l’état cependant, rien n’empêche un utilisateur de passer des lettres dans cette URL au lieu de chiffres représentant un identifiant. Bien sûr nous aurons une erreur grâce au typage PHP ( int
devant l’argument), mais ce n’est pas suffisant. Indiquons à Symfony que ce paramètre doit être constitué d’un ou plusieurs chiffres grâce à la clé requirements
. Cette clé est un array. Dans cet array chaque clé est le nom d’un paramètre, et la valeur correspondante est l’expression régulière qui sert à le valider :
<?php
#[Route('/{id}', name: 'app_admin_author_show', requirements: ['id' => '\d+'], methods: ['GET'])]
public function show(int $id): Response
Il nous reste à demander en paramètre notre AuthorRepository
et à nous servir de sa méthode find($id)
pour récupérer l’auteur qui correspond à cet identifiant…
Ou alors, nous pouvons profiter de la puissance de Symfony et d’un objet appelé EntityValueResolver
, qui va directement aller chercher notre entité en fonction de l’identifiant passé dans l’URL. Pour ce faire, remplacez votre argument id
dans la méthode du controller par un argument typé de la classe de votre entité.
Par exemple :
<?php
#[Route('/{id}', name: 'app_admin_author_show', requirements: ['id' => '\d+'], methods: ['GET'])]
public function show(?Author $author): Response
Notez le point d’interrogation devant Author
qui signifie que cet argument peut aussi avoir comme valeur null
. Si vous ne le mettez pas et que l’utilisateur demande un identifiant qui n’existe pas, il aura une erreur. En rendant l’argument nullable, vous demandez à Symfony de vous laisser gérer le cas où l’entité n’a pas été trouvée.
Affichez votre entité grâce à Twig
Nous pouvons maintenant passer notre variable $author
à Twig pour aller l’afficher.
<?php
#[Route('/{id}', name: 'app_admin_author_show', requirements: ['id' => '\d+'], methods: ['GET'])]
public function show(?Author $author): Response
{
return $this->render('admin/author/show.html.twig', [
'author' => $author,
]);
}
Profitons-en pour encapsuler notre affichage dans une balise {% if %}{% endif %}
(à laquelle vous pourrez ajouter des elseif
et else
, bien entendu). Ça nous permettra de gérer le cas où l’utilisateur a demandé une entité qui n’existe pas en base de données :
class="example-wrapper"
Auteur :
if author is not null
class="card mb-1 m-auto"
class="card-body"
class="card-title d-flex justify-content-between"
class="mb-1"{{ author.name }}
class="text-muted"Identifiant : {{ author.id }}
class="d-flex justify-content-between card-text"
class="list-group list-group-flush"
class="list-group-item"Date de naissance : {{ author.dateOfBirth }}
class="list-group-item"Date de décès : {{ author.dateOfDeath }}
Nationalité : {{ author.nationality }}
else
Auteur non trouvé
endif
Allez voir sur https://127.0.0.1:8000/admin/author/0
:
Jusqu’ici tout va bien. Maintenant retournez voir sur https://127.0.0.1:8000/admin/author
et récupérez un identifiant qui existe dans la liste.
Retournez sur https://127.0.0.1:8000/admin/author/<identifiant>
en remplaçant <identifiant>
par celui que vous avez repéré :
Que nous dit l’erreur ?
Que les objets de la classe DateTimeImmutable
ne peuvent pas être convertis en chaîne de caractères. Et la ligne en surbrillance nous montre l’origine de l’erreur. Effectivement, nous essayons d’afficher la date de naissance, mais c’est un objet. Et d’après la documentation PHP, cet objet ne dispose pas de la méthode magique __toString
qui nous aurait permis de l'afficher simplement.
Utilisez les filtres et fonctions de Twig pour afficher des données complexes
Pas de problème, Twig a ce qu’on appelle un filtre pour ça. Il suffit de rajouter une barre verticale derrière le nom de la variable à filtrer, suivie du nom du filtre. En l’occurrence, ce sera le filtre date
, auquel nous allons passer le format de date à utiliser :
class="list-group-item"Date de naissance : {{ author.dateOfBirth|date('d M Y') }}
Pour la date de décès c’est un peu plus compliqué, vu qu’elle peut être nulle. Nous allons donc devoir utiliser une condition, sous la forme d’un ternaire, comme PHP.
class="list-group-item"Date de décès : {{ author.dateOfDeath is not null ? author.dateOfDeath|date('d M Y') : '-' }}
Ici il faut lire “Si author.dateOfDeath
n’est pas nulle, affiche-la après l’avoir passée dans le filtre date ; sinon, affiche un tiret”. Et effectivement, si vous rafraîchissez la page, tout fonctionne !
On peut même faire mieux. Il existe une extension de Twig qui prend en charge l’internationalisation, c’est-à-dire le support des différentes langues et traductions. Ouvrez votre terminal et entrez la commande symfony composer require twig/intl-extra
. Ensuite, retournez dans notre template, et rajoutez le filtre country_name
derrière author.nationality
:
Nationalité : {{ author.nationality|country_name }}
Rafraîchissez de nouveau votre page et admirez le résultat : les codes pays ont été remplacés par les noms des pays dans notre locale.
Utilisez la fonction path
pour ajouter de la navigation
Nous pouvons maintenant faire le lien entre notre liste et cette page. Retournez sur le template templates/admin/author/index.html.twig
. Nous allons envelopper nos titres de cartes dans des balises de lien <a>
avec quelques classes Bootstrap. Mais que mettre dans l’attribut href
? Eh bien Twig dispose d’une fonction spéciale, path
. Celle-ci prend en premier paramètre le nom d’une route de votre application, et en second paramètre optionnel les paramètres à passer à cette route. Donc, en passant path(‘app_admin_author_show’, {id: author.id})
à href
, nous créons un lien vers la bonne page à chaque fois :
class="card-title d-flex justify-content-between"
href="{{ path('app_admin_author_show', {id: author.id}) }}" class="stretched-link link-dark"
class="mb-1"{{ author.name }}
class="text-muted"Identifiant : {{ author.id }}
Et voilà, vous pouvez aller de votre liste à vos pages de détail. Nous pourrons même rajouter un bouton sur la page tout en bas de templates/admin/author/show.html.twig
pour revenir à la liste :
href="{{ path('app_admin_author_index') }}" class="btn btn-primary"Retour
Parfois cependant, les méthodes de base de nos repositories ne sont pas suffisantes. Comment faire par exemple, si nous souhaitons rechercher tous les livres publiés entre deux dates ? Le paramètre de critères de findBy
et findOneBy
ne gère que les égalités. Ou encore, comment faire pour paginer nos résultats sur notre liste ?
Créez des requêtes spécifiques
Pour ce genre de cas, nous avons besoin d’avoir plus de contrôle sur les requêtes effectuées en base de données. Nous allons donc devoir rajouter des méthodes de requête dans notre Repository.
Ouvrez donc src/Repository/AuthorRepository.php
. Commençons par y ajouter une nouvelle méthode pour filtrer les auteurs par date de naissance. Nous allons donc l’appeler findByDateOfBirth
. Pour bien faire, cette méthode prend en paramètre un array qui contiendra deux clés optionnelles : start
et end
, pour la date de début et la date de fin de notre recherche, et retourne un array de résultats.
<?php
class AuthorRepository extends ServiceEntityRepository
{
// …
public function findByDateOfBirth(array $dates = []): array
{
// La recherche se fera ici
}
Comprenez le QueryBuilder
de Doctrine
Il y a maintenant plusieurs méthodes que vous pouvez utiliser pour construire vos requêtes personnalisées. La plus simple est d’utiliser un objet appelé QueryBuilder
. Il est là pour nous aider à construire nos requêtes. En fait, nous allons appeler des méthodes très expressives sur notre objet QueryBuilder
, comme where()
ou même join()
. Ensuite, lorsque nous demanderons à récupérer notre requête avec getQuery()
, il construira la requête tout seul à partir de ce que nous avons demandé.
Commençons par le créer. Une méthode existe pour ça dans tous nos Repositories. Nous allons l’appeler et lui passer l’alias de notre entité (qui sera habituellement la première lettre du nom de notre entité) :
<?php
// …
public function findByDateOfBirth(array $dates = []): array
{
$qb = $this->createQueryBuilder('a');
// …
Par convention, il est très souvent appelé $qb
. Si nous souhaitons ajouter une clause WHERE
à notre requête, nous pouvons appeler la méthode andWhere
sur ce QueryBuilder en lui passant nos critères de recherche.
Par exemple, si nous souhaitons ajouter que la date de naissance de nos auteurs doit être supérieure ou égale à une date passée en paramètre :
<?php
public function findByDateOfBirth(array $dates = []): array
{
$qb = $this->createQueryBuilder('a')
->andWhere('a.dateOfBirth >= :start')
->setParameter('start', new \DateTimeImmutable($dates['start']));
// …
Nous pouvons maintenant lui demander de générer la requête correspondante, et de nous retourner les résultats avec $qb->getQuery()->getResult()
:
<?php
public function findByDateOfBirth(array $dates = []): array
{
$qb = $this->createQueryBuilder('a')
->andWhere('a.dateOfBirth >= :start')
->setParameter('start', new \DateTimeImmutable($dates['start']));
return $qb->getQuery()->getResult();
}
Si nous ajoutons des conditions pour vérifier si nous avons une date de départ, de fin, ou les deux, ainsi que le tri de nos auteurs par date de naissance, on obtient ce résultat :
<?php
public function findByDateOfBirth(array $dates = []): array
{
$qb = $this->createQueryBuilder('a');
if (\array_key_exists('start', $dates)) {
$qb->andWhere('a.dateOfBirth >= :start')
->setParameter('start', new \DateTimeImmutable($dates['start']));
}
if (\array_key_exists('end', $dates)) {
$qb->andWhere('a.dateOfBirth <= :end')
->setParameter('end', new \DateTimeImmutable($dates['end']));
}
return $qb->orderBy('a.dateOfBirth', 'DESC')
->getQuery()
->getResult();
}
Utilisez vos méthodes de requêtes personnalisées
Retournez maintenant dans src/Controller/Admin/AuthorController.php
et utilisez votre nouvelle méthode. Pour obtenir des dates de départ et de fin, passez-les simplement dans l’URL en paramètre, par exemple https://127.0.0.1:8000/admin/author?start=01-01-1975
, et récupérez l’information dans l’objet Request
.
<?php
#[Route('', name: 'app_admin_author_index', methods: ['GET'])]
public function index(Request $request, AuthorRepository $repository): Response
{
$dates = [];
if ($request->query->has('start')) {
$dates['start'] = $request->query->get('start');
}
if ($request->query->has('end')) {
$dates['end'] = $request->query->get('end');
}
$authors = $repository->findByDateOfBirth($dates);
// Le reste ne change pas
Faites un essai dans votre navigateur. Ici j’ai filtré les auteurs pour ne garder que ceux nés en 1975 :
Ajoutez un système de pagination dans Symfony avec Pagerfanta
Vous pouvez ajouter de la pagination facilement à votre application Symfony grâce à Pagerfanta.
Vous devrez installer plusieurs dépendances en fonction de votre projet. Pour Symfony :
babdev/pagerfanta-bundle
,pagerfanta/doctrine-orm-adapter
etpagerfanta/twig
.On crée un objet
Pagerfanta
en lui passant notre QueryBuilder Doctrine, le numéro de la page sur laquelle nous nous trouvons au sein de la pagination, et le nombre d'objets par page.L'objet créé par Pagerfanta peut être parcouru comme le tableau d'objets récupérés en base de données que vous aviez à l'origine, pas besoin de modifier vos templates.
pagerfanta/twig
vous permet d'ajouter une fonction Twig{{ pagerfanta([array]) }}
qui gère toute seule l'affichage des liens de navigation de votre pagination.Pagerfanta embarque aussi un template de liens de navigation adaptés à Bootstrap.
Créez un formulaire d’édition pour vos entités
Vous pouvez même combiner ces routes dynamiques avec nos formulaires ! Rajoutez une deuxième route pour l’édition sur votre méthode new
dans src/Controller/Admin/AuthorController.php
, et ajoutez un argument nullable ?Author $author
dans la méthode :
<?php
#[Route('/new', name: 'app_admin_author_new', methods: ['GET', 'POST'])]
#[Route('/{id}/edit', name: 'app_admin_author_edit', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
public function new(?Author $author, Request $request, EntityManagerInterface $manager): Response
On peut tout à fait avoir deux routes pour le même controller ! Dans un cas, lorsqu’on appelle /admin/author/new
, la variable $author
vaudra null
, et si on appelle /admin/author/<identifiant>/edit
, Symfony ira chercher en base de données l’auteur correspondant et le mettra dans $author
. Du coup, nous devons gérer le cas où $author = null
. Modifiez aussi la première ligne de la méthode en ajoutant un opérateur de fusion null ( ??=
).
<?php
public function new(?Author $author, Request $request, EntityManagerInterface $manager): Response
{
$author ??= new Author();
// …
Cet opérateur signifie que si $author
vaut null
, on lui assignera comme valeur une nouvelle instance de Author
. Sinon, on garde celle qui a été passée en argument.
Cela veut donc dire que désormais, lorsqu’on appelle /admin/author/new
on tombe sur un formulaire vide, mais si on appelle /admin/author/<un_identifiant>/edit
, on tombe sur un formulaire d’édition prérempli ! Nous allons en profiter pour rendre le titre de la page conditionnel et parler d’une variable magique.
Dans Twig, dans Symfony, il existe une variable nommée app
qui est toujours disponible dans tous vos templates. Cette variable contient plein de choses intéressantes, comme la requête en cours. On peut aussi y trouver le nom de la route qui a été appelée grâce à app.current_route
.
Profitons-en pour modifier le titre de notre page : Si la route appelée est app_admin_author_new
, nous afficherons “Ajout”, sinon ce sera “Édition”. Nous pouvons écrire cette condition sous forme d’un ternaire, et mieux encore, stocker le résultat dans une variable une fois pour toute la page grâce au mot-clé set
. Ouvrez templates/admin/author/new.html.twig
:
extends 'base.html.twig'
set action = app.current_route == 'app_admin_author_new' ? 'Ajout' : 'Édition'
block title {{ action }} d'auteur endblock
block body
{# … #}
class="example-wrapper"
{{ action }} d'auteur
{# … #}
Vous pouvez même modifier la redirection de votre controller Admin\AuthorController::new
pour renvoyer sur notre nouvelle page show
, et y ajouter un bouton d’édition :
<?php
// src/Controller/Admin/AuthorController.php
if ($form->isSubmitted() && $form->isValid()) {
$manager->persist($author);
$manager->flush();
return $this->redirectToRoute('app_admin_author_show', ['id' => $author->getId()]);
}
{# dans templates/admin/author/show.html.twig #}
{# … #}
href="{{ path('app_admin_author_index') }}" class="btn btn-primary"Retour
href="{{ path('app_admin_author_edit', {id: author.id}) }}" class="btn btn-warning"Éditer
Il va maintenant être temps de répliquer tout cela pour toutes nos entités. Vous pouvez copier-coller le code relatif à Author pour créer les pages Editor et Book.
À vous de jouer
Contexte
C'est maintenant le moment d'implémenter le cœur de l'application, le besoin principal qui a été exprimé par Amélie : les pages de listes et de détails ! Nous avons tout ce dont nous avons besoin, nous savons comment enregistrer et récupérer des données, il ne nous reste plus qu'à mettre le tout en œuvre.
Consignes
Vous allez implémenter le HTML du catalogue public ainsi que la page de détail d'un livre. Pour cela, vous aurez besoin :
d'ajouter une nouvelle classe de controller (appelez-la
BookController
). Cette classe contiendra :une méthode controller appelée
index
sur laquelle on retrouvera le listing paginé des livres enregistrés en base de données,une méthode controller appelée
show
qui affichera les détails d'un livre ;
des templates pour ces deux pages, qui seront construits comme suit :
la page de listing devra se baser sur ce thème Bootstrap gratuit. N'utilisez que la
section
centrale, celle qui contient la liste. Utilisez les conditions et boucles de Twig pour afficher vos listes de livres,la page de détail d'un livre devra se baser sur ce thème Bootstrap gratuit. Ici encore, vous n'utiliserez que la
section
qui contient les détails du produit.Vous afficherez les informations suivantes : ISBN, titre du livre, auteur(s), éditeur, nombre de pages et date d'édition, statut. N’hésitez pas à vous inspirer des captures d’écran.
Les boutons de commande en bas du template Bootstrap seront remplacés par un bouton “Retour à la liste” qui ramène au catalogue.
Vérifiez votre travail à l'aide de cet exemple de corrigé
Voici deux captures d'écran du résultat attendu :
En résumé
Les Repositories sont notre point d’entrée pour les lectures en base de données.
Il en existe généralement un par entité de notre application.
Ils disposent de 5 méthodes de base pour lire notre base de données :
find
,findOneBy
,findBy
,findAll
etcount
.Vous pouvez rajouter des méthodes personnalisées pour vos besoins spécifiques.
Ces méthodes spécifiques utiliseront le plus souvent le QueryBuilder pour construire vos requêtes en base de données.
Et maintenant, voyons comment nous pouvons sécuriser notre application avec une bonne gestion des utilisateurs ! Mais avant cela, testez vos connaissances dans le quiz finalisant cette partie !