📘

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.

⚙
La majoritĂ© des frameworks disposent d’un ORM, par exemple : - Artisan pour Laravel (PHP) - Hibernate pour Java - SQLAchemy, Django, Orator pour Python
📌
Pour aller plus loin : https://www.doctrine-project.org/projects/orm.html https://fr.wikipedia.org/wiki/Mapping_objet-relationnel https://www.base-de-donnees.com/orm/

Mise en place


⚠
Pour les mots de passe, il n’est pas possible d’utiliser de dollar $ (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

⚠
Si on se trompe lors de la crĂ©ation de l’entitĂ©, il vaut mieux tout supprimer et refaire plutĂŽt que d’essayer de modifier les fichiers concernĂ©s. De mĂȘme pour un attribut, il vaut mieux le supprimer avec ses getter/setter et l’ajouter Ă  nouveau.
// 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Ă© :

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

<?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')
        );
    }

}

// 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 :

  1. en orienté objet avec le QueryBuilder
  1. en (presque) SQL avec le DQL (Doctrine Query Language)

⚠
Les requĂȘtes se font sur des objets PHP et pas sur les tables : - on met le chemin vers l’entitĂ© dans le FROM - on fait un SELECT sur une instance - on fait une jointure sur un attribut

DQL

Dans le Repository d’une entitĂ©,

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Ă©,

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


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.

  1. une seul base de donnĂ©es, avec autant de tables qu’on veut
  1. pas de login
  1. pas de mot de passe

On l’utilise notamment pour les tests unitaires.

⚠
On ne l’utilise jamais en production.
// 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.

Dans le contrĂŽleur,

#[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
        ]);
    }