Dynamic embedded form with OneToOne relationship - php

I'm trying to create a form for an article with an addon (tender).
The article is in one-to-one relationship with the addon and the addon can be disabled (null).
If the checkbox with an addon is checked, the addon needs to be validated, then submitted together with the article.
The issue I'm having is, I'm unable to set the addon to null before the submission, what produces the errors - the form still tries to submit an addon tied to the article, while its values can't be empty.
How can I disable the addon when the checkbox is unchecked and still submit the article to the database, but without the addon?
I've tried using form events to modify the event's data, but it turned out that tender is not part of data during PRE_SUBMIT.
If I try to modify the data during SUBMIT, then I have to alter the logic in the methods of an entity, but it doesn't feel right and also causes some issues that I don't remember and couldn't work around.
I've also tried using 'empty_data' => null, thinking it would do exactly what I've wanted, but it turned out it has no effect on my form at all.
Article's entity:
<?php
namespace App\Entity;
use App\Repository\ContentsRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ContentsRepository::class)
*/
class Contents
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity=ContentsTenders::class, mappedBy="content", cascade={"persist", "remove"})
*/
private $tender;
public function getId(): ?int
{
return $this->id;
}
public function getPosition(): ?int
{
return $this->position;
}
public function getTender(): ?ContentsTenders
{
return $this->tender;
}
public function setTender(ContentsTenders $tender): self
{
$this->tender = $tender;
// set the owning side of the relation if necessary
if ($tender->getContent() !== $this) {
$tender->setContent($this);
}
return $this;
}
}
Addon's entity:
<?php
namespace App\Entity;
use App\Repository\ContentsTendersRepository;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ContentsTendersRepository::class)
*/
class ContentsTenders
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity=Contents::class, inversedBy="tender", cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=false)
*/
private $content;
public function getId(): ?int
{
return $this->id;
}
public function getContent(): ?Contents
{
return $this->content;
}
public function setContent(Contents $content): self
{
$this->content = $content;
return $this;
}
}
Article's form:
class ContentsType extends AbstractType
{
//public function __construct(EntityManagerInterface $em, UserInterface $user)
//{
// $this->em = $em;
// $this->user = $user;
//}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('isTender', CheckboxType::class, [
'mapped' => false,
'required' => false,
'label' => 'Przetarg',
])
->add('tender', NewTendersType::class, [
'required' => false,
'constraints' => array(new Valid()),
'empty_data' => null
])
->add('save', SubmitType::class, [
'label' => 'Zapisz',
'attr' => ['class' => 'save'],
])
;
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if(!$form['isTender']->getData()){
//
}
}
);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Contents::Class,
'accessible_ids' => [],
]);
}
}
#Edit
I've used:
Symfony Custom Field or Dynamic Form?
and added:
class NullableTenderTransformer implements DataTransformerInterface
{
public function transform($value)
{
return $value;
}
public function reverseTransform($data)
{
if($data->getTender()->getTenContracting() === null
&& $data->getTender()->getTenOrderFor() === null
&& $data->getTender()->getTenNumber() === null) {
$data->resetTender();
}
return $data;
}
}
combined with the answer below and $content->setTender(new ContentsTenders()); in the controller, +reset method in model, it works.
It might not be a perfect solution, though.

You can use a validation group
'constraints' => array(new Valid(['groups' => "withTender")
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Contents::Class,
'accessible_ids' => [],
'validation_groups' => function (FormInterface $form): array {
$data = $form->getData();
$groups = ['Default'];
if ($data->isTender) {
$groups[] = 'withTender';
}
return $groups;
}
]);
}

Related

Symfony 4 quiz style form, embedded collection types

