
Données et Doctrine
Doctrine est lâORM de Symfony.
Un ORM ou mapping objet-relationnel (object-relational mapping) est un type de programme informatique qui se place en interface entre un programme applicatif et une base de données relationnelle pour simuler une base de données orientée objet. Ce programme définit des correspondances entre les schémas de la base de données et les classes du programme applicatif, comme une « couche d'abstraction entre le monde objet et monde relationnel ».
ConcrĂštement, lâORM met Ă disposition des classes objet permettant de manipuler les bases de donnĂ©es relationnelles. Le dĂ©veloppeur manipule ainsi des objets et lâORM transforme le tout en requĂȘtes comprĂ©hensibles par la base de donnĂ©es.
Mise en place
- installer lâORM :
composer require symfony/orm-pack
- dans le php.ini, décommenter la ligne correspondant au SGBD choisi
- ouvrir le fichier .env :
- il y a des exemples de valeurs pour la variable DATABASE_URL qui permet de se connecter à la base de données de notre choix, sous la forme
DATABASE_URL="monSBGD://monLogin:monMDP@127.0.0.1:monPort/maBaseDeDonnees"
- on les laisse commenter puisque le fichier .env est un fichier partagĂ© (notamment sur git) et on ne va pas mettre les login/mot de passe en clair iciâŠ.
- il y a des exemples de valeurs pour la variable DATABASE_URL qui permet de se connecter à la base de données de notre choix, sous la forme
- créer un fichier .env.local :
- on copie la version de la
DATABASE_URLen accord avec le SGBD quâon a choisi :DATABASE_URL="mysql://root:unjolimotdepasse@127.0.0.1:3306/bucketlist"
- on la complÚte à façon
- on ne met jamais le .env.local sur git : chaque collaborateur a ses propres identifiants
- on copie la version de la
- créer une base de données à partir des informations dans le .env.local :
symfony console doctrine:database:create- si elle nâexiste pas, la base de donnĂ©es est créée
- deux dossiers sont créés dans src : Entity et Repository
- se connecter Ă la base de donnĂ©es depuis lâonglet Database : + > Data Source > SGBD
- ajuster et tester la connexion
- télécharger les drivers, au besoin
$ (variables en PHP) ni de point dâinterrogation ? (paramĂštres dans lâURL).
Entités
Une entité est une classe PHP représentant les données, ainsi une classe correspond à une table et une propriété de classe correspond à un champ de table.
BP : Une entité est une classe dite POJO ou minimaliste avec simplement des attributs, des getters/setters, un constructeur, un toString.
A chaque entitĂ© (classe) créée, la table correspondante est créée en base avec en plus, pour les manitoumani, une table dâassociation.
Lâidentificateur unique (id) est créé de maniĂšre automatique, auto-incrĂ©mentĂ©, spĂ©cifiĂ© comme clĂ© primaire (PK) et utilisĂ© comme clĂ© Ă©trangĂšre (FK).
Pour créer (et modifier) une entité : symfony console make:entity > NomEntite
- lâentitĂ© NomEntite.php et le repository NomEntiteRepository.php sont créés
BP : On donne le mĂȘme nom Ă lâentitĂ© et Ă son contrĂŽleur.
- suite de questions pour dĂ©finir chaque propriĂ©tĂ© : nom de lâattribut, type, null ou nonâŠ
BP : On ne met pas dâimages dans des classes, mais des liens vers les images (qui sont stockĂ©es dans public).
// entite Movie
<?php
namespace App\Entity;
use App\Repository\MovieRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MovieRepository::class)]
class Movie
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(nullable: true)]
private ?int $releaseYear = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $country = null;
#[ORM\Column(nullable: true)]
private ?bool $wasSeen = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getReleaseYear(): ?int
{
return $this->releaseYear;
}
public function setReleaseYear(?int $releaseYear): self
{
$this->releaseYear = $releaseYear;
return $this;
}
public function getCountry(): ?string
{
return $this->country;
}
public function setCountry(?string $country): self
{
$this->country = $country;
return $this;
}
public function isWasSeen(): ?bool
{
return $this->wasSeen;
}
public function setWasSeen(?bool $wasSeen): self
{
$this->wasSeen = $wasSeen;
return $this;
}
}Tout est généré automatiquement : propriétés et getters/setters.
Les contraintes de champs à appliquer en base de données sont indiquées par des attributs @ORM\ (ou annotations avant PHP 8).
// repository MovieRepository
<?php
namespace App\Repository;
use App\Entity\Movie;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Movie>
*
* @method Movie|null find($id, $lockMode = null, $lockVersion = null)
* @method Movie|null findOneBy(array $criteria, array $orderBy = null)
* @method Movie[] findAll()
* @method Movie[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class MovieRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Movie::class);
}
public function save(Movie $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Movie $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return Movie[] Returns an array of Movie objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('m.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Movie
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}Le repository hérite de ServiceEntityRepository.
Tout est gĂ©nĂ©rĂ© automatiquement : le lien avec lâentitĂ© Movie, le constructeur, les mĂ©thodes pour enregistrer, supprimer et rechercher dans la base de donnĂ©es (save(), remove(), find(), findOneBy(), findAll(), findBy()).
Pour crĂ©er (ou mettre Ă jour) Ă la table dâune entitĂ© :
- en environnement de développement :
symfony console doctrine:schema:update --force
- en production, on veut garder un historique des modifications de la base de données :
symfony console make:migration
EntityManager
Gestionnaire dâentitĂ©s.
Un seul EntityManager pour lâensemble des entitĂ©s, qui gĂšre la crĂ©ation, la mise Ă jour et la suppression dans la base de donnĂ©es : Create Update Delete du CRUD.
Il est récupéré par injection de dépendances de EntityManagerInterface dans les méthodes concernées des contrÎleurs et instancié $em ou $entityManager.
Principales méthodes :
persist() pour créer ou mettre à jour une ligne en base de données (INSERT et UPDATE),
flush() pour effectivement envoyer en base les modifications stockées dans le cache,
remove() pour supprimer une ligne en base de données (DELETE),
createQuery() pour crĂ©er une requĂȘte personnalisĂ©e.
Repository
DépÎt.
Un Repository par entité, qui gÚre la lecture et la recherche dans la base de données : Read du CRUD.
Il est récupéré par injection de dépendances de dans les méthodes concernées des contrÎleurs et instancié $repository.
Principales méthodes :
findAll(), find($var), findOneByTitle($title), findOneBy(["name" => "bob"]), findBy([], ["price" => "DESC"], 30, 0),
count() pour rĂ©cupĂ©rer le nombre dâenregistrements.
Démonstration
- MovieController avec une mĂ©thode pour lister tous les films, une pour afficher le dĂ©tail dâun seul film et une pour ajouter un film (voir module suivant pour un formulaire)
<?php
namespace App\Controller;
use App\Entity\Movie;
use App\Repository\MovieRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/movie', name: 'movie')]
class MovieController extends AbstractController
{
#[Route('/', name: '_list')]
public function list(
MovieRepository $movieRepository
): Response
{
$movies = $movieRepository->findAll();
dump($movies);
return $this->render('movie/list.html.twig',
compact('movies')
);
}
#[Route('/{id}', name: '_detail')]
public function detail(
int $id,
MovieRepository $movieRepository
): Response
{
$movie = $movieRepository->find($id);
// $movie = $movieRepository->findOneBy(['id' => $id]); // equivalent
return $this->render('movie/detail.html.twig',
compact('movie', 'id')
);
}
#[Route('/create', name: '_create')]
public function create(
EntityManagerInterface $entityManager
): Response
{
// a transformer en formulaire
$movie = (new Movie())
->setTitle("Vol au-dessus d'un nid de coucou")
->setReleaseYear(1975)
->setCountry("Etats-Unis")
->setWasSeen(false);
$entityManager->persist($movie);
$entityManager->flush();
return $this->render('movie/create.html.twig',
compact('movie')
);
}
}
- Twigs associĂ©s : un par mĂ©thode â uniquement la partie Ă lâintĂ©rieur du bloc body
// list.html.twig
<h2>Liste de films Ă voir absolument</h2>
<ol>
{% for movie in movies %}
<li><a href="{{ path('movie_detail', {'id': movie.id}) }}">{{ movie.title }}</a> ({{ movie.country }}) sorti en {{ movie.releaseYear }}</li>
{% else %}
Aucun film n'a été ajouté pour l'instant.
{% endfor %}
</ol>
// detail.html.twig
<div>
Le film n°{{ id }} est <strong>{{ movie.title }}</strong> ({{ movie.country }}) sorti en {{ movie.releaseYear }}.<br><br>
{% if movie.wasSeen %}
Vu !!
{% else %}
Pas encore vu !
{% endif %}
</div>
// create.html.twig
<div>
Le film ajouté est <strong>{{ movie.title }}</strong> ({{ movie.country }}) sorti en {{ movie.releaseYear }}.
</div>
RequĂȘtes personnalisĂ©es
Les mĂ©thodes proposĂ©es par lâEntityManager et les Repository permettent de rĂ©pondre Ă la trĂšs grande majoritĂ© des besoins.
NĂ©anmoins, on peut crĂ©er notre propre requĂȘte personnalisĂ©e :
- en orienté objet avec le QueryBuilder
- en (presque) SQL avec le DQL (Doctrine Query Language)
DQL
Dans le Repository dâune entitĂ©,
- on crée une nouvelle méthode
- on injecte lâEntityManager
- on crĂ©e une requĂȘte
$dql
- on exĂ©cute la requĂȘte
createQuery()
- on récupÚre le résultat avec
getResult()
La mĂ©thode peut ĂȘtre appelĂ©e dans les contrĂŽleurs comme toutes les autres mĂ©thodes.
QueryBuilder
Lâobjectif est de gĂ©nĂ©rer une requĂȘte DQL grĂące Ă un enchaĂźnement de conditions.
Dans le Repository dâune entitĂ©,
- on crée une nouvelle méthode
- on crée une instance
$queryBuilderde QueryBuilder
- on ajoute des conditions avec des méthodes comme
select(),andWhere()âŠ
- on exĂ©cute la requĂȘte avec
getQuery()
- on récupÚre le résultat avec
getResult()
RequĂȘtes dynamiques
Il est possible de rendre la requĂȘte dynamique avec des if.
Exemple :
Dans le SerieRepository, une fonction pour la moyenne des notes des séries de Science-Fiction
/**
* @return Serie
* @throws NonUniqueResultException
*/
public function findAverageSF() {
return $this->createQueryBuilder('s')
->select('avg(s.vote)')
->andWhere("s.genres like '%Sci-Fi'")
->getQuery()
->getOneOrNullResult();
}Dans le SerieController, une nouvelle méthode :
#[Route('/averagesf', name: '_averagesf')]
public function averagesf(
SerieRepository $serieRepository
): Response
{
$avgsf = $serieRepository->findAverageSF();
dump($avgsf); // retourne le resultat dans la barre de debug
return $this->render('serie/avgsf.html.twig,
['avgsf' => $avgsf]
);
}
En vrac
- Dans un twig, on concatĂšne avec
~.
- Pour savoir les requĂȘtes SQL qui ont Ă©tĂ© faites, on les a dans la barre de dĂ©bogage.
- Quand on fait un moteur de recherche, on crĂ©e une requĂȘte personnalisĂ©e.
- On peut crĂ©er un âserviceâ (gros contrĂŽleur) : on y met les contraintes mĂ©tier et les autres contrĂŽleurs sây rĂ©fĂšrent. On peut aussi mettre des contraintes mĂ©tiers dans lâattribut de lâentitĂ©.
- Un paquet pour envoyer des mails :
composer require symfony/mailer
- Plugins : rainbow brackets, wakatime
BP : En collaboration, on indique en commentaires TODO (âĂ faireâ) et FIXME (âje sais que ça marche pas mais jâai pas eu le tempsâ).
SQLite
Une base de donnĂ©e SQL minimaliste qui tient en un seul fichier et quâon peut mettre sur Git.
- une seul base de donnĂ©es, avec autant de tables quâon veut
- pas de login
- pas de mot de passe
On lâutilise notamment pour les tests unitaires.
// exemple dans le .env.local
DATABASE_URL="sqlite:///%kernel.project_dir%/hello.sqlite"
Hors-programme
Nouveau paquet qui simplifie le passage en paramĂštre de lâURL, uniquement pour lâid. On rĂ©cupĂšre un objet et pas un int.
- installer le paquet :
composer require sensio/framework-extra-bundle
Dans le contrĂŽleur,
- on injecte lâentitĂ© voulue (au lieu dâun int) et la nomme
$id
#[Route('/{id}', name: '_detail')]
public function detail(
Movie $id,
MovieRepository $movieRepository
): Response
{
$movie = $movieRepository->find($id);
return $this->render('movie/detail.html.twig',[
'movie' => $id
]);
}