Vous êtes sur la page 1sur 20

Module Framework - partie 4 - année 2021-22

Yoann DIEUDONNÉ & Stéphane DEVISMES - Université de Picardie

Doctrine : association d’entité


et formulaire sur entité
Association … Relation :
• « ordinateur(s) installé(s) dans la salle ».

ci-contre la relation entre tables dans une base


de données :

Les relations sont « classiques » en base de


données : comment se traduisent-elles entre
entités de Doctrine ?

◦ En association entre entités :


D’un coté, des références (pointeurs) sur
des objets,
de l’autre, des clés étrangères (foreign
key) et des jointures.

◦ Cardinalité des associations :


▪ ManyToOne exemple : la salle dans laquelle est installée l'ordinateur
▪ OneToMany exemple : les ordinateurs installés dans la salle
donc bidirectionnelle de la ManyToOne
▪ ManyToMany exemple : les logiciels installés sur des ordinateurs
un logiciel peut être installé sur plusieurs ordinateurs
un ordinateur peut avoir plusieurs logiciels
▪ OneToOne exemple: une personne et son numéro de sécurité
sociale …. non étudié

◦ Sens de l'association:
▪ unidirectionnelle : une seule !
• ManyToOne
• ManyToMany
• OneToOne
▪ Bidirectionnelle : l’association et son inverse
• une ManyToOne et la OneToMany inverse
• deux ManyToMany
• deux OneToOne
• Le coté « owner » des associations bidirectionnelles :
problème compliqué …...à suivre

many to one unidirectionnel :

• Ajoutons une association à l’entité Ordinateur :


◦ lancez la commande make:entity :
symfony console make:entity
Class name of the entity to create or update (e.g. OrangePuppy):
> Ordinateur
New property name (press <return> to stop adding fields):
> salle
Field type (enter ? to see all types) [string]:
> relation
What class should this entity be related to?:
> Salle
What type of relationship is this?
Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToOne
Is the Ordinateur.salle property allowed to be null (nullable)? (yes/no) [yes]:
> yes
Do you want to add a new property to Salle so that you can access/update
Ordinateur objects from it - e.g. $salle->getOrdinateurs()? (yes/no) [yes]:
> no
updated: src/Entity/Ordinateur.php
Add another property? Enter the property name (or press <return> to stop adding
fields):
>
Success!
◦ Nous définissons une association unidirectionnelle "salle" de l'entité Ordinateur
vers l'entité Salle:
• rmq : le nom de l'association «salle» pourrait être différent de celui de
l'Entité correspondante
▪ chaque ordinateur est associé à une salle (au plus) ou pas
▪ plusieurs ordinateurs pourront être associés à la même salle
◦ Voici ce qui a été ajouté dans le fichier de l'entité Ordinateur :
// ne pas programmer !!
...
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Salle")
*/
private $salle;
...
public function setSalle(\SalleTpBundle\Entity\Salle $salle = null) {
$this->salle = $salle;
return $this;
}
public function getSalle() {
return $this->salle;
}
}
◦ Mettez à jour la base de données
symfony console make:migration
Success!

symfony console doctrine:migrations:migrate


-> ALTER TABLE ordinateur ADD salle_id INT DEFAULT NULL
-> ALTER TABLE ordinateur ADD CONSTRAINT FK_8712E8DBDC304035 FOREIGN
KEY (salle_id) REFERENCES salle (id)
-> CREATE INDEX IDX_8712E8DBDC304035 ON ordinateur (salle_id)
▪ Ceci ajoute un champ "salle_id" dans la table ordinateur qui est une clé
étrangère vers l"id" de la table salle
• Ajout d'entités reliées
◦ Associons un ordinateur à une salle
▪ modifier EssaiController :
use App\Entity\Ordinateur;
use App\Entity\Salle;
...
public function test25() {
$em = $this->getDoctrine()->getManager();
$salle = $em->getRepository(Salle::class)->findOneBy(array('batiment'=>'D',
'etage'=>7, 'numero'=>71));
$ordi = new Ordinateur;
$ordi->setNumero(702);
$ordi->setIp('192.168.7.02');
$ordi->setSalle($salle);
$em->persist($ordi);
$em->flush();
dump($ordi);
return new Response('<html><body></body></html>');
}

◦ tester http://localhost:8000/essai/test25
App\Entity\Ordinateur^ {#676
-id: 3
-ip: "192.168.7.02"
-numero: 702
-salle: App\Entity\Salle^ {#678
-id: 9
-batiment: "D"
-etage: 7
-numero: 71
}
}
▪ L’association s’est bien faite.

