Objectifs de cette séquence :
Exécution typique d’une application Web
Mais aussi interaction entre utilisateurs sur données partagées
Programmer :
Modèle de données en mémoire (à chaud) :
programmation objet
Modèle de données en base de données (à froid, persistant) :
base de données relationnelle (SGBD, SQL)
ORM Object Relational Mapper
https://www.doctrine-project.org/projects/orm.html
Conception orientée objet
Propriétés mono-valuées des classes :
types de bases + références à des instances d’autres classes
Propriétés multi-valués :
Collections d’objets (ou de références d’objets)
Modéliser des tâches : classe Todo
Propriétés :
title
: chaînecompleted
: booléencreated
, updated
: datesclass Todo
{
private string $title;
private bool $completed;
private DateTime $created;
private DateTime $updated;
Modéliser des projets : classe Project
Propriétés :
title
: chaînedescription
: chaîneclass Project
{
private string $title;
private string $description;
Association 1-n entre Project
et Todo
les tâches d’un projet (Project::todos
)
class Project
{
private string $title;
private description $completed;
private array $todos = array();
le projet d’une tâche (Todo::project
)
class Todo
{
private string $title;
private bool $completed;
private DateTime $created;
private DateTime $updated;
private Project $project;
Quelle est la « force » de l’association ?
Pourra être approfondi dans CSC4102 « Introduction au Génie Logiciel Orienté Objet »
Vous maîtrisez déjà :
À l’exécution, sous le capot : génère des requêtes SQL dans SGBD relationnel (appris en CSC3601)… mais pas besoin de les programmer
Comprendre pour debugger si tout ne marche pas comme prévu automagiquement.
Objectifs de cette séquence :
Classe de l’entité Todo
:
Todo
src/Entity/Todo.php
App\Entity\Todo
class Todo
{
private string $title;
private bool $completed;
public function getTitle() {
// ...
public function setTitle($title) {
// ...
// ...
Pas de typage des propriétés, en « PHP basique », mais possible en PHP moderne (et objet).
Docblocks PHP « standard » (sans ORM) :
class Todo {
/**
* @var string task title
*/
private string $title;
/**
* @var bool Is the task completed/finished.
*/
private bool $completed;
}
Ajout de méta-données pour Doctrine
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Todo
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(length: 255, nullable: true)]
private string $title;
#[ORM\ManyToOne(targetEntity: Project::class,
inversedBy: 'todos')]
private Project $project;
//...
}
#[ORM\Entity]
class Project
{
#[ORM\Id, ORM\GeneratedValue]
private int $id;
#[ORM\Column(type: "text", nullable: true)]
private string description;
#[ORM\OneToMany(targetEntity: Todo::Class, mappedBy: 'project')]
private $todos;
//...
}
make:entity
Asssistant générateur de code : make:entity
Abus fortement recommandé !
On gère le chargement des données grâce à un « générateur d’objets » (le Repository), associé à une classe Doctrine.
use Doctrine\ORM\Mapping as ORM;
use App\Repository\TodoRepository;
#[ORM\Entity(repositoryClass: TodoRepository::class)]
class Todo
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
findAll()
)
Utilisation pour charger toutes les instances de Todo
:
use App\Entity\Todo;
//...
protected function execute()
{
// récupère une liste toutes les instances de Todo
$todos = $this->todoRepository->findAll();
foreach($todos as $todo)
{
//...
}
}
use App\Entity\Todo; use App\Repository\TodoRepository; //... class ListTodosCommand extends Command { /** * @var TodoRepository data access repository */ private $todoRepository; public function __construct(ManagerRegistry $doctrineManager) { $this->todoRepository = $doctrineManager ->getRepository(Todo::class); parent::__construct(); } protected function execute(): int { // fetches all instances of class Todo from the DB $todos = $this->todoRepository->findAll(); // ...
find...()
)Chargement d’un seul objet depuis la base de données.
3 variantes :
Encore grâce à la même classe repository.
find($id)
$todo = $this->todoRepository->find($id);
génère, via Doctrine :
SELECT * FROM ... WHERE ID=[$id]
Attention : les identifiants sont uniques, mais un détail d’implémentation (valeur générée par le SGBD).
findOneBy()
)
Chargement d’une instance de Todo
, étant donnés un titre et un état terminé
(extrait de ShowTodoCommand.php
) :
$todo = $this->todoRepository->findOneBy(
['title' => $title,
'completed' => $completed]);
génère une requête SQL style :
SELECT ... FROM todo
WHERE title = [$title]
AND completed = [$completed]
LIMIT 1
Méthodes findBy*
nommées d’après les propriétés de la classe (magic
finders) :
Exemple :
$todos = $this->todoRepository->findByCompleted(false);
donne :
..
WHERE completed = 0;
Les classes repository sont générées avec des fonctionnalités basiques.
Si besoin, on complète le repository pour ajouter des requêtes spécifiques à notre contexte applicatif :
class TodoRepository extends ServiceEntityRepository
{
//...
public function findLatest($page)
{
//...
}
Outils à utiliser dans env. de développement !
Qui gère la base de données ?
Dans les deux cas Doctrine fait le job
Dans notre contexte, on est dans le second cas.
ORM
dans code des classes PHPCREATE TABLE todo (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
title VARCHAR(255) NOT NULL,
completed BOOLEAN NOT NULL);
.env
Exécuter :
symfony console doctrine:database:create
symfony console doctrine:schema:create
La base de données (fichier SQLite) est créée dans le répertoire courant, avec des tables vides.
Voir le schéma de données généré dans la base :
$ bin/console doctrine:schema:create --dump-sql
Exemple :
CREATE TABLE project (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
title VARCHAR(255) NOT NULL,
description CLOB DEFAULT NULL);
CREATE TABLE todo (
tid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
project_id INTEGER DEFAULT NULL,
title CLOB DEFAULT NULL,
completed BOOLEAN NOT NULL,
-- ...
CONSTRAINT FK_CD826255166D1F9C FOREIGN KEY (project_id)
REFERENCES project (id) NOT DEFERRABLE INITIALLY IMMEDIATE);
...
ManyToMany
Exemple : étiquettes (Tag
) sur des tâches (Todo
)
CREATE TABLE todo (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
...);
CREATE TABLE tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
...);
-- Génération d'une table de relation
CREATE TABLE todo_tag (
todo_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY(todo_id, tag_id),
CONSTRAINT FK_D767A0BAEA1EBC33
FOREIGN KEY (todo_id)
REFERENCES todo (id),
CONSTRAINT FK_D767A0BABAD26311
FOREIGN KEY (tag_id)
REFERENCES tag (id) );
Sauvegarder les ajouts / suppressions / modifications
Deux étapes :
C’est le rôle de l’entity manager (em) de l’ORM Doctrine d’effectuer cette synchronisation.
public function createAction(ManagerRegistry $doctrine)
{
$project = new Project();
$project->setTitle('CSC4101');
$project->setDescription("Architectures et applications Web");
// entity manager
$entityManager= $doctrine->getManager();
// indique à Doctrine que vous aimeriez éventuellement
// sauvegarder ce Projet (mais pas encore de requête)
$entityManager->persist($project);
// exécute effectivement la sauvegarde (ex. la requête INSERT)
$entityManager->flush();
// L'identifiant est enfin renseigné (généré par le SGBD)
return new Response("Sauvegardé le projet d'id ".
$project->getId());
}
class Project
{
#[ORM\OneToMany(targetEntity: Todo::Class, mappedBy: 'project')]
private $todos;
public function addTodo(Todo $todo) {
if (!$this->todos->contains($todo)) {
$this->todos->add($todo);
$todo->setProject($this);
}
//...
}
public function removeTodo(Todo $todo) {
if ($this->todos->contains($todo)) {
$this->todos->removeElement($todo);
// set the owning side to null
// (unless already changed)
if ($todo->getProject() === $this) {
$todo->setProject(null);
}
}
OneToMany
entre Project
et Todo
class Project
{
public function addTodo(Todo $todo): self
{
if (!$this->todos->contains($todo)) {
$this->todos->add($todo);
$todo->setProject($this);
}
return $this;
}
Attention : qui est modifié (pour le persist()
ultérieur) ?
Le nouveau Todo
!
Code généré par l’assistant make:entity
:
class Project
{
public function removeTodo(Todo $todo): self
{
if ($this->todos->contains($todo)) {
$this->todos->removeElement($todo);
// set the owning side to null
// (unless already changed)
if ($todo->getProject() === $this) {
$todo->setProject(null);
}
}
return $this;
}
Persist ?
Possibilité gestion automatique des associations
persist()
en cascadePropagation en cascade :
#[OneToMany(... cascade: ['persist', 'remove'] ...)]
$toto = new Todo();
$project->addTodo($todo)
$entityManager->persist($project);
Sauvegarde de ses todos
modifiés
#[OneToMany(... cascade: ['persist'], orphanRemoval: true)]
$todo = ...
$todo->setProject(null);
$entityManager->persist($todo);
Suppression en base
En cas de doute : vérifier les requêtes générées (dans l’outil Doctrine de la barre d’outils Symfony, ou dans les logs)
Coder une classe utilitaire DataFixtures pour Doctrine
Exemple :
Chargement dans la base de données :
private function loadProjects(ObjectManager $manager)
{
foreach ($this->getProjectsData() as [$title, $description]) {
$project = new Project();
$project->setTitle($title);
$project->setDescription($description);
$manager->persist($project);
}
$manager->flush();
}
Définition des données dans un générateur :
private function getProjectsData()
{
// project = [title, description];
yield ['CSC4101', "Architectures et applications Web"];
yield ['CSC4102', "Introduction au Génie Logiciel Orienté Objet"];
}
À refaire à chaque recréation de la base de données dans l’environnement de développement :
$ symfony console doctrine:fixtures:load
Careful, database will be purged. Do you want to continue y/N ?y
> purging database
> loading App\DataFixtures\ProjectFixtures