I'm trying to get a multi-relation (multiple, dependent one-to-many relations) form working, but no success. I'm using Symfony 2.3 with FOSUserbundle.
Entity User
use FOS\UserBundle\Entity\User as BaseUser;
[...]
/**
* #ORM\Entity
* #Gedmo\Loggable
* #ORM\Table(name="ta_user", indexes={#ORM\Index(name="IDX_LOGIN_TOKEN", columns={"login_token"})})
*/
class User extends BaseUser
{
[...]
/**
* #ORM\OneToMany(targetEntity="UserLifestyle", mappedBy="user", fetch="LAZY", cascade={"persist", "remove"})
*/
protected $lifestyle;
UserManager
use Doctrine\ORM\EntityManager;
use FOS\UserBundle\Entity\UserManager as BaseUserManager;
use Acme\UserBundle\Entity\LifestyleQuestion;
use Acme\UserBundle\Entity\UserLifestyle;
[...]
class UserManager extends BaseUserManager {
public function createUser() {
$user = parent::createUser();
$lifestyle = new UserLifestyle();
$lifestyle->setQuestion($this->objectManager->getReference('Acme\UserBundle\Entity\LifestyleQuestion', 1));
$user->addLifeStyle($lifestyle);
$lifestyle = new UserLifestyle();
$lifestyle->setQuestion($this->objectManager->getReference('Acme\UserBundle\Entity\LifestyleQuestion', 2));
$user->addLifeStyle($lifestyle);
$lifestyle = new UserLifestyle();
$lifestyle->setQuestion($this->objectManager->getReference('Acme\UserBundle\Entity\LifestyleQuestion', 3));
$user->addLifeStyle($lifestyle);
return $user;
}
Entity UserLifestyle
/**
* #ORM\Entity
* #Gedmo\Loggable
* #ORM\Table(name="ta_user_lifestyle")
*/
class UserLifestyle
{
/**
* #ORM\Id
* #ORM\Column(type="smallint")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="lifestyle")
* #ORM\JoinColumn(name="user_id")
*/
protected $user;
/**
* #ORM\ManyToOne(targetEntity="LifestyleQuestion", inversedBy="answeredByUser")
* #ORM\JoinColumn(name="question_id")
*/
protected $question;
/**
* #ORM\ManyToOne(targetEntity="LifestyleAnswer", inversedBy="userAnswers")
* #ORM\JoinColumn(name="answer_id")
* #Gedmo\Versioned
*/
protected $answer;
Then, there's a form type
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ORM\EntityRepository;
class RegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', NULL, array('label' => 'E-Mail'))
[...]
->add('lifestyle', 'collection', array(
'type' => new RegistrationLifestyleType(),
'allow_add' => false,
'allow_delete' => false,
'label' => false,
))
and now there should be a related RegistrationLifestyleType. But I've no idea how it should look like. I expect, that there are three choice fields in my registration form, showing a question (as label) and bunch of answers (as choice field) related to these questions. The UserManager assigns three questions to a newly created user, so one can get a question with:
$lifestyles = $user->getLifestyles();
foreach ($lifestyles as $lifestyle) {
$question = $lifestyle->getQuestion(); // echo $question->getQuestion();
$answers = $lifestyle->getQuestion()->getAnswers(); // loop through $answers and echo $answer->getAnswer();
}
But how I can modify the form type, to get this working. Important: my intention is to use built-in functionality as most as possible and trying to avoid inflating form types and others by injecting service containers and entity managers.
Found a solution, perhaps someone can use it. The problem seems, that LifestyleQuestion and LifestyleAnswer are 1:n relations at the same object (UserLifestyle), so Symfony does not know how to deal with it, even if I set the LifestyleQuestion to a specific question in UserManager already. Regarding https://stackoverflow.com/a/9729888/672452 one has to use form listeners, so the parent object is available in sub form. So here is my "simple" RegistrationLifestyleType (without using any injected container or manager):
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Security\Core\SecurityContext;
class RegistrationLifestyleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder) {
$form = $event->getForm();
$lifestyle = $event->getData();
if (!($lifestyle instanceof \Acme\UserBundle\Entity\UserLifestyle) || !$lifestyle->getQuestion()) return;
$label = $lifestyle->getQuestion()->getQuestion();
$questionId = $lifestyle->getQuestion()->getId();
$form->add('answer', 'entity', array(
'class' => 'AcmeUserBundle:LifestyleAnswer',
'empty_value' => '',
'property' => 'answer',
'query_builder' => function(EntityRepository $er) use ($questionId) {
return $er
->createQueryBuilder('t1')
->andWhere('t1.question = :question')
->setParameter('question', $questionId)
->orderBy('t1.answer', 'ASC')
;
},
'label' => $label,
));
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\UserBundle\Entity\UserLifestyle',
'error_bubbling' => false,
));
}
}
Related
Here is my entity:
<?php
namespace App\Entity\Contact;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="contact_contact")
*/
class Contact
{
/**
* #ORM\Id
* #ORM\Column(type="integer", options={"unsigned":true})
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #Assert\NotBlank
* #ORM\Column(type="string", length=40, nullable=true)
*/
private $fname;
/**
* #ORM\Column(type="string", length=40, nullable=true)
*/
private $lname;
public function getId(): ?int
{
return $this->id;
}
public function getFname(): ?string
{
return $this->fname;
}
public function setFname(string $fname): self
{
$this->fname = $fname;
return $this;
}
public function getLname(): ?string
{
return $this->lname;
}
public function setLname(?string $lname): self
{
$this->lname = $lname;
return $this;
}
}
Here is the edit controller action code:
/**
* #Route("/{id}/edit", name="contact_contact_edit", methods={"GET","POST"})
*/
public function edit(Request $request, Contact $contact): Response
{
$form = $this->createForm(ContactType::class, $contact);
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$this->getDoctrine()->getManager()->flush();
}
}
return $this->render('contact/contact/edit.html.twig', [
'contact' => $contact,
'form' => $form->createView(),
]);
}
When I post the form but leave the fname (first name) field empty...I get this error (Symfony\Component\PropertyAccess\Exception\InvalidArgumentException)
Expected argument of type "string", "null" given at property path
"fname".
When creating the entity, the #Assert works as expected and the message says so...but if I leave it blank and update post...bzzzt error.
What am I missing?
EDIT | Here is the form class incase thats doing something?
<?php
namespace App\Form\Contact;
use App\Entity\Contact\Contact;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fname', TextType::class, ['label' => 'First Name'])
->add('lname', TextType::class, ['label' => 'Last Name']);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Contact::class,
'min_entry' => false,
// NOTE: Must be added to every form class to disable HTML5 validation
'attr' => ['novalidate' => 'novalidate']
]);
$resolver->setAllowedTypes('min_entry', 'bool');
}
}
That's one of the reasons you should avoid allowing the form component changing your entities directly. It will set the data, and then validate it. So it's totally possible for the entity to be in an invalid state after the form has been submitted.
Anyway, you can specify what an empty value should be:
->add('fname', TextType::class, ['label' => 'First Name', 'empty_data' => ''])
Hove to create collections of field and store in one db column type json_array?
My entity have column date_data which is json_array type. I want to render two fields on frontent.
First Field -> from - date type.
Second Field -> to - date type.
I use jQuery repeater lib, for render this fields as repeater field on frontend. And want to store fields data from repeater in date_data column in db like this.
[{"from": '12/31/2009' , "to": '01/16/2010' }, {"from": '02/10/2011' , "to": '02/16/2011' }]
You can create entity with json column for your data:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Test
*
* #ORM\Table(name="test")
* #ORM\Entity(repositoryClass="App\Repository\TestRepository")
*/
class Test
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", options={"unsigned":true})
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var array|null
*
* #ORM\Column(name="data", type="json", nullable=true)
*/
private $data;
public function getId(): ?int
{
return $this->id;
}
public function getData(): ?array
{
return $this->data;
}
public function setData(?array $data): self
{
$this->data = $data;
return $this;
}
}
and 2 forms: first for entity and second for data collection item:
App\Form\Test
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type as FormType;
class Test extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('data', FormType\CollectionType::class, [
'allow_add' => true,
'allow_delete' => true,
'entry_type' => 'App\\Form\\Data',
'label' => 'Data',
])
->add('save', FormType\SubmitType::class, [
'label' => 'Save',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'App\\Entity\\Test',
]);
}
}
App\Form\Data
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type as FormType;
class Data extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('from', FormType\TextType::class, [
'label' => 'from',
])
->add('to', FormType\TextType::class, [
'label' => 'to',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
}
}
And in controller
$test = $this->getDoctrine()->getRepository('App:Test')->find(1);
$form = $this->createForm(\App\Form\Test::class, $test, []);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dump($form->getData());
$this->getDoctrine()->getManager()->flush();
}
I have a form built on the idea of a Purchase. It has Purchase Items, a Payment Amount and a Payment Type (VISA, Mastercard, Cash). I have the form preloaded with 2 Purchase Items however I am trying to add an additional Purchase Item if the user chooses a Card Payment Type (VISA or Mastercard) Type.
This additional Purchase Item I am trying to add via the Controller.
Rendered Form view
The question is really, where do I implement this functionality in the Controller Action... Or is it better as an EventListener on the Form Type?
When the form is submitted with the additional Card Fee Purchase Item I get the following error...
Catchable Fatal Error: Argument 1 passed to Aazp\BookingBundle\Entity\PurchaseItem::__construct() must be an instance of Aazp\BookingBundle\Entity\Product, none given, called in /project/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/FormType.php on line 141 and defined in /project/src/Aazp/BookingBundle/Entity/PurchaseItem.php line 20
The BookingController
namespace Aazp\BookingBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Aazp\BookingBundle\Entity\Booking;
use Aazp\BookingBundle\Entity\Passenger;
use Aazp\BookingBundle\Entity\Payment;
use Aazp\BookingBundle\Entity\Purchase;
use Aazp\BookingBundle\Entity\PurchaseItem;
use Aazp\BookingBundle\Form\PurchaseItemType;
use Aazp\BookingBundle\Form\PurchaseType;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class BookingController extends Controller
{
public function passengerPaymentAction($passenger_id)
{
$request = Request::createFromGlobals();
$purchaseBalance = 0.0;
//Query the selected Passenger
$em = $this->getDoctrine()->getManager();
$passenger = $request->attributes->get('passenger', $em->getRepository('AazpBookingBundle:Passenger')->find($passenger_id));
if (!$passenger) {
throw $this->createNotFoundException('Unable to find Passenger entity.');
}
$purchase = $passenger->getPurchase();
//Has this Passenger made a Payment. If yes then Purchase exists.
if($purchase === NULL) //If Purchase does not exist then preload the form with default products.
{
$purchase = new Purchase();
$product_category_photo = $em->getRepository('AazpBookingBundle:ProductCategory')->findOneByName('FLIGHT-PHOTO');
$product_photo_option = $em->getRepository('AazpBookingBundle:Product')->findOneByProductCategory($product_category_photo);
if (!$product_photo_option) {
throw $this->createNotFoundException('Unable to find Flight Photo Product entity.');
}
$purchase_item_flight = new PurchaseItem($passenger->getFlight());
$purchase_item_photo_option = new PurchaseItem($product_photo_option);
//Add Purchase Items to the Purchase
$purchase->addPurchaseItem($purchase_item_flight);
$purchase->addPurchaseItem($purchase_item_photo_option);
//Set the Purchase on the Passenger
$passenger->setPurchase($purchase);
}
//Ajax call triggered by onChange event on PaymentType radio button in form
//Add additional Purchase Item for Card Type Payment
if($form->get('paymentType')->getData()->getId() > 1)
{
//PaymentType selected/modified then calculate Payment Fee
$product_category_card_fee = $em->getRepository('AazpBookingBundle:ProductCategory')->findOneByName('CARD-FEE');
$product_card_fee = $em->getRepository('AazpBookingBundle:Product')->findOneByProductCategory($product_category_card_fee);
if (!$productcard_fee) {
throw $this->createNotFoundException('Unable to find Card Fee Product entity.');
}
$purchase_item_card_fee = new PurchaseItem($product_card_fee);
//Add Purchase Items to the Purchase
$purchase->addPurchaseItem($purchase_item_card_fee);
$passenger->setPurchase($purchase);
return $this->render('AazpBookingBundle:Purchase:summary.html.twig', array(
'passenger' => $passenger,
'form' => $form->createView(),
));
}
$form = $this->createForm(new PurchaseType($em), $purchase);
$form->handleRequest($request);
//If form is Valid create Payment and persist.
if ($form->isValid())
{
$payment = new Payment();
$payment->setAmount($form->get('paymentAmount')->getData());
$payment->setPaymentType($form->get('paymentType')->getData());
$payment->setDescription($form->get('description')->getData());
$passenger->getPurchase()->addPayment($payment);
$passenger->getBooking()->setStatus(Booking::STATUS_CONFIRMED);
$em->persist($passenger->getPurchase());
$em->flush();
$this->get('session')->getFlashBag()->add('message', 'Payment '.$payment->getAmount().' CHF has been successful!');
return $this->redirect($this->generateUrl('booking_show', array ('id'=> $passenger->getBooking()->getId())));
}
return $this->render('AazpBookingBundle:Purchase:summary.html.twig', array(
'passenger' => $passenger,
'form' => $form->createView(),
));
}
}
The PurchaseItemType
namespace Aazp\BookingBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Aazp\BookingBundle\Entity\ProductRepository;
class PurchaseItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('product', 'entity', array('label' => 'Flight', 'class' => 'AazpBookingBundle:Product','property' => 'name', 'empty_value' => 'Please Select', 'required' => false, ));
$builder->add('amount', 'number', array('precision' => 2));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Aazp\BookingBundle\Entity\PurchaseItem', ));
}
public function getName()
{
return 'purchaseItem';
}
}
The PurchaseType
namespace Aazp\BookingBundle\Form;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Aazp\BookingBundle\Entity\PurchaseItem;
class PurchaseType extends AbstractType
{
protected $em;
function __construct(EntityManager $em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('purchaseItems', 'collection', array('type' => new PurchaseItemType(), 'allow_add' => true, 'allow_delete' => true, 'by_reference' => false));
$builder->add('paymentType', 'entity', array('label' => 'Payment Type', 'class' => 'AazpBookingBundle:PaymentType','property' => 'name', 'mapped' => false, 'expanded' => true));
$builder->add('paymentAmount', 'number', array('precision' => 2, 'data' => 0.0, 'mapped' => false));
$builder->add('description', 'text', array('mapped' => false, 'required' => false));
$builder->add('cancel', 'submit', array('attr' => array('formnovalidate' => true, 'data-toggle' => 'modal', 'data-target' => '#cancelWarning', )));
$builder->add('pay', 'submit');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Aazp\BookingBundle\Entity\Purchase', ));
}
public function getName()
{
return 'purchase';
}
}
The Purchase Entity
<?php
namespace Aazp\BookingBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Validator\Constraints as Assert;
use Aazp\MainBundle\Entity\BaseEntity;
/**
* #ORM\Entity(repositoryClass="Aazp\BookingBundle\Entity\PurchaseRepository")
* #ORM\Table(name="purchase")
* #Gedmo\SoftDeleteable(fieldName="deleted")
*/
class Purchase extends BaseEntity
{
/**
* Constructor
*/
public function __construct()
{
$this->purchaseItems = new \Doctrine\Common\Collections\ArrayCollection();
$this->payments = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="PurchaseItem", cascade={"all"})
* #ORM\JoinTable(name="purchase_purchase_items",
* joinColumns={#ORM\JoinColumn(name="purchase_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="purchase_item_id", referencedColumnName="id", unique=true)}
* )
**/
protected $purchaseItems;
/**
* #ORM\ManyToMany(targetEntity="Payment", inversedBy="purchases", cascade={"all"})
* #ORM\JoinTable(name="purchases_payments",
* joinColumns={#ORM\JoinColumn(name="purchase_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="payment_id", referencedColumnName="id")}
* )
**/
protected $payments;
/**
* #ORM\OneToOne(targetEntity="Passenger", mappedBy="purchase")
**/
protected $passenger;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Add purchaseItems
*
* #param \Aazp\BookingBundle\Entity\PurchaseItem $purchaseItems
* #return Purchase
*/
public function addPurchaseItem(\Aazp\BookingBundle\Entity\PurchaseItem $purchaseItems)
{
$this->purchaseItems[] = $purchaseItems;
return $this;
}
/**
* Remove purchaseItems
*
* #param \Aazp\BookingBundle\Entity\PurchaseItem $purchaseItems
*/
public function removePurchaseItem(\Aazp\BookingBundle\Entity\PurchaseItem $purchaseItems)
{
$this->purchaseItems->removeElement($purchaseItems);
}
/**
* Get purchaseItems
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPurchaseItems()
{
return $this->purchaseItems;
}
/**
* Add payments
*
* #param \Aazp\BookingBundle\Entity\Payment $payments
* #return Purchase
*/
public function addPayment(\Aazp\BookingBundle\Entity\Payment $payments)
{
$this->payments[] = $payments;
return $this;
}
/**
* Remove payments
*
* #param \Aazp\BookingBundle\Entity\Payment $payments
*/
public function removePayment(\Aazp\BookingBundle\Entity\Payment $payments)
{
$this->payments->removeElement($payments);
}
/**
* Get payments
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPayments()
{
return $this->payments;
}
/**
* Set passenger
*
* #param \Aazp\BookingBundle\Entity\Passenger $passenger
* #return Purchase
*/
public function setPassenger(\Aazp\BookingBundle\Entity\Passenger $passenger = null)
{
$this->passenger = $passenger;
return $this;
}
/**
* Get passenger
*
* #return \Aazp\BookingBundle\Entity\Passenger
*/
public function getPassenger()
{
return $this->passenger;
}
}
This happened because the default data_class option calls that constructor with no arguments. If your class Aazp\Booking Bundle\Entity\PurchaseItem has parameters in the constructor, you need to use the empty_data option to instantiate:
// Aazp\BookingBundle\Form\PurchaseItemType.php
$resolver->setDefaults([
'empty_data' => function (FormInterface $form) {
return new PurchaseItem($form->get('product')->getData());
},
]);
You can lean more about this option here.
I am trying to create a form which will collect a list of facilities and contact information:
The issue is I would like to have the facility, and exactly 4 contacts of different types. I realize I could make this work separately by making each contact a property on the Facility entity but it feels like it would be cleaner and easier to use the data later if it is in a collection.
You'll notice in the FacilityType class, I use $builder->create to add a sub-form which gives me the structure I'm expecting but I get an error when I try to persist.
"A new entity was found through the relationship 'AppBundle\Entity\Facility#contacts' that was not configured to cascade persist operations for entity:...."
Thanks. My code is below.
The Facility entity:
<?php
namespace AppBundle\Form;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Facility
{
/**
* #ORM\Column(length=200)
*/
public $name;
/**
* #ORM\OneToMany(targetEntity="FacilityContact", mappedBy="facility")
*/
public $contacts;
public function __construct()
{
$this->contacts = new ArrayCollection;
}
}
The FacilityContact entity:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class FacilityContact
{
/**
* #ORM\ManyToOne(targetEntity="facility", inversedBy="contacts")
*/
public $facility;
/**
* #ORM\Column(type="string", length=50)
*/
public $contactType;
/**
* #ORM\Column(type="string", length=200)
*/
public $name;
/**
* #ORM\Column(type="string", length=100)
*/
public $email;
/**
* #ORM\Column(type="string", length=15)
*/
public $phone;
}
Facility Form
class FacilityType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$centre = $this->centre;
$builder->add('name', 'text', array("label"=>"Facility Name");
$contacts = $builder->create("contacts", "form");
$contacts
->add("radonc", new FacilityContactType("radonc"), array("label" => "Radiation Oncologist"))
->add("physicist", new FacilityContactType("physicist"), array("label" => "Physicist Responsible"))
->add("radtherapist", new FacilityContactType("radtherapist"), array("label" => "Radiation Therapists"))
->add("datamanager", new FacilityContactType("datamanager"), array("label" => "Data manager"))
;
$builder->add($contacts);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Facility'
));
}
public function getName()
{
return 'appbundle_facility';
}
}
FacilityContact form
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class FacilityContactType extends AbstractType
{
private $contactType;
public function __construct($contactType)
{
$this->contactType = $contactType;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('contactType', 'hidden', array("read_only" => true, "data"=>$this->contactType))
->add('name', 'text', array("required" => false))
->add('phone', 'text', array("required" => false))
->add('email', 'email', array("required" => false))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\FacilityContact'
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_facilitycontact';
}
}
Doctrine is trying to tell you that there are one or more objects, that it does not know about, because they were not persisted into the database.
This happens, because in the FacilityForm you are using the FacilityContactForm, that creates 4 new FacilityContact entities. You are trying to persist only the Facility entity without the other 4 entities, it wouldn't be any problem, but there is a relationship between them and in the database the Facility entity doesn't have its FacilityContacts.
You can resolve this in two ways:
using the cascade options, something like this: #ORM\OneToMany(targetEntity="FacilityContact", mappedBy="facility", cascade={"persist"}), basically this tells doctrine that it should persist also the FacilityContacts entities, you can read more at: http://doctrine-orm.readthedocs.org/en/latest/reference/working-with-associations.html, and you also have here some explanation about cascade: Doctrine Cascade Options for OneToMany.
another option is to create before the Facility entity with the contact collection empty, persist it to the database, and then to persist the FacilityContacts to the database and add them to the contact array with the add method, and then only call $em->update(), because doctrine knows and is monitoring any changes about the Facility entity.
Names changed due to NDA.
I'm trying to come up with a survey form. Each survey question can have multiple answers/scores, so there's a natural 1:* relationship to them. That said, for the public-facing form, I need to have a 1:1 relationship between the score and the question it relates to, which is what I'm working on now. Right now, the survey is open to the public, so each completed survey is not related to a user.
The interesting parts of my current setup are as follows...
Question:
namespace Acme\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Question
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string question
*
* #ORM\Column(name="question", type="string", length=255)
*/
private $question;
/**
* #var ArrayCollection scores
*
* #ORM\OneToMany(targetEntity="Score", mappedBy="question")
*/
private $scores;
public function __construct()
{
$this->scores = new ArrayCollection();
}
// other getters and setters
/**
* #param $score
*/
public function setScore($score)
{
$this->scores->add($score);
}
/**
* #return mixed
*/
public function getScore()
{
if (get_class($this->scores) === 'ArrayCollection') {
return $this->scores->current();
} else {
return $this->scores;
}
}
}
Those last two are helper methods so I can add/retrieve individual scores. The type checking convolutions were due to an error I encountered here
Score:
namespace Acme\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Score
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var integer $question
*
* #ORM\ManyToOne(targetEntity="Question", inversedBy="scores")
* #ORM\JoinColumn(name="question_id", referencedColumnName="id")
*/
private $question;
/**
* #var float score
*
* #ORM\Column(name="score", type="float")
*/
private $score;
// getters and setters
}
Controller method:
public function takeSurveyAction(Request $request)
{
$em = $this->get('doctrine')->getManager();
$questions = $em->getRepository('Acme\MyBundle\Entity\Question')->findAll();
$viewQuestions = array();
foreach ($questions as $question) {
$viewQuestions[] = $question;
$rating = new Score();
$rating->setQuestion($question->getId());
$question->setRatings($rating);
}
$form = $this->createForm(new SurveyType(), array('questions' => $questions));
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
foreach ($questions as $q) {
$em->persist($q);
}
$em->flush();
$em->clear();
$url = $this->get('router')->generate('_main');
$response = new RedirectResponse($url);
return $response;
}
}
return $this->render('MyBundle:Survey:take.html.twig', array('form' => $form->createView(), 'questions' => $viewQuestions));
}
My form types....
SurveyType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('questions', 'collection', array('type' => new SurveyListItemType()));
}
public function getName()
{
return 'survey';
}
}
SurveyListItemType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyListItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('rating', new SurveyScoreType());
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Question'));
}
public function getName()
{
return 'survey_list_item_type';
}
}
SurveyScoreType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyRatingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('score', 'choice', array('choices' => array(
'0' => '',
'0.5' => '',
'1' => '',
'1.5' => '',
'2' => '',
'2.5' => '',
'3' => '',
'3.5' => '',
'4' => '',
'4.5' => '',
'5' => ''
), 'expanded' => true, 'multiple' => false));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Score'));
}
public function getName()
{
return 'survey_score_type';
}
}
Okay, with all of that, I'm getting the following error when Doctrine's EntityManager attempts to flush() in my controller action:
Catchable Fatal Error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in /home/kevin/www/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 547 and defined in /home/kevin/www/project/vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php line 47
I believe it has to do with the questions' related scores, as they're supposed to be an array(collection) in Question, but they're individual instances in this case. The only problem is I'm not sure how to fix it.
I'm thinking my form setup may be too complex. All I really need to do is attach each Question.id to each related Score. I'm just not sure the best way to build the form part of it so everything is persisted properly.
I believe your error is here
$rating = new Score();
//...
$question->setRatings($rating);
Usually if you have an ArrayCollection in your Entity, then you have addChildEntity and removeChildEntity methods that add and remove elements from the ArrayCollection.
setRatings() would take an array of entities, rather than a single entity.
Assuming that you do have this method, try
$question->addRating($rating);
I think you have a mistake in your setRating method.
You have
$this->score->add($score);
It should be:
$this->scores->add($score);
I was able to solve it by simply handling the Scores. So, with that approach, I was able to remove SurveyListItemType, and make the following changes:
SurveyType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('scores', 'collection', array('type' => new SurveyRatingType()));
}
public function getName()
{
return 'survey';
}
}
Note how the collection type is now mapped to SurveyRatingType.
SurveyRatingType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyRatingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('score', 'choice', array('choices' => array(
'0' => '',
'0.5' => '',
'1' => '',
'1.5' => '',
'2' => '',
'2.5' => '',
'3' => '',
'3.5' => '',
'4' => '',
'4.5' => '',
'5' => ''
), 'expanded' => true, 'multiple' => false));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Score'));
}
public function getName()
{
return 'survey_rating_type';
}
}
And my modified controller action:
public function takeSurveyAction(Request $request)
{
$em = $this->get('doctrine')->getManager();
$questions = $em->getRepository('Acme\MyBundle\Entity\Question')->findAll();
$ratings = array();
foreach ($questions as $question) {
$rating = new SurveyRating();
$rating->setQuestion($question);
$ratings[] = $rating;
}
$form = $this->createForm(new SurveyType(), array('ratings' => $ratings));
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
foreach ($ratings as $r) {
$em->persist($r);
}
$em->flush();
$em->clear();
$url = $this->get('router')->generate('_main');
$response = new RedirectResponse($url);
return $response;
}
}
return $this->render('MyBundle:Survey:take.html.twig', array('form' => $form->createView(), 'questions' => $questions));
}
I had a feeling I was doing it wrong due to the three form types. That really jumped out as a bad code smell. Thanks to everyone for their patience and attempts at helping. :)