I'm trying to display a quiz style form where end-user gets presented with questions that can have one or multiple correct answers.
Entities:
Exam Question
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Ramsey\Uuid\UuidInterface;
/**
* #ORM\Entity(repositoryClass="App\Repository\ExamQuestionRepository")
*/
class ExamQuestion
{
use TimestampableEntity;
/**
* #var UuidInterface
*
* #ORM\Id
* #ORM\Column(type="uuid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Module", inversedBy="questions")
* #ORM\JoinColumn(nullable=false)
*/
private $module;
/**
* #ORM\Column(type="text")
*/
private $text;
/**
* #ORM\OneToMany(targetEntity="App\Entity\ExamQuestionAnswer", mappedBy="question", orphanRemoval=true, cascade={"persist"})
*/
private $examQuestionAnswers;
/**
* #ORM\Column(type="boolean")
*/
private $hasMultipleAnswers;
public function __construct()
{
$this->examQuestionAnswers = new ArrayCollection();
}
public function __toString()
{
return $this->text;
}
public function getId(): ?UuidInterface
{
return $this->id;
}
public function getModule(): ?Module
{
return $this->module;
}
public function setModule(?Module $module): self
{
$this->module = $module;
return $this;
}
public function getText(): ?string
{
return $this->text;
}
public function setText(string $text): self
{
$this->text = $text;
return $this;
}
/**
* #return Collection|ExamQuestionAnswer[]
*/
public function getExamQuestionAnswers(): Collection
{
return $this->examQuestionAnswers;
}
public function addExamQuestionAnswer(ExamQuestionAnswer $examQuestionAnswer): self
{
if (!$this->examQuestionAnswers->contains($examQuestionAnswer)) {
$this->examQuestionAnswers[] = $examQuestionAnswer;
$examQuestionAnswer->setQuestion($this);
}
return $this;
}
public function removeExamQuestionAnswer(ExamQuestionAnswer $examQuestionAnswer): self
{
if ($this->examQuestionAnswers->contains($examQuestionAnswer)) {
$this->examQuestionAnswers->removeElement($examQuestionAnswer);
// set the owning side to null (unless already changed)
if ($examQuestionAnswer->getQuestion() === $this) {
$examQuestionAnswer->setQuestion(null);
}
}
return $this;
}
public function getHasMultipleAnswers(): ?bool
{
return $this->hasMultipleAnswers;
}
public function setHasMultipleAnswers(bool $hasMultipleAnswers): self
{
$this->hasMultipleAnswers = $hasMultipleAnswers;
return $this;
}
}
Exam Question Answer
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Ramsey\Uuid\UuidInterface;
/**
* #ORM\Entity(repositoryClass="App\Repository\ExamQuestionRepository")
*/
class ExamQuestionAnswer
{
use TimestampableEntity;
/**
* #var UuidInterface
*
* #ORM\Id
* #ORM\Column(type="uuid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\ExamQuestion", inversedBy="examQuestionAnswers")
* #ORM\JoinColumn(nullable=false)
*/
private $question;
/**
* #ORM\Column(type="boolean")
*/
private $isCorrect;
/**
* #ORM\Column(type="text")
*/
private $text;
/**
* #ORM\Column(type="boolean", nullable=true)
*/
private $selected;
public function __toString()
{
return $this->text;
}
public function getId(): ?UuidInterface
{
return $this->id;
}
public function getQuestion(): ExamQuestion
{
return $this->question;
}
public function setQuestion(ExamQuestion $question): self
{
$this->question = $question;
return $this;
}
public function getIsCorrect(): ?bool
{
return $this->isCorrect;
}
public function setIsCorrect(bool $isCorrect): self
{
$this->isCorrect = $isCorrect;
return $this;
}
public function getText(): ?string
{
return $this->text;
}
public function setText(string $text): self
{
$this->text = $text;
return $this;
}
public function getSelected(): ?bool
{
return $this->selected;
}
public function setSelected(?bool $selected): self
{
$this->selected = $selected;
return $this;
}
}
Exam Take
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Ramsey\Uuid\UuidInterface;
/**
* #ORM\Entity(repositoryClass="App\Repository\ExamTakeRepository")
*/
class ExamTake
{
use TimestampableEntity;
/**
* #var UuidInterface
*
* #ORM\Id
* #ORM\Column(type="uuid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\ExamQuestion")
*/
private $questions;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Module", inversedBy="examTakes")
* #ORM\JoinColumn(nullable=false)
*/
private $module;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Student", inversedBy="examTakes")
* #ORM\JoinColumn(nullable=false)
*/
private $student;
/**
* #ORM\Column(type="boolean", nullable=true)
*/
private $passed;
/**
* #ORM\OneToMany(targetEntity="App\Entity\ExamQuestionStudentAnswer", mappedBy="examTake", orphanRemoval=true)
*/
private $examQuestionStudentAnswers;
public function __construct()
{
$this->questions = new ArrayCollection();
$this->examQuestionStudentAnswers = new ArrayCollection();
}
public function getId(): ?UuidInterface
{
return $this->id;
}
/**
* #return Collection|ExamQuestion[]
*/
public function getQuestions(): Collection
{
return $this->questions;
}
public function addQuestion(ExamQuestion $question): self
{
if (!$this->questions->contains($question)) {
$this->questions[] = $question;
}
return $this;
}
public function removeQuestion(ExamQuestion $question): self
{
if ($this->questions->contains($question)) {
$this->questions->removeElement($question);
}
return $this;
}
public function getModule(): ?Module
{
return $this->module;
}
public function setModule(?Module $module): self
{
$this->module = $module;
return $this;
}
public function getStudent(): ?Student
{
return $this->student;
}
public function setStudent(?Student $student): self
{
$this->student = $student;
return $this;
}
public function getPassed(): ?bool
{
return $this->passed;
}
public function setPassed(?bool $passed): self
{
$this->passed = $passed;
return $this;
}
/**
* #return Collection|ExamQuestionStudentAnswer[]
*/
public function getExamQuestionStudentAnswers(): Collection
{
return $this->examQuestionStudentAnswers;
}
public function addExamQuestionStudentAnswer(ExamQuestionStudentAnswer $examQuestionStudentAnswer): self
{
if (!$this->examQuestionStudentAnswers->contains($examQuestionStudentAnswer)) {
$this->examQuestionStudentAnswers[] = $examQuestionStudentAnswer;
$examQuestionStudentAnswer->setExamTake($this);
}
return $this;
}
public function removeExamQuestionStudentAnswer(ExamQuestionStudentAnswer $examQuestionStudentAnswer): self
{
if ($this->examQuestionStudentAnswers->contains($examQuestionStudentAnswer)) {
$this->examQuestionStudentAnswers->removeElement($examQuestionStudentAnswer);
// set the owning side to null (unless already changed)
if ($examQuestionStudentAnswer->getExamTake() === $this) {
$examQuestionStudentAnswer->setExamTake(null);
}
}
return $this;
}
}
And the forms:
class ExamTakeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('questions', CollectionType::class, [
'entry_type' => ExamQuestionType::class,
'allow_add' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ExamTake::class,
]);
}
}
class ExamQuestionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('text', TextType::class, [
'attr' => ['readonly' => true],
])
->add('examQuestionAnswers', CollectionType::class, [
'entry_type' => ExamQuestionAnswerType::class,
'allow_add' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ExamQuestion::class,
]);
}
}
class ExamQuestionAnswerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('text');
$builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) {
$form = $event->getForm();
/** #var ExamQuestion $question */
$question = $event->getData()->getQuestion();
if ($question->getHasMultipleAnswers()) {
$form
->add('select', ChoiceType::class, [
'expanded' => true,
'multiple' => true,
'mapped' => false,
]);
} else {
$form
->add('select', ChoiceType::class, [
'expanded' => true,
'multiple' => false,
'mapped' => false,
]);
}
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ExamQuestionAnswer::class,
]);
}
}
It seems i can't get the examQuestionAnswers field in collection displayed correctly. What i get is a collection of unrelated fields(inputs) if question has only one answer, and if question has multiple answers checkboxes are not shown. Any help is much appreciated!
The ChoiceType expects the choices attribute"
if ($question->getHasMultipleAnswers()) {
$form
->add('select', ChoiceType::class, [
'choices' => $question->getExamQuestionAnswers(),
'expanded' => true,
'multiple' => true,
'mapped' => false,
]);
} else {
$form
->add('select', ChoiceType::class, [
'choices' => $question->getExamQuestionAnswers(),
'expanded' => true,
'multiple' => false,
'mapped' => false,
]);
}
This will result in putting $examQuestionAnswers as choices to your ChoiceType. In case you get an exception change the type to EntityType(it inherits from ChoiceType) so choices should still work the same. You might also needed to implement either a __toString() method in your ExamQuestionAnswer entity or define choice_label attribute this should solve your problem in case it doesn't add a comment about what does not work and I'll implement it myself