• Premier Problème … de persistance


◦ modifier EssaiController :
public function test26() {
$em = $this->getDoctrine()->getManager();
$salle = new Salle;
$salle->setBatiment('B');
$salle->setEtage(0);
$salle->setNumero(0);
$ordi = new Ordinateur;
$ordi->setNumero(701);
$ordi->setIp('192.168.7.01');
$ordi->setSalle($salle);
$em->persist($ordi);
$em->flush();
dump($ordi);
return new Response('<html><body></body></html>');
}
◦ tester http://localhost:8000/essai/test26

▪ effectivement, la salle n’est pas "persistée" donc persistante


dans le test25, il n'y a pas eu d'erreur car "find()" entraîne la persistance de
la salle obtenue.
◦ Correction : Persistons aussi l'entité salle
public function test26() {
...
$em->persist($ordi);
$em->persist($salle);
$em->flush();
...
◦ Executons http://localhost:8000/essai/test26
▪ ça marche !
▪ C'est au programmeur de mettre en place pour les entités les actions qui
devront être répercutées au niveau de la base de données ! Bref, la
persistance.

• Les cascades :
◦ Modifiez le fichier Entity/Ordinateur.php
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Salle", cascade={"persist"})
*/
private $salle;
◦ la persistance de l'entité Ordinateur est étendue en cascade à son entité
associée
▪ Écrivons un nouveau test du code qui avait généré une erreur de
persistance : … avec d'autres numéros d'ordinateur
public function test27() {
$em = $this->getDoctrine()->getManager();
$salle = new Salle;
$salle->setBatiment('B');
$salle->setEtage(0);
$salle->setNumero(1);
$ordi = new Ordinateur;
$ordi->setNumero(703);
$ordi->setIp('192.168.7.03');
$ordi->setSalle($salle);
$em->persist($ordi);
$em->flush();
dump($ordi);
return new Response('<html><body></body></html>');
}
▪ Exécutons http://localhost:8000/essai/test27 : la cascade de persistance
marche !

◦ (++) Il existe d’autres cascades : remove, …. merge, detach, refresh

• Lire des données d’entités associées


▪ Écrivons un nouveau test28 :
public function test28() {
$ordi = $this->getDoctrine()->getManager()
->getRepository(Ordinateur::class)
->findOneByNumero(703);
dump($ordi);
$batiment = $ordi->getSalle()->getBatiment();
dump($batiment);
dump($ordi);
return new Response('<html><body></body></html>');
}
◦ Ce qui donne bien le résultat escompté

◦ Comment s’est fait la lecture des données ?


▪ Regardons les requêtes SqL
dans le Profiler :
• il y a eu 2 requêtes
successives
• Cela met en évidence le «
lazy loading » chargement
fainéant
◦ le chargement de l'entité associée est confié à un « proxy » qui le fera
uniquement quand ce sera nécessaire
◦ Heureusement, par défaut, quand une entité est lue depuis la base,
toutes les entités qui lui sont associées ne sont pas lues
automatiquement, sinon les performances seraient dégradées.

Pensez à sauvegarder ! → chap4page14.zip

many to one bidirectionnel :

Créons une association OneToMany nommée


ordinateurs de Salle vers Ordinateur :
◦ c'est l'inverse de la ManyToOne "salle"
◦ c'est une collection/liste/tableau de
pointeur vers les ordinateurs associés
◦ Nous obtenons une association
bidirectionnelle entre les entités
Ordinateur et Salle

• Ajoutez l'association de Salle vers Ordinateur :


.....
/**
* @ORM\OneToMany(targetEntity="App\Entity\Ordinateur", mappedBy="salle",
cascade={"persist"})
*/
private $ordinateurs;

◦ et modifiez l'entité Ordinateur.php ainsi :


.....
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Salle", inversedBy="ordinateurs",
cascade={"persist"})
*/
private $salle;
▪ les paramètres mappedBy et inversedBy désignent l'association
inverse/réciproque : Ils ne s'appellent pas, tous les deux, inversedBy car
l'une joue un rôle plus important … à suivre

• Régénérez les entités :


symfony console make:entity --regenerate
◦ L'entité Salle.php est modifiée ainsi :
.....
public function __construct() {
$this->ordinateurs = new ArrayCollection();
}

