Doctrine et les entités
BaseEntity, entités du projet, fixtures, services de récupération de données et pagination avec Doctrine.
Durant cet atelier, nous allons créer notre modèle de données avec Doctrine, remplir la base de données avec des données, et faire nos premières requêtes pour afficher des données dans nos pages.
À savoir
La plupart de l'atelier sera fait en live-coding. Pensez à bien prendre des notes pour vous y référer plus tard !
Reset de la base de données
Avant de commencer à créer nos entités, nous allons réinitialiser notre base de données pour être sûrs d'avoir un environnement propre.
symfony console doctrine:database:drop --force
symfony console doctrine:database:createInstallation des dépendances
Plus tard dans cet atelier, nous allons avoir besoin de générer des UUID.
Pour les générer simplement via Symfony, nous allons installer la bibliothèque symfony/uid dont la documentation est disponible ici : https://symfony.com/doc/current/components/uid.html
Consigne
- Installez la bibliothèque
symfony/uidvia Composer avec la commande suivante :
composer require symfony/uidModification de l'utilisateur
Dans l'atelier précédent, nous avons mis en place le système d'authentification avec un utilisateur de base. Nous allons maintenant modifier l'entité User pour y ajouter un champ name (nom ou pseudo).
Pour ce faire, nous allons utiliser la commande suivante :
symfony console make:entity UserConsigne
- Ajoutez un champ
namede typestringà l'entitéUser. - Générez une migration pour mettre à jour la base de données.
- Exécutez la migration.
Astuce
Pour le moment, votre formulaire d'inscription ne prendra pas encore en compte ce nouveau champ name et risque d'être cassé. Nous y reviendrons plus tard.
Rappel - Modélisation des données
Voici la modélisation des données que nous allons utiliser pour ce projet :
Création d'une classe d'aide pour les entités
Pour faciliter la gestion des dates de création et de mise à jour de nos entités, nous allons créer une classe d'aide (classe Abstraite) qui contiendra ces champs et les méthodes associées. Nous pourrons ensuite faire hériter nos entités de cette classe.
Astuce
Vous verrez les mécanismes d'héritage et de réutilisation du code plus tard dans l'année.
Considérez simplement cette classe comme un "copier-coller" des champs et méthodes dans chaque entité.
Consigne
- Dans le dossier
src/Entity, créez un dossierImpl. - Dans ce dossier, créez un fichier
BaseEntity.php. - Collez le contenu suivant :
<?php
namespace App\Entity\Impl;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use App\Entity\User;
abstract class BaseEntity
{
#[ORM\Column(name: 'created_date', type: Types::DATETIME_MUTABLE, nullable: false, options: ['default' => "CURRENT_TIMESTAMP"])]
protected \DateTime $createdDate;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
protected User $createdBy;
#[ORM\Column(name: 'updated_date', type: Types::DATETIME_MUTABLE, nullable: true)]
protected null|\DateTime $updatedDate;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
protected null|User $updatedBy;
#[ORM\Column(name: 'deleted_date', type: Types::DATETIME_MUTABLE, nullable: true)]
protected null|\DateTime $deletedDate;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
protected null|User $deletedBy;
#[ORM\Column(name: 'is_deleted', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
protected bool $isDeleted = false;
public function getCreatedDate(): \DateTime {
return $this->createdDate;
}
public function setCreatedDate(\DateTime $createdDate): self {
$this->createdDate = $createdDate;
return $this;
}
public function getCreatedBy(): ?User {
return $this->createdBy;
}
public function setCreatedBy(?User $createdBy): self {
$this->createdBy = $createdBy;
return $this;
}
public function getUpdatedDate(): ?\DateTime {
return $this->updatedDate;
}
public function setUpdatedDate(?\DateTime $updatedDate): self {
$this->updatedDate = $updatedDate;
return $this;
}
public function getUpdatedBy(): ?User {
return $this->updatedBy;
}
public function setUpdatedBy(?User $updatedBy): self {
$this->updatedBy = $updatedBy;
return $this;
}
public function getDeletedDate(): ?\DateTime {
return $this->deletedDate;
}
public function setDeletedDate(?\DateTime $deletedDate): self {
$this->deletedDate = $deletedDate;
return $this;
}
public function getDeletedBy(): ?User {
return $this->deletedBy;
}
public function setDeletedBy(?User $deletedBy): self {
$this->deletedBy = $deletedBy;
return $this;
}
public function isDeleted(): bool {
return $this->isDeleted;
}
public function setIsDeleted(bool $isDeleted): self {
$this->isDeleted = $isDeleted;
return $this;
}
}Création des portefeuilles (Wallets)
Nous allons maintenant créer l'entité Wallet (portefeuille) qui représentera un portefeuille de dépenses partagé entre plusieurs utilisateurs.
Consigne
- Utilisez la commande
symfony console make:entity Walletpour créer l'entitéWallet. - Ajoutez les champs suivants :
uid: guid, uniquetotalAmount: integer, défaut 0label: string (255)paymentsDue: json, défaut '[]'lastSettlementDate: datetime, nullable, défaut null
- Faites hériter l'entité
Walletde la classeBaseEntityavec le mot cléextends. - Effectuez les modifications nécessaires dans le fichier
Wallet.phppour que l'entité corresponde aux consignes. - Synchronisez les modifications avec la base de données via la commande
symfony console doctrine:schema:update --dump-sql(puis repassez là avec-fpour appliquer).
Astuce
Quelques indications.
- Pour garantir que le champ
uidsoit unique, vous pouvez ajouter l'attributunique: truedans l'attribut#[ORM\Column(...)]. - Pour le champ
totalAmount, vous pouvez définir une valeur par défaut en utilisant l'optionoptions: ['default' => 0]dans l'attribut#[ORM\Column(...)]. - Pour le champ
lastSettlementDate, vous pouvez utiliser le typeTypes::JSONde Doctrine.
Création des dépenses (Expenses)
Nous allons maintenant créer l'entité Expense (dépense) qui représentera une dépense effectuée dans un portefeuille.
Consigne
- Utilisez la commande
symfony console make:entity Expensepour créer l'entitéExpense. - Ajoutez les champs suivants :
uid: guid, uniquewallet: relation ManyToOne vers l'entitéWalletamount: integerdescription: string (255), nullable, défaut null
- Faites hériter l'entité
Expensede la classeBaseEntityavec le mot cléextends. - Effectuez les modifications nécessaires dans le fichier
Expense.phppour que l'entité corresponde aux consignes. - Synchronisez les modifications avec la base de données via la commande
symfony console doctrine:schema:update --dump-sql(puis repassez là avec-fpour appliquer).
Création des relations utilisateur-portefeuille (XUserWallet)
Nous allons maintenant créer l'entité XUserWallet qui représentera la relation entre un utilisateur et un portefeuille, ainsi que le rôle de l'utilisateur dans ce portefeuille.
Consigne
- Utilisez la commande
symfony console make:entity XUserWalletpour créer l'entitéXUserWallet. - Ajoutez les champs suivants :
wallet: relation ManyToOne vers l'entitéWallettargetUser: relation ManyToOne vers l'entitéUser(ne peut pas être nomméusercar ce mot est réservé en PHP)role: string (50), défaut 'user'
- Faites hériter l'entité
XUserWalletde la classeBaseEntityavec le mot cléextends. - Effectuez les modifications nécessaires dans le fichier
XUserWallet.phppour que l'entité corresponde aux consignes. - Synchronisez les modifications avec la base de données via la commande
symfony console doctrine:schema:update --dump-sql(puis repassez là avec-fpour appliquer).
Remplissage de la base de données avec des données de test
Pour tester nos entités et nos relations, nous allons remplir la base de données avec des données de test en utilisant des fixtures.
Astuce
Les Fixtures sont un moyen simple de remplir une base de données de test avec des données prédéfinies.
Vous trouverez la documentation officielle sur les fixtures Symfony ici : https://symfony.com/bundles/DoctrineFixturesBundle/current/index.html
Nous allons utiliser le fichier de fixtures pré-généré par Symfony pour créer :
- 2 utilisateurs
- 3 portefeuilles
- 5 dépenses par portefeuille
- 2 relations utilisateur-portefeuille par portefeuille
Consigne
- Si ce n'est pas déjà fait, installez le bundle de fixtures Doctrine avec la première commande de la documentation.
- Ouvrez le fichier
src/DataFixtures/AppFixtures.phpet remplacez son contenu par le code suivant :
<?php
namespace App\DataFixtures;
use App\Entity\Expense;
use App\Entity\User;
use App\Entity\Wallet;
use App\Entity\XUserWallet;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Uid\Uuid;
class AppFixtures extends Fixture
{
private const array EXPENSE_TYPES = [
"hotel", "bar", "essence", "restau", "sortie", "souvenir", "sushi", "imprévu"
];
private array $generatedUsers = [];
private array $generatedWallets = [];
private UserPasswordHasherInterface $hasher;
private ObjectManager $manager;
public function __construct(UserPasswordHasherInterface $hasher)
{
$this->hasher = $hasher;
}
// ...
public function load(ObjectManager $manager): void
{
// initialisation des variables
$this->manager = $manager;
$this->generatedUsers = [];
$this->generatedWallets = [];
// generate 2 different users
$this->generatedUsers[] = $this->generateUser("Alice", "alice@coda.fr", "alice");
$this->generatedUsers[] = $this->generateUser("Bob", "bob@coda.fr", "bob");
// generate 3 different wallets
$this->generatedWallets[] = $this->generateWallet("Vacances");
$this->generatedWallets[] = $this->generateWallet("Colloc");
$this->generatedWallets[] = $this->generateWallet("Montagne");
// now, generate 5 expenses per wallets
foreach ($this->generatedWallets as $wallet) {
$this->generateExpense(
$wallet,
random_int(1, 150) * 100,
self::EXPENSE_TYPES[random_int(0, sizeof(self::EXPENSE_TYPES) - 1)],
$this->generatedUsers[0]
);
$this->generateExpense(
$wallet,
random_int(1, 150) * 100,
self::EXPENSE_TYPES[random_int(0, sizeof(self::EXPENSE_TYPES) - 1)],
$this->generatedUsers[0]
);
$this->generateExpense(
$wallet,
random_int(1, 150) * 100,
self::EXPENSE_TYPES[random_int(0, sizeof(self::EXPENSE_TYPES) - 1)],
$this->generatedUsers[1]
);
$this->generateExpense(
$wallet,
random_int(1, 150) * 100,
self::EXPENSE_TYPES[random_int(0, sizeof(self::EXPENSE_TYPES) - 1)],
$this->generatedUsers[1]
);
$this->generateExpense(
$wallet,
random_int(1, 150) * 100,
self::EXPENSE_TYPES[random_int(0, sizeof(self::EXPENSE_TYPES) - 1)],
$this->generatedUsers[1]
);
}
// generating links between users and wallets
foreach ($this->generatedWallets as $wallet) {
foreach ($this->generatedUsers as $user) {
$role = $user->getName() == "Alice" ? "admin" : "user";
$this->generateXUserWallet($user, $wallet, $role);
}
}
}
public function generateXUserWallet(User $user, Wallet $wallet, string $role): XUserWallet
{
$xUserWallet = new XUserWallet();
// ...
$xUserWallet->setCreatedBy($this->generatedUsers[0]);
$xUserWallet->setCreatedDate(new \DateTime());
$this->manager->persist($xUserWallet);
$this->manager->flush();
return $xUserWallet;
}
public function generateExpense(Wallet $wallet, int $amount, string $description, User $createdBy): Expense
{
$expense = new Expense();
$expense->setUid(Uuid::v7()->toString());
// ...
$expense->setCreatedBy($createdBy);
$expense->setCreatedDate(new \DateTime());
$this->manager->persist($expense);
$this->manager->flush();
return $expense;
}
public function generateWallet(string $label): Wallet
{
$wallet = new Wallet();
$wallet->setUid(Uuid::v7()->toString());
// ...
$wallet->setCreatedBy($this->generatedUsers[0]);
$wallet->setCreatedDate(new \DateTime());
$this->manager->persist($wallet);
$this->manager->flush();
return $wallet;
}
/**
* Generates a User instance and store it in the databases.
*
* It also ensures that the password is hashed.
*
* @param string $name name of the user
* @param string $email email of the user
* @param string $password clear (not encrypted) password of the user
* @return User the created user
*/
private function generateUser(string $name, string $email, string $password): User
{
$user = new User();
$user->setName($name);
$user->setEmail($email);
$user->setPassword($this->hasher->hashPassword($user, $password));
$this->manager->persist($user);
$this->manager->flush();
return $user;
}
}
- Complétez les méthodes
generateXUserWallet,generateExpenseetgenerateWalletpour initialiser tous les champs des entités correspondantes. - Exécutez les fixtures avec la commande suivante :
symfony console doctrine:fixtures:load
Creation du WalletService et listing des Wallets pour un utilisateur
Nous allons maintenant créer un service WalletService pour gérer la logique métier liée aux portefeuilles (Wallets).
Consigne
- Créez un dossier
Servicedans le dossiersrc. - Dans ce dossier, créez un fichier
WalletService.php. - Avec l'autowiring, injectez le
WalletRepositorydans le constructeur du service. - Créez une méthode
findWalletsForUser(User $user): arrayqui retourne la liste des portefeuilles (Wallets) pour un utilisateur donné. Cette méthode fera appel à une requête personnalisée dans le repository. - Implémentez la requête nécessaire dans le repository pour récupérer ces portefeuilles liés à l'utilisateur via l'entité
XUserWallet.- Astuce : vous devrez faire une jointure entre les entités
WalletetXUserWalletpour filtrer les portefeuilles liés à l'utilisateur.
- Astuce : vous devrez faire une jointure entre les entités
- Appelez la requêtte dans la méthode
getWalletsForUserdu service.
Maintenant que la logique est en place, vous pouvez l'utiliser dans un contrôleur pour afficher la liste des portefeuilles d'un utilisateur.
Consigne
- Dans le contrôleur
ListControllerdu dossierWallet, injectez leWalletServicedans la méthode du controller. - Dans la méthode
index, utilisez le service pour récupérer les portefeuilles de l'utilisateur connecté ($this->getUser()). - Passez la liste des portefeuilles à la vue Twig pour les afficher.
- Modifiez la vue Twig
index.html.twigdans le dossierWalletpour afficher les portefeuilles récupérés.- La liste pourra afficher le label et le montant total de chaque portefeuille
- Elle devra aussi pouvoir être cliquable pour accéder aux détails du portefeuille (nous verrons cela plus tard).
- Testez en vous connectant avec un des utilisateurs créés par les fixtures.
Création de l'ExpenseService et listing des Expenses pour un Wallet
Nous allons maintenant créer un service ExpenseService pour gérer la logique métier liée aux dépenses (Expenses).
Consigne
- Dans le dossier
Service, créez un fichierExpenseService.php. - Avec l'autowiring, injectez le
ExpenseRepositorydans le constructeur du service. - Créez une méthode
findExpensesForWallet(Wallet $wallet, int $page, int $limit): arrayqui retourne la liste paginée des dépenses (Expenses) pour un portefeuille (Wallet) donné. - Implémentez la requête nécessaire dans le repository pour récupérer ces dépenses liées au portefeuille avec la pagination.
- Astuce : vous devrez utiliser les méthodes
setFirstResultetsetMaxResultsde Doctrine pour gérer la pagination. - Pour le calcul du
firstResult, utilisez la formule suivante :(page - 1) * limit.
- Astuce : vous devrez utiliser les méthodes
- Appelez la requête dans la méthode
findExpensesForWalletdu service.
Maintenant que la logique est en place, vous pouvez l'utiliser dans un contrôleur pour afficher la liste des dépenses d'un portefeuille.
Consigne
- Dans le contrôleur
DetailControllerdu dossierWallet, injectez leExpenseServicedans la méthode du controller. - Injectez aussi le
WalletService - Dans le
WalletService, créez une méthodegetUserAccessOnWallet(User $user, Wallet $wallet): null|XUserWalletqui retourne la relationXUserWalletentre un utilisateur et un portefeuille, ounullsi l'utilisateur n'a pas accès au portefeuille.- Vous aurez besoin d'injecter le
XUserWalletRepositorydans le service pour faire cette requête. - Vous pourrez utiliser la méthode
findOneBydu repository pour récupérer la relation.
- Vous aurez besoin d'injecter le
- Dans la méthode
indexdu contrôleur, utilisez cette méthode pour vérifier que l'utilisateur connecté a bien accès au portefeuille demandé.- Si l'utilisateur n'a pas accès, vous pouvez le renvoyer sur la liste des wallets avec un message d'erreur (utilisez les Flash Messages de Symfony).
- Changez la route :
- le paramètre dans la route doit devenir
{uid}au lieu de{id}. - le paramètre de la fonction doit devenir
Wallet $walletau lieu deint $id. (Symfony fera l'injection automatique de l'entité basée sur l'UID passé dans l'URL).
- le paramètre dans la route doit devenir
- Toujours dans la méthode
index, ajoutez deux paramètres supplémentaires$pageet$limitpour gérer la pagination (vous pouvez définir des valeurs par défaut, par exemple$page = 1et$limit = 10). Utilisez la synthaxe suivante :
public function index(
// le wallet lié à la route
Wallet $wallet,
// numéro de page où l'on se trouve
#[MapQueryParameter] int $page = 1,
// nombre d'éléments par page
#[MapQueryParameter] int $limit = 25
// injection de dépendances
WalletService $walletService,
ExpenseService $expenseService
) {
//...
}- Utilisez le service pour récupérer les dépenses du portefeuille avec la pagination.
- Passez la liste des dépenses à la vue Twig pour les afficher.
- Modifiez la vue Twig
details/index.html.twigdans le dossierWalletpour afficher les dépenses récupérées.- La liste pourra afficher la description, le montant et la date de chaque dépense.
- Testez en vous connectant avec un des utilisateurs créés par les fixtures et en accédant à un portefeuille.
Enfin, nous allons gérer l'affichage de la pagination dans la vue Twig.
Consigne
- Dans le service
ExpenseService, créez une méthodecountExpensesForWallet(Wallet $wallet): intqui retourne le nombre total de dépenses pour un portefeuille donné. (vous pouvez utiliser une requête COUNT dans le repository). - Dans le contrôleur
DetailController, utilisez cette méthode pour récupérer le nombre total de dépenses. - Calculez le nombre total de pages en fonction du nombre total de dépenses et du nombre d'éléments par page.
- Utilisez la formule suivante :
totalPages = ceil(totalExpenses / limit).
- Utilisez la formule suivante :
- Passez le nombre total de pages à la vue Twig.
- Dans la vue Twig
details/index.html.twig, ajoutez des liens de pagination en bas de la liste des dépenses.- Affichez des liens pour chaque page (1, 2, 3, etc.) en utilisant une boucle.
- Assurez-vous que les liens conservent les paramètres de pagination dans l'URL. (vous pouvez utiliser la fonction
pathde Twig avec les paramètres appropriés).
- Testez la pagination en naviguant entre les pages des dépenses.