Bonjour à tous !
Pour un petit projet personnel, je suis en train de créer un site web pour des dégustations de whisky. J'ai décidé d'utiliser Symfony pour son coté "boite à outil complète, il reste plus qu'à construire toi la maison !".
J'ai suivi quelques tuto, des vidéos, etc. mais j'arrive à un problème que mes connaissances n'arrivent pas à résoudre. C'est certainement plus le concept général qu'une erreur de code, donc je m'excuse d'avance pour ça !
J'ai une relation Many to Many bidirectionnelle avec attributs. Dégustation -> DegustationWhiskys -> Whisky. Logiquement, une dégustation a plusieurs whisky et les whiskys peuvent être présent dans plusieurs dégustation. L'entité du milieu me permet de définir l'ordre de dégustation, et éventuellement un petit commentaire.
Mon entité Degustation :
Mon entité DegustationWhisky :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242 <?php namespace App\Entity; use App\Entity\DW; use App\Entity\DWU; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\HttpFoundation\File\File; use Vich\UploaderBundle\Mapping\Annotation as Vich; /** * @ORM\Entity(repositoryClass="App\Repository\DegustationRepository") * @ORM\HasLifecycleCallbacks() * @Vich\Uploadable */ class Degustation { /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string", length=255) */ private $Name; /** * @ORM\Column(type="string", length=255) */ private $Location; /** * @ORM\Column(type="date", nullable=true) */ private $Date; /** * @ORM\Column(type="text", nullable=true) */ private $ShortDescription; /** * @Vich\UploadableField(mapping="degustations", fileNameProperty="imageName") * @var File */ private $imageFile; /** * @ORM\Column(type="string", length=255, nullable=true) */ private $imageName; /** * @ORM\OneToMany(targetEntity="App\Entity\DW", mappedBy="degustation", cascade={"persist"}) */ private $DWs; /** * @ORM\Column(type="datetime", nullable=true) */ private $UpdatedAt; /** * @ORM\Column(type="datetime") */ private $CreatedAt; /** * @ORM\Column(type="string", length=255) */ private $CreatedBy; public function getId(): ?int { return $this->id; } public function getName(): ?string { return $this->Name; } public function setName(?string $Name): self { $this->Name = $Name; return $this; } public function getLocation(): ?string { return $this->Location; } public function setLocation(?string $Location): self { $this->Location = $Location; return $this; } public function getDate(): ?\DateTimeInterface { return $this->Date; } public function setDate(?\DateTimeInterface $Date): self { $this->Date = $Date; return $this; } public function getShortDescription(): ?string { return $this->ShortDescription; } public function setShortDescription(?string $ShortDescription): self { $this->ShortDescription = $ShortDescription; return $this; } public function getImageName(): ?string { return $this->imageName; } public function setImageName(?string $imageName): self { $this->imageName = $imageName; return $this; } /** * * @param File|\Symfony\Component\HttpFoundation\File\UploadedFile $imageFile */ public function setImageFile(?File $imageFile = null): void { $this->imageFile = $imageFile; if (null !== $imageFile) { $this->updatedAt = new \DateTimeImmutable(); } } public function getImageFile(): ?File { return $this->imageFile; } public function __construct() { $this->DWs = new ArrayCollection(); } /** * @return Collection|DW[] */ public function getDWs(): Collection { return $this->DWs; } public function addDW(DW $dW): self { if (!$this->DWs->contains($dW)) { $this->DWs[] = $dW; $dW->setDegustation($this); } return $this; } public function removeDW(DW $dW): self { if ($this->DWs->contains($dW)) { $this->DWs->removeElement($dW); // set the owning side to null (unless already changed) if ($dW->getDegustation() === $this) { $dW->setDegustation(null); } } return $this; } public function getUpdatedAt(): ?\DateTimeInterface { return $this->UpdatedAt; } public function setUpdatedAt(?\DateTimeInterface $UpdatedAt): self { $this->UpdatedAt = $UpdatedAt; return $this; } public function getCreatedAt(): ?\DateTimeInterface { return $this->CreatedAt; } public function setCreatedAt(\DateTimeInterface $CreatedAt): self { $this->CreatedAt = $CreatedAt; return $this; } public function getCreatedBy(): ?string { return $this->CreatedBy; } public function setCreatedBy(string $CreatedBy): self { $this->CreatedBy = $CreatedBy; return $this; } /** * @ORM\PreUpdate */ public function updateDate() { $this->setUpdatedAt(new \Datetime()); } }
Toujours logiquement, l'entité DegustationWhisky est la propriétaire des deux relations, vu qu'elle est en sandwich dans une One-to-many-to-One. Jusqu'à la pas de problème.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99 <?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="App\Repository\DWRepository") */ class DW { /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="integer") */ private $placeOrder; /** * @ORM\Column(type="text", nullable=true) */ private $description; /** * @ORM\ManyToOne(targetEntity="App\Entity\Whisky") * @ORM\JoinColumn(nullable=false) */ private $Whisky; /** * @ORM\ManyToOne(targetEntity="App\Entity\Degustation", inversedBy="DWs", cascade={"persist"}) * @ORM\JoinColumn(nullable=false) */ private $degustation; public function getId(): ?int { return $this->id; } public function getPlaceOrder(): ?int { return $this->placeOrder; } public function setPlaceOrder(int $placeOrder): self { $this->placeOrder = $placeOrder; return $this; } public function getDescription(): ?string { return $this->description; } public function setDescription(?string $description): self { $this->description = $description; return $this; } public function getDegustation(): ?Degustation { return $this->Degustation; } public function setDegustation(?Degustation $Degustation): self { $this->Degustation = $Degustation; return $this; } public function getWhisky(): ?Whisky { return $this->Whisky; } public function setWhisky(?Whisky $Whisky): self { $this->Whisky = $Whisky; return $this; } public function __toString() { return $this->Whisky->getName(); } }
Comme le nombre de whisky par dégustation est fixe, j'instancie directement mes entités du milieu lors de sa création.
J'instancie mon entité $degustation, renseigne les champs "créer par" et "créer à", puis une petite boucle for pour créer 5 entités DegustationWhisky, habilement raccourci par le nom DW.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 public function new(Request $request): Response { $degustation = new Degustation(); $degustation->setCreatedAt(new \Datetime()); $degustation->setCreatedBy($this->getUser()->getFullName()); for ($x=0;$x<=4;$x++){ $dw = new DW; $dw->setPlaceOrder($x+1); $degustation->addDW($dw); } $form = $this->createForm(DegustationType::class, $degustation); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $entityManager = $this->getDoctrine()->getManager(); $entityManager->persist($degustation); $entityManager->flush(); return $this->redirectToRoute('degustation_index'); }
Mon formulaire, générer avec le bundle maker, est comme suit :
La aussi, rien de magique me semble-t-il, J'utilise le collectionType pour mes 5 entités DegustationWhisky (DWs). Selon les tuto, je renseigne le champ 'by_reference' à false afin que cela soit mis à jours comme il se doit.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 public function buildForm(FormBuilderInterface $builder, array $options) { /* This form is the base form to hydrate Degustation. The 5 DWs is fixed and is created directly in the class when its instancied. */ $builder ->add('Name') ->add('Location') ->add('Date') /* The image is persisted through the use of Vich Bundle */ ->add('imageFile',VichImageType::class,[ 'required'=>false, 'allow_delete'=>true, 'download_uri'=>true, 'image_uri'=>true, ]) /* the decription of each degustation is a HTML text in the DB, using CKEditor */ ->add('ShortDescription',CKEditorType::class,array( 'config'=>array( 'uiColor'=>'#ffffff' ), )) /* Here we add the user that will attend to the degustation. This field is not mapped, it will be used in the base degustation class to create DWUs */ ->add('user',EntityType::class,[ 'class'=>User::class, 'label'=>false, 'choice_label'=>'fullName', 'multiple'=>true, 'mapped'=>false ]) /* Each degustation has a fixed number of DW (5) and it's set in the class */ ->add('DWs',CollectionType::class,[ 'entry_type'=>DWType::class, 'by_reference'=>false, 'prototype'=>true, ]) ; }
Je créé ma vue sans problème, les 5 DW sont la, avec les bons champs. Je renseigne tout selon ce que j'ai besoin, et au moment de cliquer sur "Save", j'ai l'erreur suivante qui apparaît :
L'erreur est cristalline : je tente de persister un champ qui est marqué comme obligatoire, refus net et rollback. Le champ obligatoire qui est vide est l'ID de ma dégustation. L'outil de débug me montre que le "INSERT INTO" de ma dégustation est bien avant celui de DegustationWhisky, ce qui fait du sens: il faut la persister en premier pour générer un ID. Mais comment l'ajouter à mes DegustationWhisky ?An exception occurred while executing 'INSERT INTO dw (place_order, description, whisky_id, degustation_id) VALUES (?, ?, ?, ?)' with params [1, null, 1, null]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Le champ 'degustation_id' ne peut être vide (null)
Or mon problème est là. Je supposais que le cascade:persist dans mon entité prendrait soin de ce genre de détail. Je pourrais sans problème faire un gros détour pour contourner ce problème (par exemple récupérer l'ID de la dégustation après le ->flush()), mais cela me semble tellement peu élégant que je suis persuadé qu'il doit y avoir un moyen de faire ça directement, surtout que tout le monde dit de mettre le moins de code dans son contrôleur. Help !
Merci d'avance et désolé du long message !
Partager