Form issue when trying to create a checkbox with CollectionType calling to another FromType (cross entities situation)

This case is a case study, I'm trying to resolve this issue in order to explain how to organize entities and create forms to my students.
I have this singular relation between 3 of my entities :
Protagonist <--(OneToMany)--> EventRegistration <--(ManyToOne)--> Event
Which could not be transformed as a many to many relation because there are some columns inside the EventRegistration table :
Protagonist :
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProtagonistRepository")
*/
class Protagonist
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="string", length=100, nullable=true)
*/
private $japaneseName;
/**
* #ORM\Column(type="text")
*/
private $description;
/**
* #ORM\Column(type="string", length=80, nullable=true)
*/
private $picture;
/**
* #ORM\Column(type="string", length=80, nullable=true)
*/
private $background;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $updated_at;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="protagonists")
* #ORM\JoinColumn(nullable=false)
*/
private $category;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Tag", mappedBy="protagonists")
*/
private $tags;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Registration", mappedBy="protagonist")
*/
private $registrations;
/**
* #ORM\Column(type="boolean", nullable=true)
*/
private $isAlive;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Event", mappedBy="protagonists")
*/
private $events;
/**
* #ORM\OneToMany(targetEntity="App\Entity\EventRegistration", mappedBy="protagonist")
*/
private $eventRegistrations;
public function __construct()
{
$this->tags = new ArrayCollection();
$this->registrations = new ArrayCollection();
$this->events = new ArrayCollection();
$this->eventRegistrations = new ArrayCollection();
}
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 getJapaneseName(): ?string
{
return $this->japaneseName;
}
public function setJapaneseName(?string $japaneseName): self
{
$this->japaneseName = $japaneseName;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
public function getPicture(): ?string
{
return $this->picture;
}
public function setPicture(?string $picture): self
{
$this->picture = $picture;
return $this;
}
public function getBackground(): ?string
{
return $this->background;
}
public function setBackground(?string $background): self
{
$this->background = $background;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updated_at;
}
public function setUpdatedAt(?\DateTimeInterface $updated_at): self
{
$this->updated_at = $updated_at;
return $this;
}
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
/**
* #return Collection|Tag[]
*/
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(Tag $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags[] = $tag;
$tag->addProtagonist($this);
}
return $this;
}
public function removeTag(Tag $tag): self
{
if ($this->tags->contains($tag)) {
$this->tags->removeElement($tag);
$tag->removeProtagonist($this);
}
return $this;
}
/**
* #return Collection|Registration[]
*/
public function getRegistrations(): Collection
{
return $this->registrations;
}
public function addRegistration(Registration $registration): self
{
if (!$this->registrations->contains($registration)) {
$this->registrations[] = $registration;
$registration->setProtagonist($this);
}
return $this;
}
public function removeRegistration(Registration $registration): self
{
if ($this->registrations->contains($registration)) {
$this->registrations->removeElement($registration);
// set the owning side to null (unless already changed)
if ($registration->getProtagonist() === $this) {
$registration->setProtagonist(null);
}
}
return $this;
}
public function getIsAlive(): ?bool
{
return $this->isAlive;
}
public function setIsAlive(?bool $isAlive): self
{
$this->isAlive = $isAlive;
return $this;
}
/**
* #return Collection|Event[]
*/
public function getEvents(): Collection
{
return $this->events;
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events[] = $event;
$event->addProtagonist($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->contains($event)) {
$this->events->removeElement($event);
$event->removeProtagonist($this);
}
return $this;
}
/**
* #return Collection|EventRegistration[]
*/
public function getEventRegistrations(): Collection
{
return $this->eventRegistrations;
}
public function addEventRegistration(EventRegistration $eventRegistration): self
{
if (!$this->eventRegistrations->contains($eventRegistration)) {
$this->eventRegistrations[] = $eventRegistration;
$eventRegistration->setProtagonist($this);
}
return $this;
}
public function removeEventRegistration(EventRegistration $eventRegistration): self
{
if ($this->eventRegistrations->contains($eventRegistration)) {
$this->eventRegistrations->removeElement($eventRegistration);
// set the owning side to null (unless already changed)
if ($eventRegistration->getProtagonist() === $this) {
$eventRegistration->setProtagonist(null);
}
}
return $this;
}
}
EventRegistration :
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\EventRegistrationRepository")
*/
class EventRegistration
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetimetz")
*/
private $registrationDate;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Protagonist", inversedBy="eventRegistrations")
*/
private $protagonist;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Event", inversedBy="eventRegistrations")
*/
private $event;
public function getId(): ?int
{
return $this->id;
}
public function getRegistrationDate(): ?\DateTimeInterface
{
return $this->registrationDate;
}
public function setRegistrationDate(\DateTimeInterface $registrationDate): self
{
$this->registrationDate = $registrationDate;
return $this;
}
public function getProtagonist(): ?Protagonist
{
return $this->protagonist;
}
public function setProtagonist(?Protagonist $protagonist): self
{
$this->protagonist = $protagonist;
return $this;
}
public function getEvent(): ?Event
{
return $this->event;
}
public function setEvent(?Event $event): self
{
$this->event = $event;
return $this;
}
}
Event :
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\EventRepository")
*/
class Event
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="datetimetz", nullable=true)
*/
private $start_date;
/**
* #ORM\Column(type="datetimetz", nullable=true)
*/
private $end_date;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Protagonist", inversedBy="events")
*/
private $protagonists;
/**
* #ORM\OneToMany(targetEntity="App\Entity\EventRegistration", mappedBy="event")
*/
private $eventRegistrations;
public function __construct()
{
$this->protagonists = new ArrayCollection();
$this->eventRegistrations = new ArrayCollection();
}
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 getStartDate(): ?\DateTimeInterface
{
return $this->start_date;
}
public function setStartDate(?\DateTimeInterface $start_date): self
{
$this->start_date = $start_date;
return $this;
}
public function getEndDate(): ?\DateTimeInterface
{
return $this->end_date;
}
public function setEndDate(?\DateTimeInterface $end_date): self
{
$this->end_date = $end_date;
return $this;
}
/**
* #return Collection|Protagonist[]
*/
public function getProtagonists(): Collection
{
return $this->protagonists;
}
public function addProtagonist(Protagonist $protagonist): self
{
if (!$this->protagonists->contains($protagonist)) {
$this->protagonists[] = $protagonist;
}
return $this;
}
public function removeProtagonist(Protagonist $protagonist): self
{
if ($this->protagonists->contains($protagonist)) {
$this->protagonists->removeElement($protagonist);
}
return $this;
}
/**
* #return Collection|EventRegistration[]
*/
public function getEventRegistrations(): Collection
{
return $this->eventRegistrations;
}
public function addEventRegistration(EventRegistration $eventRegistration): self
{
if (!$this->eventRegistrations->contains($eventRegistration)) {
$this->eventRegistrations[] = $eventRegistration;
$eventRegistration->setEvent($this);
}
return $this;
}
public function removeEventRegistration(EventRegistration $eventRegistration): self
{
if ($this->eventRegistrations->contains($eventRegistration)) {
$this->eventRegistrations->removeElement($eventRegistration);
// set the owning side to null (unless already changed)
if ($eventRegistration->getEvent() === $this) {
$eventRegistration->setEvent(null);
}
}
return $this;
}
}
I can access a collection of eventRegistrations with my Protagonist and Event entity, and I can access the protagonist and event with my EventRegistration entity.
The issue crops up when I try to create a checkbox with all the events available for the protagonist : I don't have any attribute that allows me to make a collection of those events :
ProtagonistType
<?php
namespace App\Form;
use App\Entity\Category;
use App\Entity\Event;
use App\Entity\EventRegistration;
use App\Entity\Protagonist;
use App\Entity\Tag;
use App\Repository\EventRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class ProtagonistType
* #package App\Form
*/
class ProtagonistType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
->add('japaneseName', TextType::class, [
'required' => false
])
->add('description', TextareaType::class)
->add('picture', TextType::class, [
'required' => false
])
->add('background', TextType::class, [
'required' => false
])
->add('isAlive', CheckboxType::class)
->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'title',
'expanded' => true,
'multiple' => false
])
->add('tags', EntityType::class, [
'class' => Tag::class,
'choice_label' => 'name',
'expanded' => true,
'multiple' => true,
'by_reference' => false,
])
**->add('eventRegistrations', CollectionType::class, [
'entry_type' => EventRegistrationType::class
])**
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Protagonist::class,
]);
}
}
EventRegistrationType :
<?php
namespace App\Form;
use App\Entity\Event;
use App\Entity\EventRegistration;
use App\Repository\EventRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class EventRegistrationType
* #package App\Form
*/
class EventRegistrationType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('event', EntityType::class, [
'class' => Event::class,
'choice_label' => 'name',
'multiple' => true,
'expanded' => true,
'by_reference' => false,
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => EventRegistration::class,
]);
}
}
The only effective solution I found is to create a ManyToMany relation between Protagonist and Event, then set another Registration table which is on a ManyToOne relation with Protagonist in order to get the protagonists registrations.
Still I'd like to make this many to many relation with extra fields works, I'm all ears for any solution you'd get to solve this issue.
Thank you!
Some theory
Entities are your models, business objects that have identities, data, behavior.
They are the heart, building blocks of your business model.
When we design entities - in the first place we should consider them as objects, that have their own shape and responsibilities instead of them being just containers for data stored in a database. Also, we should care about proper relations between entities.
Ideally, entities should always be valid. If so - they can be persisted at any time.
Persistence is a separate concern.
In general case, it's even not necessary for entities to be persisted into a database. They could just be persisted in memory, file system, key-value storage etc.
Forms is also a separate concern that is closer to working with user interfaces.
Forms help us to render user interface, translate requests from users to some structures of known shape that is easier to work with, than with raw data from requests, and validate this submitted data.
These structures are just containers for data retrieved from requests, they should not have any behavior.
These structures can be invalid at certain times.
What about the issue described?
So making entities playing roles of these forms underlying data structures might be not the best idea ever.
It's just clearly mixing of concerns and rigid coupling between different layers.
That is why you're having these issues.
So instead of using EventRegistration class as a data_class for EventRegistrationType and Protagonist - for ProtagonistType - consider creating separate data structures. Propagate submitted data to entities only when it's successfully validated.
Some useful links to read more on the topic (though the author calls underlying structures - commands somewhy):
Decoupling (Symfony2) Forms from Entities
Form, Command, and Model Validation
Other useful topics:
The Clean Architecture
Domain-Driven Design in PHP