public function getOrdinateurs(): Collection {


return $this->ordinateurs;
}

public function addOrdinateur(Ordinateur $ordinateur): self {


if (!$this->ordinateurs->contains($ordinateur)) {
$this->ordinateurs[] = $ordinateur;
$ordinateur->setSalle($this);
}
return $this;
}

public function removeOrdinateur(Ordinateur $ordinateur): self {


if ($this->ordinateurs->contains($ordinateur)) {
$this->ordinateurs->removeElement($ordinateur);
if ($ordinateur->getSalle() === $this) {
$ordinateur->setSalle(null);
}
}
return $this;
}
▪ la propriété "ordinateurs" est un tableau de références d'objets donc ses
getters/setters sont adaptées à une propriété indexée

◦ Mettez à jour la base de données :


symfony console make:migration
[WARNING] No database changes were
detected.
• l'association OneToMany n'ajoute rien
en base de donnée :

• Le coté « owner » des associations bidirectionnelles !


◦ Pas de problème pour associer un ordinateur à une salle avec la méthode
setSalle( ) de l’association manyToOne « salle »
◦ essayons l’inverse :
▪ associer une salle à un ordinateur avec la fonction addOrdinateur( ) de
l’association oneToMany « ordinateurs »
▪ Voici une nouvelle action test :
public function test29() {
$em = $this->getDoctrine()->getManager();
$ordi = new Ordinateur;
$ordi->setNumero(803);
$ordi->setIp('192.168.8.03');
$salle = new Salle ;
$salle->setBatiment('D');
$salle->setEtage(8);
$salle->setNumero(03);
$salle->addOrdinateur($ordi);
$em->persist($ordi);
$em->flush();
$ordi = $salle = null;
$ordi = $this->getDoctrine()->getManager()
->getRepository(Ordinateur::class)->findOneByNumero(803);
dump($ordi);
return new Response('<html><body></body></html>');
}
▪ Exécutons http://localhost:8000/essai/test29
Ça marche !
…... car la fonction addOrdinateur fait appel à setSalle (sa réciproque)
▪ Expérience : commentez cette ligne :
public function addOrdinateur(Ordinateur $ordinateur): self {
if (!$this->ordinateurs->contains($ordinateur)) {
$this->ordinateurs[] = $ordinateur;
// $ordinateur->setSalle($this);
}
return $this;
}
• Essayez l'action test30
public function test30() {
$em = $this->getDoctrine()->getManager();
$ordi = new Ordinateur;
$ordi->setNumero(804);
$ordi->setIp('192.168.8.04');
$em->persist($ordi);
$salle = new Salle ;
$salle->setBatiment('D');
$salle->setEtage(8);
$salle->setNumero(8);
$salle->addOrdinateur($ordi);
$em->persist($salle);
$em->flush();
dump($ordi);
return new Response('<html><body></body></html>');
}
• Exécutons test30
L'association ne s'est pas faite !

• Fin de l'expérience :
Enlevez le commentaire sur la ligne setSalle
dans la fonction addOrdinateur

▪ Seule l'association coté


« owner » est effectuée
réellement en base de donnée
• Doctrine « n’écoute » (ne
détecte) que les
modifications du coté « owner » (directeur)

◦ Comment décider du coté « owner » d'une association unidirectionnelle ?


• le coté propriétaire est désigné par l’attribut mappedBy. Plus précisément
la valeur de l’attribut mappedBy désigne le nom qu’a la relation (ici salle)
dans l’entité propriétaire (ici Ordinateur).
• ManyToOne est obligatoirement le coté « owner » : c’est le coté naturel
en SQL qui contient la foreign key
• ManyToMany : peu importe !

Pensez à sauvegarder ! → chap4page8.zip

• Pas de problème de lecture des données de la base via le coté non « owner » :
en clair, par l'appel à la méthode getOrdinateurs()
◦ notre code test :
public function test32() {
$em = $this->getDoctrine()->getManager();
$ordi = new Ordinateur;
$ordi->setNumero(805);
$ordi->setIp('192.168.8.05');
$em->persist($ordi);
$salle = new Salle ;
$salle->setBatiment('D');
$salle->setEtage(8);
$salle->setNumero(85);
$salle->addOrdinateur($ordi);
$em->persist($salle);
$ordi2 = new Ordinateur;
$ordi2->setNumero(806);
$ordi2->setIp('192.168.8.06');
$em->persist($ordi2);
$salle->addOrdinateur($ordi2);
$em->flush();
$id = $salle->getId();
$em->clear();
$salleTrouve = $em->getRepository(Salle::class)->find($id);
$result = "";
foreach($salleTrouve ->getOrdinateurs() as $ordi)
$result .= $ordi->getIp().' ';
$ordinateurs = $salleTrouve->getOrdinateurs();
dump($ordinateurs);
return new Response('<html><body>'.$result.'</body></html>');
}
◦ la méthode clear( ) de l'EntityManager supprime toute persistance
• la réponse http://localhost:8000/essai/test32
192.168.8.05 192.168.8.06
◦ ça marche !
Voici le dump :