The class 'App\Form\Database_InteractionType' was not found in the chain configured namespaces App\Entity

Hello i can't find the error when i try to delete something out of my database.
It says that my namespace is wrong but i really cant find any issues.
Here is the action im calling
/**
* #Route("/delete/{id}", name="delete")
* #param $id
* #return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function deleteById($id)
{
$em = $this->getDoctrine()->getManager();
$entries = $em->getRepository(Database_InteractionType::class)->find($id);
$em->remove($entries);
$em->flush();
return $this->redirectToRoute('show');
}
Here is Entitiy and my Form.
I have been looking at it for 2 hours straight and i cant seem to find the Iusse..
Entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\Database_InteractionRepository")
*/
class Database_Interaction
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $question;
/**
* #ORM\Column(type="string", length=255)
*/
private $answer;
public function getId(): ?int
{
return $this->id;
}
public function getQuestion(): ?string
{
return $this->question;
}
public function setQuestion(string $question): self
{
$this->question = $question;
return $this;
}
public function getAnswer(): ?string
{
return $this->answer;
}
public function setAnswer(string $answer): self
{
$this->answer = $answer;
return $this;
}
}
My Form:
<?php
namespace App\Form;
use App\Entity\Database_Interaction;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class Database_InteractionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'question',
TextType::class,
[
'attr' => [
'placeholder' => 'Enter your Question',
'class' => 'form-control'
],
'required' => true
]
)
->add(
'answer',
TextType::class,
[
'required' => true,
'attr' => [
'class' => 'form-control',
'placeholder' => 'Enter your Answer',
]
]
)
->add(
'save',
SubmitType::class,
[
'attr' => [
'class' => 'btn btn-primary'
]
]
)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Database_Interaction::class,
]);
}
}
And my Repository if this is to any help..
<?php
namespace App\Repository;
use App\Entity\Database_Interaction;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
* #method Database_Interaction|null find($id, $lockMode = null, $lockVersion = null)
* #method Database_Interaction|null findOneBy(array $criteria, array $orderBy = null)
* #method Database_Interaction[] findAll()
* #method Database_Interaction[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class Database_InteractionRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Database_Interaction::class);
}
/**
* #param string $id
* #return Database_Interaction|null
*/
public function findById(string $id)
{
return $this->findOneBy(
['id' => $id]
);
}
// /**
// * #return SubmitNew[] Returns an array of SubmitNew objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('s')
->andWhere('s.exampleField = :val')
->setParameter('val', $value)
->orderBy('s.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?SubmitNew
{
return $this->createQueryBuilder('s')
->andWhere('s.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
If anyone could help me out I don't know where to look anymore...
Thank you
Look closely at this line:
$entries = $em->getRepository(Database_InteractionType::class)->find($id);
This refers to the form type class(App\Form\Database_InteractionType) not your persisted entity class, which lives in App\Entity namespace (the namespace it is looking in), this is why the error is produced. Forms do not have a repository, the related entity does.
Try this:
$entries = $em->getRepository(Database_Interaction::class)->find($id);

Symfony2 Many to Many relation update

I have a many to many relation that is actually made of two many to ones on a third entity.
Domain ← one-to-many → DomainTag ← many-to-one → Tag
I'm building a Rest API, and I want to add domains to a Tag via a specific route. Everything works fine when the tags has no association.
But when I try to add new domains to a tag, there is an error if the tag already as one (or more) domain(s).
The error is
The form's view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class AppBundle\Entity\Domain. You can avoid this error by setting the "data_class" option to "AppBundle\Entity\Domain" or by adding a view transformer that transforms an instance of class AppBundle\Entity\Domain to scalar, array or an instance of \ArrayAccess.
Here are the three entities :
Tag
class Tag
{
…
/**
* #ORM\OneToMany(targetEntity="DomainTag", mappedBy="tag", cascade={"persist", "remove"})
*/
private $domainTags;
…
}
DomainTag
class DomainTag
{
…
/**
* #var Tag
*
* #ORM\ManyToOne(targetEntity="Tag", inversedBy="domainTags")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="tag_id", referencedColumnName="id")
* })
*/
private $tag;
/**
* #var Domain
*
* #ORM\ManyToOne(targetEntity="Domain", inversedBy="domainTags")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="domain_id", referencedColumnName="id")
* })
*/
private $domain;
…
}
Domain
class Domain
{
…
/**
* #ORM\OneToMany(targetEntity="DomainTag", mappedBy="domain", cascade={"persist", "remove"})
*/
private $domainTags;
…
}
Here is the form
class AddDomainTagFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('domainTags', 'collection', array(
'type' => new DomainTagFormType(),
'allow_add' => true,
))
;
}
// BC for SF < 2.7
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Tag',
'csrf_protection' => false,
));
}
public function getName()
{
return 'api_set_domaintag';
}
}
DomainTagFormType
class DomainTagFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('weight')
->add('domain', 'entity', array(
'class' => 'AppBundle:Domain',
'property' => 'id',
'multiple' => false,
'expanded' => true
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\DomainTag',
'csrf_protection' => false,
));
}
// BC for SF < 2.7
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
public function getName()
{
return 'api_domaintag';
}
}
And finally, the controller
/**
* #Rest\Put("/{id}/domains", requirements={"id" = "\d+"})
* #Security("has_role('ROLE_SUPER_ADMIN')")
* #Rest\View
*/
public function setDomainAction(Request $request, $id)
{
$tagManager = $this->get('app.manager.tag');
$domainManager = $this->get('app.manager.domain');
$tag = $tagManager->find($id);
if (!$tag) {
return new Response('Tag not found', 404);
}
$form = $this->createForm(new AddDomainTagFormType(), $tag, array('method' => 'PUT'));
$form->handleRequest($request);
if ($form->isValid()) {
foreach ($tag->getDomainTags() as $dt) {
$dt->setTag($tag);
$tagManager->persist($dt);
}
$tagManager->flush($tag);
return new Response('', 204);
} else {
return $this->view($form, 400);
}
}
Do I have to remove all DomainTags from the Tag and the recreate all associations ? Or is there a way to just add/remove DomainTags ?
Thank you