Exercices 4.1
• Ajouter une entité Chef qui possède un nom
• Modifier l'entité resto de façon à avoir une association de resto vers chef :
• un resto aura un chef au plus
• un chef peut posséder plusieurs restos
• avec PhpMyAdmin, créer quelques chefs qui seront associés à des restos
• Ajouter une action ModifierResto qui permet de modifier un resto existant sur
le modèle de l’action d’ajout qui a été mis en place via l’exo 1.3.
Supprimer l'association entre entités
• supprimer l'association coté "owner" :
◦ la seule solution est de mettre
l'association à la valeur nulle, si le
champs est "nullable"
public function test33() {
$em = $this->getDoctrine()
->getManager();
$ordi = new Ordinateur;
$ordi->setNumero(807);
$ordi->setIp('192.168.8.07');
$em->persist($ordi);
$salle = new Salle ;
$salle->setBatiment('D');
$salle->setEtage(8);
$salle->setNumero(87);
$ordi->setSalle($salle);
$em->persist($salle);
$em->flush();
dump($ordi);
$ordi->setSalle(null);
$em->flush();
$ordi = $this->getDoctrine()->getManager()
->getRepository(Ordinateur::class)
->findOneByNumero(807);
dump($ordi);
return new Response('<html><body></body></html>');
}
▪ Exécutons