Form edition for a N:M relationship with extra fields is not working

I'm working on a form that handle N:M relationship with an extra parameter (extra field/column). This is what I've done until now:
In OrdersType.php form:
class OrdersType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder fields
// $builder fields only need on edit form not in create
if ($options['curr_action'] !== NULL)
{
$builder
// other $builder fields
->add("orderProducts", "collection", array(
'type' => new OrdersHasProductType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
));
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\Orders',
'render_fieldset' => FALSE,
'show_legend' => FALSE,
'intention' => 'orders_form',
'curr_action' => NULL
));
}
public function getName()
{
return 'orders';
}
}
In OrderHasProductType.php:
class OrdersHasProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', 'text', array(
'required' => FALSE,
'label' => FALSE
))
->add('amount', 'text', array(
'required' => TRUE,
'label' => FALSE
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\OrderHasProduct',
'intention' => 'order_has_product'
));
}
public function getName()
{
return 'order_has_product';
}
}
And finally this is Orders.php and OrdersHasProduct.php entities:
class Orders {
use IdentifiedAutogeneratedEntityTrait;
// rest of fields for the entity
/**
* #ORM\OneToMany(targetEntity="OrderHasProduct", mappedBy="order", cascade={"all"})
*/
protected $orderProducts;
protected $products;
/**
* #ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
protected $deletedAt;
public function __construct()
{
$this->orderProducts = new ArrayCollection();
$this->products = new ArrayCollection();
}
public function getOrderProducts()
{
return $this->orderProducts;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
public function getProduct()
{
$products = new ArrayCollection();
foreach ($this->orderProducts as $op)
{
$products[] = $op->getProduct();
}
return $products;
}
public function setProduct($products)
{
foreach ($products as $p)
{
$ohp = new OrderHasProduct();
$ohp->setOrder($this);
$ohp->setProduct($p);
$this->addPo($ohp);
}
}
public function getOrder()
{
return $this;
}
public function addPo($ProductOrder)
{
$this->orderProducts[] = $ProductOrder;
}
public function removePo($ProductOrder)
{
return $this->orderProducts->removeElement($ProductOrder);
}
}
/**
* #ORM\Entity
* #ORM\Table(name="order_has_product")
* #Gedmo\SoftDeleteable(fieldName="deletedAt")
* #UniqueEntity(fields={"order", "product"})
*/
class OrderHasProduct {
use IdentifiedAutogeneratedEntityTrait;
/**
* Hook timestampable behavior
* updates createdAt, updatedAt fields
*/
use TimestampableEntity;
/**
* #ORM\ManyToOne(targetEntity="\Tanane\FrontendBundle\Entity\Orders", inversedBy="orderProducts")
* #ORM\JoinColumn(name="general_orders_id", referencedColumnName="id")
*/
protected $order;
/**
* #ORM\ManyToOne(targetEntity="\Tanane\ProductBundle\Entity\Product", inversedBy="orderProducts")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
protected $product;
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected $amount;
/**
* #ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
protected $deletedAt;
public function setOrder(\Tanane\FrontendBundle\Entity\Orders $order)
{
$this->order = $order;
}
public function getOrder()
{
return $this->order;
}
public function setProduct(\Tanane\ProductBundle\Entity\Product $product)
{
$this->product = $product;
}
public function getProduct()
{
return $this->product;
}
public function setAmount($amount)
{
$this->amount = $amount;
}
public function getAmount()
{
return $this->amount;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
}
But when I try to edit a order with this code in my controller:
public function editAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('FrontendBundle:Orders')->find($id);
$type = $order->getPerson()->getPersonType() === 1 ? "natural" : "legal";
$params = explode('::', $request->attributes->get('_controller'));
$actionName = substr($params[1], 0, -6);
$orderForm = $this->createForm(new OrdersType(), $order, array('action' => '#', 'method' => 'POST', 'register_type' => $type, 'curr_action' => $actionName));
return array(
"form" => $orderForm->createView(),
'id' => $id,
'entity' => $order
);
}
I get this error:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
Proxies__CG__\Tanane\ProductBundle\Entity\Product. You can avoid this
error by setting the "data_class" option to
"Proxies__CG__\Tanane\ProductBundle\Entity\Product" or by adding a
view transformer that transforms an instance of class
Proxies__CG__\Tanane\ProductBundle\Entity\Product to scalar, array or
an instance of \ArrayAccess.
And I don't find or know how to fix it, can any give me some help?
I think this is happening because this code
class OrdersHasProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', 'text', array(
'required' => FALSE,
'label' => FALSE
))
));
}
//...
}
means that Symfony is expecting "product" to be a field of type "text", but when it calls getProduct() on the OrderHasProduct it gets a Product object (or a Doctrine Proxy to a Product, since it's not been loaded at that point). Symfony Fields inherit from Form/AbstractType, so they're essentially Forms in their own right, with just one field, hence the error message.
The solution is either to make that field of type "entity", or to create a different method which only gives the name of the Product, e.g. getProductName() on OrderHasProduct, and then use that as the data behind the field.

Categories