http://localhost:8000/essai/test33
Ça marche !
• supprimer l'association coté non-"owner" :
Pas de problème car le code de la fonction removeOrdinateur() opère la
suppression sur le coté "Owner"
public function removeOrdinateur(Ordinateur $ordinateur): self {
if ($this->ordinateurs->contains($ordinateur)) {
$this->ordinateurs->removeElement($ordinateur);
if ($ordinateur->getSalle() === $this) {
$ordinateur->setSalle(null);
...

Supprimer une entité qui est associée à une autre

• Suppression coté « many» de l’association : pas de PB


public function test35() {
$em = $this->getDoctrine()
->getManager();
$ordi = new Ordinateur;
$ordi->setNumero(808);
$ordi->setIp('192.168.8.08');
$em->persist($ordi);
$salle = new Salle ;
$salle->setBatiment('D');
$salle->setEtage(8);
$salle->setNumero(88);
$ordi->setSalle($salle);
$em->persist($salle);
$em->flush();
dump($ordi);
$em->remove($ordi);
$em->flush();
return new Response('<html><body>'.$salle.'</body></html>');
}
• exécutez et vérifiez avec
PhpMyAdmin que l’ordi 808 n’existe
plus
voici la requête SQL réalisée :

• Suppression coté «one» de l’association :


public function test36() {
$em = $this->getDoctrine()
->getManager();
$salle = new Salle ;
$salle->setBatiment('D');
$salle->setEtage(9);
$salle->setNumero(01);
$ordi = new Ordinateur;
$ordi->setNumero(901);
$ordi->setIp('192.168.9.01');
$em->persist($ordi);
$ordi->setSalle($salle);
$em->flush();
dump($ordi);
$em->remove($salle);
$em->flush();
return new Response('<html><body></body></html>');
}
◦ Ce qui donne une erreur
▪ Du point vue SQL, l'erreur est justifiée, ainsi que du point de vue "pointeur" :
l’ordinateur 901 pointerait sur une salle n'existant plus

▪ Solution possible : le « cascade ondelete »


◦ Ajoutez la cascade sur la colonne associée dans Entity/Ordinateur.php :
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Salle", inversedBy="ordinateurs...
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $salle;

◦ mettez à jour la base de données :


symfony console make:migration
symfony console doctrine:migrations:migrate
-> ALTER TABLE ordinateur DROP FOREIGN KEY FK_8712E8DBDC304035
-> ALTER TABLE ordinateur ADD CONSTRAINT FK_8712E8DBDC304035 FOREIGN
KEY (salle_id) REFERENCES salle (id) ON DELETE CASCADE
◦ cela ajoute la contrainte dans la base
• Testez l'essai test38 :
public function test38() {
$em = $this->getDoctrine()->getManager();
$salle = new Salle ;
$salle->setBatiment('D');
$salle->setEtage(9);
$salle->setNumero(04);
$em->persist($salle);
$ordi1 = new Ordinateur;
$ordi1->setNumero(904);
$ordi1->setIp('192.168.9.04');
$em->persist($ordi1);
$ordi1->setSalle($salle);
$ordi2 = new Ordinateur;
$ordi2->setNumero(905);
$ordi2->setIp('192.168.9.05');
$em->persist($ordi2);
$ordi2->setSalle($salle);
$em->flush();
$idSalle = $salle->getId();
$em->flush();
dump($salle);
$em->remove($salle);
$em->flush();
return new Response('<html><body>rechercher la salle D-9.04 puis
les ordis 904 et 905 avec PhpMyAdmin</body></html>');
}
◦ on a bien l'effet escompté

Exercice 4.2
• Créer une fonction qui supprime un chef et tous ses restos s'il y a
• Ajouter cette nouvelle fonctionnalité dans le site

Pensez à sauvegarder ! → chap4page11.zip


Formulaire avec choix sur une entité associée

• Fabriquons une page de création


d’ordinateur :
◦ le formulaire devra proposer une liste de
choix déroulante des salles disponibles

◦ ci-contre la page souhaitée :

◦ Ajoutez un formulaire type Form/Type/OrdinateurType.php :


<?php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Entity\Ordinateur;
use App\Entity\Salle;

class OrdinateurType extends AbstractType {


public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('numero', TextType::class)
->add('ip', TextType::class)
->add('salle', EntityType::class,
array('class' => Salle::class));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => Ordinateur::class,
));
}
}
▪ il ajoute simplement le champ salle qui est une référence à une entité.
• Il faut que l'entité ait une fonction __toString() discriminante

◦ Créez le contrôleur OrdinateurController.php :


symfony console make:controller
Choose a name for your controller class :
> OrdinateurController
Success!
Et ajoutez lui une fonction ajouter( ) :
use Symfony\Component\HttpFoundation\Request;
use App\Form\Type\OrdinateurType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use App\Entity\Ordinateur;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
...
/**
* @Route("/ajout_ordinateur", name="ajout_ordinateur")
*/
public function ajouter(Request $request) {
$ordinateur = new Ordinateur;
$form = $this->createForm(OrdinateurType::class, $ordinateur);
$form->add('submit', SubmitType::class,
array('label' => 'Ajouter'));
$form->handleRequest($request);
if ($form->isSubmitted()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($ordinateur);
$entityManager->flush();
return $this->redirectToRoute('ordinateur');
}
return $this->render('ordinateur/ajouter.html.twig',
array('monFormulaire' => $form->createView()));}

◦ Créez la vue (pas terrible) ordinateur/ajouter.html.twig :


{% extends 'base.html.twig' %}

{% block title %}Hello OrdinateurController!{% endblock %}

{% block body %}
<h2>ajouter un ordinateur</h2>
{{ form(monFormulaire) }}
{% endblock %}
◦ testez
http://localhost:8000/ajout_ordinateur
Ça marche ! vérifiez avec
PhpmyAdmin

• Expérience :
Commentez la fonction __toString() de l'entité Salle et re testez

L'entité associée doit avoir une visualisation en string


Fin de l'expérience : Enlevez le commentaire de la fonction

• query_builder
◦ Restreignons ce choix aux salles de numéro inférieur à 10 :
ajoutons à OrdinateurType :
use App\Repository\SalleRepository;
...
->add('salle', EntityType::class,
array('class' => Salle::class,
'query_builder' => function (SalleRepository $repo) {
return $repo->createQueryBuilder('s')
->where('s.numero < 10'); }
));
...
◦ testez l’ajout d’ordinateur :
http://localhost:8000/ajout_ordinateur

Pensez à sauvegarder ! → chap4page13.zip

Exercice 4.3 :
• Mettez à jour la page « ajouter un resto » de telle manière à ce que le champs chef
du formulaire corresponde à la liste des chefs (ordonnée selon les noms).
• Mettez à jour le reste de votre site de façon à prendre en compte le fait que le
champs chef de votre classe resto correspond dorénavant à une association et non
une chaîne de caractères.
many to many bidirectionnel :

• Créons une entité Logiciel et une


association avec l'entité Ordinateur :
logiciel_installes/machine_installees

◦ Voici la description de notre


nouvelle entité Entity/Logiciel.php
<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity(repositoryClass="App\Repository\LogicielRepository")
*/
class Logiciel {
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;

/**
* @ORM\Column(type="string", length=25)
*/
private $nom;

/**
* @ORM\ManyToMany(targetEntity="App\Entity\Ordinateur",
* inversedBy="logiciel_installes", cascade={"persist"})
*/
private $machine_installees;
}

◦ et ajoutez la relation inverse dans l'entité Entity/Ordinateur.php


/**
* @ORM\ManyToMany(targetEntity="App\Entity\Logiciel",
* mappedBy="machine_installees", cascade={"persist"})
*/
private $logiciel_installes;
▪ désigné par le « mappedBy », machine_installées est le nom de l’association
coté propriétaire "owner" qui est ici l’entité Logiciel.

◦ Générons, ou mettons à jour, les entités correspondantes puis la BD :


$ symfony console make:entity --regenerate
Enter a class or namespace to regenerate [App\Entity]:
> App\Entity\Ordinateur
Success!
▪ Ajoute des fonctions : getLogicielInstalles( ) addLogicielInstalle( )
removeLogicielInstalle( )
• les fonctions add et remove font appels aux fonctions add et remove du
coté "owner"
$ symfony console make:entity --regenerate
Enter a class or namespace to regenerate [App\Entity]:
> App\Entity\Logiciel
Success!
▪ Ajoute des fonctions : getMachineInstallees( ) addMachineInstallee( )
removeMachineInstallee( )
$ symfony console make:migration
Success!

$ symfony console doctrine:migrations:migrate


-> CREATE TABLE logiciel (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(25)
NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE
utf8mb4_unicode_ci ENGINE = InnoDB
-> CREATE TABLE logiciel_ordinateur (logiciel_id INT NOT NULL, ordinateur_id
INT NOT NULL, INDEX IDX_37263968CA84195D (logiciel_id), INDEX
IDX_37263968828317CA (ordinateur_id), PRIMARY KEY(logiciel_id, ordinateur_id))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB
-> ALTER TABLE logiciel_ordinateur ADD CONSTRAINT FK_37263968CA84195D
FOREIGN KEY (logiciel_id) REFERENCES logiciel (id) ON DELETE CASCADE
-> ALTER TABLE logiciel_ordinateur ADD CONSTRAINT FK_37263968828317CA
FOREIGN KEY (ordinateur_id) REFERENCES ordinateur (id) ON DELETE CASCADE
▪ Crée une table de jointure
« logiciel_ordinateur »

◦ Dans une action test à ajouter dans


src/Controller/EssaiController.php, réalisons
quelques associations entre entités :
use App\Entity\Logiciel;
. . .
public function test39() {
$entityManager = $this->getDoctrine()->getManager();
$ordi1 = new Ordinateur;
$ordi1->setNumero(1000);
$ordi1->setIp('192.168.10.00');
$salle = $entityManager->getRepository(Salle::class)
->findOneByBatiment('D');
$ordi1->setSalle($salle);
$ordi2 = new Ordinateur;
$ordi2->setNumero(1001);
$ordi2->setIp('192.168.10.01');
$ordi2->setSalle($salle);
$logicielF = new Logiciel ;
$logicielF->setNom('Firefox');
$entityManager->persist($logicielF);
$logicielG = new Logiciel ;
$logicielG->setNom('Gimp2.2');
$entityManager->persist($logicielG);
$logicielF->addMachineInstallee($ordi1);
$logicielG->addMachineInstallee($ordi1);
$logicielF->addMachineInstallee($ordi2);
$ordi2->addLogicielInstalle($logicielG);
$entityManager->flush();
return new Response('<html><body></body></html>');
}
◦ tester http://localhost:8000/essai/test39
▪ regardez avec PhpMyAdmin :
Dans la table de jointure
logiciel_ordinateur, il y a
bien 4 lignes
◦ Enfin lisons des entités depuis la base de données :
public function test40() {
$em = $this->getDoctrine()->getManager();
$repo = $em->getRepository(Logiciel::class);
$logiciel = $repo->findOneByNom('Firefox');
$result="";
foreach ($logiciel->getMachineInstallees() as $ordi)
$result.=" - ".$ordi->getIp();
return new Response('<html><body>'.$result.'</body></html>');
}
▪ tester http://localhost:8000/essai/test40
- 192.168.10.00 - 192.168.10.01
• Pas de problème d'hydratation même avec une table de jointure
• dans les DBQueries du profiler, remarquez une requête avec un INNER
JOIN

Suppression d'une entité associée par un ManyToMany


◦ nouvelle fonction test43 :
on enlève l’ordinateur ayant l’ip '192.168.10.00' puis on affiche ensuite les ips
des ordinateur sur lesquels Firefox est installé.
public function test43() {
$em = $this->getDoctrine()->getManager();
$ordi = $em->getRepository(Ordinateur::class)
->findOneByIp('192.168.10.00');
$em->remove($ordi);
$em->flush();
$repo = $em->getRepository(Logiciel::class);
$logiciel = $repo->findOneByNom('Firefox');
$result="";
foreach ($logiciel->getMachineInstallees() as $ordi)
$result.=" - ".$ordi->getIp();
return new Response('<html><body>'.$result.'</body></html>');
}
◦ tester http://localhost:8000/essai/test43
- 192.168.10.01
▪ pas de PB ! Donc il y a un cascade delete
◦ Regardez soit le schéma de la BD avec PhpMyadmin, ou encore les dernières
migrations dans src/Migrations
...
$this->addSql('ALTER TABLE logiciel_ordinateur ADD CONSTRAINT FK_37263968CA84195D
FOREIGN KEY (logiciel_id) REFERENCES logiciel (id) ON DELETE CASCADE') ;
$this->addSql('ALTER TABLE logiciel_ordinateur ADD CONSTRAINT FK_37263968828317CA
FOREIGN KEY (ordinateur_id) REFERENCES ordinateur (id) ON DELETE CASCADE') ;
...

Join dans les QueryBuilder


• Reprenons le QueryBuilder qui aide à construire des Query sur les entités
◦ Enrichissons notre classe LogicielRepository.php :
class LogicielRepository extends EntityRepository {
public function findByOrdinateur($ip) {
return $this->createQueryBuilder('l')
->join('l.machine_installees', 'o', 'WITH', 'o.ip = :ip')
->setParameter('ip', $ip)
->getQuery()
->getResult();
}
}
▪ la fonction join est en fait un "innerJoin" :
join($join, $alias=null, $conditionType=null, $condition=null)
il n'y a pas de clause « ON », elle est déduite.

Si on ajoute des conditions de jointure, la conditionType est WITH et

l'alias celui qui nomme l'entité jointe.
◦ Nouvelle action test :
public function test41() {
$em = $this->getDoctrine()->getManager();
$ordi = new Ordinateur;
$ordi->setNumero(1000);
$ordi->setIp('192.168.10.00');
$repo = $em->getRepository(Logiciel::class);
$ordi->addLogicielInstalle($repo->findOneByNom('Firefox'));
$ordi->addLogicielInstalle($repo->findOneByNom('Gimp2.2'));
$em->persist($ordi);
$em->flush();
$logiciels = $repo->findByOrdinateur('192.168.10.00');
$result = ' ';
foreach ($logiciels as $logiciel)
$result .= $logiciel->getNom().' ';
return new Response('<html><body>'.$result.'</body></html>');
}
◦ tester http://localhost:8000/salle/essai/test41
Firefox Gimp2.2

◦ et un "leftJoin" :
Ajoutons-le dans LogicielRepository.php :
public function getLogicielsEtEventuellementOrdinateurs() {
return $this->createQueryBuilder('l')
->select('l.nom', 'o.ip')
->leftjoin('l.machine_installees', 'o')
->getQuery()
->getResult();
}
▪ on sélectionne les logiciels installés sur des machines ou pas
◦ nouvelle action test42 :
public function test42() {
$em = $this->getDoctrine()->getManager();
$logicielC = new Logiciel ;
$logicielC->setNom('Apache');
$em->persist($logicielC);
$em->flush();
$repom = $em->getRepository(Logiciel::class);
$logicielOrdis = $repom->getLogicielsEtEventuellementOrdinateurs();
$result = ' ';
foreach ($logicielOrdis as $logicielOrdi)
$result .= $logicielOrdi['nom'].' : '.$logicielOrdi['ip'].'<br />';
return new Response('<html><body>'.$result.'</body></html>');
}
◦ tester http://localhost:8000/salle/essai/test42
Firefox : 192.168.10.01
Firefox : 192.168.10.00
Gimp2.2 : 192.168.10.01
Gimp2.2 : 192.168.10.00
Apache :

Pensez à sauvegarder ! → chap4page17.zip


Exercices 4.4

• Ajouter une entité Plat avec son intitulé


◦ un resto proposera plusieurs plats
◦ un plat pourra figurer dans la carte de plusieurs restos
• Ajouter une page qui permet d'ajouter des nouveaux plats
• Modifier la page de modification des restos pour prendre en charge les plats

make:crud
• Générez un nouveau projet crudSalleTp
commencez par stopper l'actuel serveur
$ symfony server:stop
$ cd ..
$ symfony new --version=4.4 crudSalleTp
$ cd crudSalleTp
$ symfony server:start -d

• Remettre les configurations d'entités Salle et Ordinateur … simplifiées :


◦ créer un fichier Entity/Ordinateur.php :
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity(repositoryClass="App\Repository\OrdinateurRepository")
*/
class Ordinateur {

/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;

/**
* @ORM\Column(type="string", length=15)
*/
private $ip;

/**
* @ORM\Column(type="smallint", nullable=true)
*/
private $numero;

/**
* @ORM\ManyToOne(targetEntity="App\Entity\Salle", inversedBy="ordinateurs",
* cascade={"persist"})
*/
private $salle;

public function __toString() {


return $ip ;
}
}
◦ et le fichier Entity/Salle.php :
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\SalleRepository")
*/
class Salle {

/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;

/**
* @ORM\Column(type="string", length=1)
*/
private $batiment;

/**
* @ORM\Column(type="smallint")
*/
private $etage;

/**
* @ORM\Column(type="smallint")
*/
private $numero;

/**
* @ORM\OneToMany(targetEntity="App\Entity\Ordinateur", mappedBy="salle",
* cascade={"persist"})
*/
private $ordinateurs;

public function __toString() {


return $this->getBatiment().'-'.$this->getEtage().'.'
.$this->getNumero();
}
}
◦ Important : les fonction to String nécessaires pour l'affichage dans les
formulaires

• Installez plusieurs packages


dont le maker-bundle qui permet de créer des controlleurs, des entités, … le crud
$ composer require symfony/orm-pack symfony/form symfony/validator
symfony/security-csrf
$ composer require twig-bundle annotations
$ composer require --dev symfony/debug symfony/maker-bundle
◦ Créez un utilisateur «upjv2» de mot de passe «n37gOLYaahCTaUJ4$» et sa
base de données nommée «upjv2», accès par serveur local
▪ Renseignez le fichier général .env :
DATABASE_URL=mysql://upjv2:n37gOLYaahCTaUJ4$@127.0.0.1:3306/upjv2
▪ Mettez à jour les entités et la base de données :
$ symfony console make:entity --regenerate
Enter a class or namespace to regenerate [App\Entity]:
> App\Entity
Success !
$ symfony console make:migration
$ symfony console doctrine:migrations:migrate

Générez tout le code réalisant les opérations CRUD sur les 2 entités :
$ symfony console make:crud Salle

created: src/Controller/SalleController.php
created: src/Form/SalleType.php
created: templates/salle/_delete_form.html.twig
created: templates/salle/_form.html.twig
created: templates/salle/edit.html.twig
created: templates/salle/index.html.twig
created: templates/salle/new.html.twig
created: templates/salle/show.html.twig
Success !

$ symfony console make:crud Ordinateur

◦ Testez : http://localhost:8000/salle/
▪ Allez voir le code des contrôleurs,
templates, forms, ...

◦ Le code généré est assez brut et la


vue très dépouillée, mais c'est un
bon squelette à enrichir.
◦ Le routing est en annotation
◦ Attention, les ids des éléments ne devraient pas être affichés !
◦ Le delete utilise la protection Csrf ...

Pensez à sauvegarder ! → chap4page20.zip

Exercices 4.5

• Refaire un projet avec restos, chefs et plats … générés par le CRUD


◦ Améliorez ! Entre-autre, supprimer les ids !
◦ Ré-implémentez :
▪ une page d'accueil et un menu des différentes actions
▪ base.html.twig
▪ la page qui affiche les restos selon un nombre d'étoile fourni
▪ celle qui affiche les restos d'un même chef selon son nom
▪ page qui supprime un chef et tous ses restos s'il y a

Vous aimerez peut-être aussi