I'm building a form generator using Symfony 2.2 with Doctrine. The base principle is that the user create a new form by filling its name and selecting the widgets he'd likes to have in a select menu.
We can think of WidgetInputText, WidgetSelect, WidgetFile, etc.
Here is an example of my model:
<?php
namespace Ineat\FormGeneratorBundle\Entity\Widget;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
/**
* Widget
*
* #ORM\Table(name="widget")
* #ORM\Entity
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"widget_text" = "WidgetText", "widget_input_text" = "WidgetInputText", "widget_select" = "WidgetSelect"})
*/
abstract class Widget
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Form", inversedBy="widgets")
*/
private $form;
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="Question")
*/
private $question;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set form
*
* #param \Ineat\FormGeneratorBundle\Entity\Form $form
* #return Widget
*/
public function setForm(\Ineat\FormGeneratorBundle\Entity\Form $form = null)
{
$this->form = $form;
return $this;
}
/**
* Get form
*
* #return \Ineat\FormGeneratorBundle\Entity\Form
*/
public function getForm()
{
return $this->form;
}
/**
* Set question
*
* #param \Ineat\FormGeneratorBundle\Entity\Question $question
* #return Widget
*/
public function setQuestion(\Ineat\FormGeneratorBundle\Entity\Question $question = null)
{
$this->question = $question;
return $this;
}
/**
* Get question
*
* #return \Ineat\FormGeneratorBundle\Entity\Question
*/
public function getQuestion()
{
return $this->question;
}
}
<?php
namespace Ineat\FormGeneratorBundle\Entity\Widget;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
/**
* Widget
*
* #ORM\Entity
* #ORM\Table(name="widget_text")
*/
class WidgetText extends Widget
{
/**
* #var string
*
* #ORM\Column(type="text")
*/
private $text;
/**
* Set text
*
* #param string $text
* #return WidgetText
*/
public function setText($text)
{
$this->text = $text;
return $this;
}
/**
* Get text
*
* #return string
*/
public function getText()
{
return $this->text;
}
}
<?php
namespace Ineat\FormGeneratorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Form
*
* #ORM\Table(name="form")
* #ORM\Entity(repositoryClass="Ineat\FormGeneratorBundle\Entity\FormRepository")
* #UniqueEntity("name")
* #UniqueEntity("slug")
*/
class Form
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=255)
*/
private $slug;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Widget", mappedBy="form", cascade={"persist"})
*/
private $widgets;
public function __construct()
{
$this->widgets = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Form
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set slug
*
* #param string $slug
* #return Form
*/
public function setSlug($slug)
{
$this->slug = $slug;
return $this;
}
/**
* Get slug
*
* #return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* Add widgets
*
* #param \Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget
* #return Form
*/
public function addWidget(\Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget)
{
$this->widgets[] = $widget;
return $this;
}
/**
* Remove widgets
*
* #param \Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget
*/
public function removeWidget(\Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget)
{
$this->widgets->removeElement($widget);
}
/**
* Get widgets
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getWidgets()
{
return $this->widgets;
}
/**
* Set widgets
*
* #param \Doctrine\Common\Collections\Collection $widgets
*/
public function setWidget(\Doctrine\Common\Collections\Collection $widgets)
{
$this->widgets = $widgets;
}
public function __set($name, $obj)
{
if (is_a($obj, '\Ineat\FormGeneratorBundle\Entity\Widget\Widget')) {
$this->addWidget($obj);
}
}
}
As you can see a form can have multiple widgets attached to him.
I've made an abstract class Widget because all widgets have common fields and are of type Widget and because in the Form entity it seems really really bad to had one collection per Widget type (bad and boring).
This model works, I've unit tested it and I'm able to attach a WidgetText to a form and then retrieve it.
The issue comes when I try to use forms with it.
<?php
namespace Ineat\FormGeneratorBundle\Form;
use Ineat\FormGeneratorBundle\Entity\Widget\WidgetText;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class FormType extends AbstractType
{
protected $widgets;
public function __construct(array $widgets = array())
{
$this->widgets = $widgets;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text')
->add('slug', 'text')
->add('WidgetText', 'collection', array(
'type' => new WidgetTextType(),
'allow_add' => true,
'attr' => array('class' => 'widget-text'),
'by_reference' => false
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Ineat\FormGeneratorBundle\Entity\Form',
));
}
public function getName()
{
return 'ineat_formgeneratorbundle_formtype';
}
}
<?php
namespace Ineat\FormGeneratorBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class WidgetTextType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('text', 'text')
;
}
public function getName()
{
return 'ineat_formgeneratorbundle_widgettexttype';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Ineat\FormGeneratorBundle\Entity\Widget\WidgetText',
));
}
}
When I try to display the form I've got the following error:
Neither property "WidgetText" nor method "getWidgetText()" nor method "isWidgetText()" exists in class "Ineat\FormGeneratorBundle\Entity\Form"
It's like Symfony doesn't know that my WidgetText is of type Widget too.
If in the controller (generated by Symfony) I change this line:
$this->createForm(new FormType(), new Form())
To:
$this->createForm(new FormType())
The form displays well but when submitting I've got no data binded.
I'm completly stuck there, from an OOP point of view I think this should work but I'm not sure if Symfony allows me to do what I want.
As mentioned in the comments of your question, you should change the name of the "WidgetText" field to "widgets". The reason behind that is that the names of the fields should match the accessors in your model (i.e. "name" for (set|get)Name(), "widgets" for (set|get)Widgets() etc.)
If you really want the name of a field to differ from the accessor in the model, you can also use the "property_path" option (which is set to the field's name by default):
$builder->add('WidgetText', ..., array(
...
'property_path' => 'widgets',
));
Related
I have these entities with one to many relationships:
freightOrder has many shipments
shipment has many shipmentLines
The freightOrder entity looks like this:
<?php
namespace Fraktportalen\Bundle\ShipmentBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* #ExclusionPolicy("all")
*/
class FreightOrder
{
/**
* #Expose
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #Expose
*
* #ORM\OneToMany(
* targetEntity="Fraktportalen\Bundle\ShipmentBundle\Entity\Shipment",
* mappedBy="freightOrder",
* cascade={"persist","remove"}
* )
*/
private $shipments;
/**
* #Expose
*
* #ORM\ManyToOne(targetEntity="Fraktportalen\Bundle\AccountBundle\Entity\Account")
* #ORM\JoinColumn(name="freighter_id", referencedColumnName="id")
*/
private $freighter;
/**
* Order constructor.
*/
public function __construct()
{
$this->shipments = new ArrayCollection();
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getShipments()
{
return $this->shipments;
}
/**
* #param mixed $shipment
*/
public function addShipment(Shipment $shipment)
{
$shipment->setFreightOrder($this);
$this->shipments->add($shipment);
}
public function removeShipment(Shipment $shipment)
{
$this->shipments->removeElement($shipment);
}
/**
* #return mixed
*/
public function getFreighter()
{
return $this->freighter;
}
/**
* #param mixed $freighter
*/
public function setFreighter($freighter)
{
$this->freighter = $freighter;
}
}
The shipment entity:
<?php
namespace Fraktportalen\Bundle\ShipmentBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Fraktportalen\Bundle\AccountBundle\Entity\Account;
use Fraktportalen\Bundle\AddressBundle\Entity\Address;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* #ExclusionPolicy("all")
*/
class Shipment
{
/**
* #Expose
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #Expose
*
* #ORM\Column(type="datetime", nullable=true)
*/
private $pickup;
/**
* #Expose
*
* #ORM\Column(type="datetime", nullable=true)
*/
private $delivery;
/**
* #Expose
*
* #ORM\OneToMany(
* targetEntity="Fraktportalen\Bundle\ShipmentBundle\Entity\ShipmentLine",
* mappedBy="shipment",
* fetch="EAGER",
* cascade={"persist"}
* )
*/
private $shipmentLines;
/**
* #Expose
*
* #ORM\ManyToOne(targetEntity="Fraktportalen\Bundle\ShipmentBundle\Entity\FreightOrder", inversedBy="shipments")
* #ORM\JoinColumn(name="freight_order_id", referencedColumnName="id", nullable=false)
*/
private $freightOrder;
/**
* #Expose
*
* #ORM\ManyToOne(targetEntity="Fraktportalen\Bundle\AccountBundle\Entity\Account")
* #ORM\JoinColumn(name="sender_id", referencedColumnName="id")
*/
private $sender;
/**
* #Expose
*
* #ORM\ManyToOne(targetEntity="Fraktportalen\Bundle\AccountBundle\Entity\Account")
* #ORM\JoinColumn(name="receiver_id", referencedColumnName="id")
*/
private $receiver;
/**
* Shipment constructor.
*/
public function __construct()
{
$this->shipmentLines = new ArrayCollection();
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getFreightOrder() {
return $this->freightOrder;
}
/**
* #param mixed $freightOrder
*
* #return Shipment
*/
public function setFreightOrder($freightOrder) {
$this->freightOrder = $freightOrder;
$freightOrder->addShipment($this);
return $this;
}
/**
* #return mixed
*/
public function getPickup()
{
return $this->pickup;
}
/**
* #param mixed $pickup
*/
public function setPickup(\DateTime $pickup = null)
{
$this->pickup = $pickup;
}
/**
* #return mixed
*/
public function getDelivery()
{
return $this->delivery;
}
/**
* #param mixed $delivery
*/
public function setDelivery(\DateTime $delivery = null)
{
$this->delivery = $delivery;
}
/**
* #return mixed
*/
public function getShipmentLines()
{
return $this->shipmentLines;
}
/**
* #param mixed $shipmentLine
*/
public function addShipmentLine(ShipmentLine $shipmentLine)
{
$shipmentLine->setShipment($this);
$this->shipmentLines->add($shipmentLine);
}
public function removeShipmentLine(ShipmentLine $shipmentLine)
{
$this->shipmentLines->removeElement($shipmentLine);
}
/**
* #return mixed
*/
public function getSender()
{
return $this->sender;
}
/**
* #param mixed $sender
*/
public function setSender(Account $sender)
{
$this->sender = $sender;
}
/**
* #return mixed
*/
public function getReceiver()
{
return $this->receiver;
}
/**
* #param mixed $receiver
*/
public function setReceiver(Account $receiver)
{
$this->receiver = $receiver;
}
}
The freightOrderType next:
<?php
namespace Fraktportalen\Bundle\ShipmentBundle\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FreightOrderType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('freighter', EntityType::class, array(
'class' => 'AccountBundle:Account',
))
->add('shipments', CollectionType::class, array(
'entry_type' => ShipmentType::class,
'allow_add' => true
))
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Fraktportalen\Bundle\ShipmentBundle\Entity\FreightOrder',
'csrf_protection' => false
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'freightOrder';
}
}
When saving a freightOrder from an Ember app, it sends this json:
{
"freightOrder": {
"freighter": "3",
"shipments": [{
"pickup": "2017-03-22 12:32:00",
"delivery": "2017-03-23 12:32:00",
"sender": "1",
"receiver": "2",
"shipment_lines": [{
"package_weight": 45,
"package_length": 240,
"package_width": 120,
"package_height": 240,
"package_type": "3"
}]
}]
}
}
Everything works as expected except that freightOrder on shipments is not saved. Even though I add it on the freightOrder entity addShipment function:
/**
* #param mixed $shipment
*/
public function addShipment(Shipment $shipment)
{
$shipment->setFreightOrder($this);
$this->shipments->add($shipment);
}
As you can see in the shipment entity, I have even tried to add shipment to freightOrder there. But nothing works.
Shipment lines are added to shipment though, and that is also a one to many relationship.
Will I have to add a form event listener to the freightOrderType and manually add freightOrder to all shipments? I was under the impression that adding freightOrder to shipments when adding said shipments to freightOrder would solve it. At least that is what I have found when searching SO.
Thanks for your time,
Tommy
I feel your pain, I fight with this problem quite often.
It sounds like the setter isnt getting called. You can sometimes fix this by declaring by_reference to false.
try this..
$builder
->add('freighter', EntityType::class, array(
'class' => 'AccountBundle:Account',
))
->add('shipments', CollectionType::class, array(
'entry_type' => ShipmentType::class,
'allow_add' => true,
'by_reference' => false,
))
;
reading the docs:
Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.
Personally, I still think by_reference is some kind of hacky fix for a bug they had once.. I guess we'll never know..
I have a form customer registration type with an embed address form (billingAddress and an other optional ShippingAddress).
If the user don't check the "different shipping Address" checkbox, I should have only one addrress in my db address table. Even so I get two differents addresses in my Address table (with the same informations).
However, I check with "addAddress" if the address is already exist before add a new address. In fact in my Customer FormType, I copy billingAddress form data in shippingAdress form but in hasAdress method, $this->addresses->contains($address) return always false. I don't understand why...
Thanks
Here the code:
Customer Class
<?php
namespace DRMS\CustomerBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use DRMS\AddressingBundle\Entity\Address;
/**
* DRMS\CustomerBundle\Entity\Customer
*
* #ORM\Entity(repositoryClass="DRMS\CustomerBundle\Repository\CustomerRepository")
* #ORM\Table(name="customer", uniqueConstraints={#ORM\UniqueConstraint(name="customer_id_UNIQUE", columns={"customer_id"})})
*/
class Customer
{
/**
* #ORM\Id
* #ORM\Column(type="integer", name="customer_id")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $customerId;
// Others attributes
// ...
/**
* #ORM\OneToOne(targetEntity="DRMS\AddressingBundle\Entity\Address")
* #ORM\JoinColumn(name="billing_address_id", referencedColumnName="address_id")
*/
protected $billingAddress;
/**
* #ORM\OneToOne(targetEntity="DRMS\AddressingBundle\Entity\Address")
* #ORM\JoinColumn(name="shipping_address_id", referencedColumnName="address_id")
*/
protected $shippingAddress;
/**
* #ORM\OneToMany(targetEntity="DRMS\AddressingBundle\Entity\Address", mappedBy="customer", cascade={"remove", "persist"})
** ORM\JoinColumn(name="customer_id", referencedColumnName="customer_id", onDelete="CASCADE")
*/
protected $addresses;
public function __construct()
{
$this->addresses = new ArrayCollection();
}
/**
* Set the value of customerId.
*
* #param integer $customerId
* #return \DRMS\CustomerBundle\Entity\Customer
*/
public function setCustomerId($customerId)
{
$this->customerId = $customerId;
return $this;
}
/**
* Get the value of customerId.
*
* #return integer
*/
public function getCustomerId()
{
return $this->customerId;
}
// Others Getters/Setters
// ...
/**
* Set billingAddress
*
* #param \DRMS\AddressingBundle\Entity\Address $billingAddress
* #return Customer
*/
public function setBillingAddress(Address $billingAddress = null)
{
$this->billingAddress = $billingAddress;
if (null !== $billingAddress && !$this->hasAddress($billingAddress)) {
$this->addAddress($billingAddress);
}
return $this;
}
/**
* Get billingAddress
*
* #return \DRMS\AddressingBundle\Entity\Address
*/
public function getBillingAddress()
{
return $this->billingAddress;
}
/**
* Set shippingAddress
*
* #param \DRMS\AddressingBundle\Entity\Address $shippingAddress
* #return Customer
*/
public function setShippingAddress(Address $shippingAddress = null)
{
$this->shippingAddress = $shippingAddress;
if (null !== $shippingAddress && !$this->hasAddress($shippingAddress)) {
$this->addAddress($shippingAddress);
}
return $this;
}
/**
* Get shippingAddress
*
* #return \DRMS\AddressingBundle\Entity\Address
*/
public function getShippingAddress()
{
return $this->shippingAddress;
}
/**
* Add Address entity to collection (one to many).
*
* #param \DRMS\AddressingBundle\Entity\Address $address
* #return \DRMS\CustomerBundle\Entity\Customer
*/
public function addAddress(Address $address)
{
if (!$this->hasAddress($address)) {
$this->addresses->add($address);
$address->setCustomer($this);
}
return $this;
}
/**
* Remove Address
*
* #param \DRMS\AddressingBundle\Entity\Address $address
* #return \DRMS\CustomerBundle\Entity\Customer
*/
public function removeAddress(Address $address) {
$this->addresses->removeElement($address);
return $this;
}
/**
* Get Address entity collection (one to many).
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getAddresses()
{
return $this->addresses;
}
/**
* Has address
*
* #param \DRMS\AddressingBundle\Entity\Address $address
* #return bool
*/
public function hasAddress(Address $address)
{
return $this->addresses->contains($address);
}
}
Address class
<?php
namespace DRMS\AddressingBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
/**
* DRMS\AddressingBundle\Entity\Address
*
* #ORM\Entity(repositoryClass="DRMS\AddressingBundle\Repository\AddressRepository")
* #ORM\Table(name="address"), uniqueConstraints={#ORM\UniqueConstraint(name="customer_adress_UNIQUE", columns={"customer_id","address_id"})}
*/
class Address
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $address_id;
// Others attributes
// ...
/**
* #ORM\ManyToOne(targetEntity="DRMS\CustomerBundle\Entity\Customer", inversedBy="addresses", cascade={"remove"})
* #ORM\JoinColumn(name="customer_id", referencedColumnName="customer_id", onDelete="CASCADE")
*/
protected $customer;
/**
* Get the value of address_id.
*
* #return integer
*/
public function getAddressId()
{
return $this->address_id;
}
/**
* Set customer
*
* #param \DRMS\CustomerBundle\Entity\Customer $customer
* #return Address
*/
public function setCustomer(\DRMS\CustomerBundle\Entity\Customer $customer)
{
$this->customer = $customer;
return $this;
}
/**
* Get customer
*
* #return \DRMS\CustomerBundle\Entity\Customer
*/
public function getCustomer()
{
return $this->customer;
}
}
Customer FormType
<?php
# src\DRMS\CustomerBundle\Form\Type\CustomerType.php
namespace DRMS\CustomerBundle\Form\Type;
/**
* Description of CustomerType
*
*/
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
class CustomerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!array_key_exists('differentShippingAddress', $data) || false === $data['differentShippingAddress']) {
$data['shippingAddress'] = $data['billingAddress'];
$event->setData($data);
}
});
$builder->add('customerName', null)
->add('customerFirstname', null)
->add('phone')
->add('mobilePhone')
->add('billingAddress', 'drms_address', array(
'label' => 'drms.form.customer.billing_address',
))
->add('differentShippingAddress', 'checkbox', array(
'mapped' => false,
'label' => 'drms.form.customer.different_shipping_address',
'required' => false,
))
->add('shippingAddress', 'drms_address', array(
'label' => 'drms.form.customer.shipping_address',
))
;
}
/**
* #param OptionsResolverInterface $resolver
*
* If checkbox vatNumberOwner is checked, apply "vat_number_required" validation group for activate Vat Number test
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'DRMS\CustomerBundle\Entity\Customer',
'validation_groups' => function(FormInterface $form) {
$validation_groups = array('Registration');
if($form->get('differentShippingAddress')->getData() == true) {
$validation_groups[] = 'ShippingAddressRequired';
}
return $validation_groups;
},
));
}
public function getName()
{
return 'drms_customer';
}
}
contains() is implemented like this https://github.com/doctrine/collections/blob/master/lib/Doctrine/Common/Collections/ArrayCollection.php#L189 That means you are doing a strict comparison on the object identity. Since the address object is created by the form and not managed by Doctrine in the identity map it will not compare to the objects created by Doctrine. Since custom collections are not supported i suggest you change the in_array logic to be not strict and put it in your entity directly.
In my Symdony2 project I've two related entities: "Reglement and "Article". This should be many-to-one relationship, because each "Reglement" can have many "Articles", and each "Article" can belongs to one "Reglement".
Moreover, I need a user interface to manage Reglement and Articles. So, when adding a Reglement, user should be able to add many articles it belongs.
I've already achieved this by setting up a One-To-Many relation in my Doctrine entites. Everything is working like a charm, including user interface build on custom form types in Symfony2 (I've used "Collection" form field type to allow user to add "Articles" in "Reglement). The only problem I've is that I can save only the last article in the database !!
Here is my "Reglement" entity source code:
<?php
namespace GestionEnvironnementale\ISO14001Bundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Reglement
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="GestionEnvironnementale\ISO14001Bundle\Entity\ReglementRepository")
*/
class Reglement
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="domaineApplication", type="string", length=255)
*/
private $domaineApplication;
/**
* #var string
*
* #ORM\Column(name="texteLegislatif", type="text")
*/
private $texteLegislatif;
/**
* #var string
*
* #ORM\Column(name="contenuText", type="text")
*/
private $contenuText;
/**
* #ORM\OneToMany(targetEntity="GestionEnvironnementale\ISO14001Bundle\Entity\Article", cascade={"persist"}, mappedBy="reglement")
*/
private $articles;
public function __construct()
{
$this->articles = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getArticles()
{
return $this->articles;
}
public function setArticles(ArrayCollection $articles)
{
foreach ($articles as $article) {
$article->addReglement($this);
}
$this->articles = $articles;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set domaineApplication
*
* #param string $domaineApplication
* #return Reglement
*/
public function setDomaineApplication($domaineApplication)
{
$this->domaineApplication = $domaineApplication;
return $this;
}
/**
* Get domaineApplication
*
* #return string
*/
public function getDomaineApplication()
{
return $this->domaineApplication;
}
/**
* Set texteLegislatif
*
* #param string $texteLegislatif
* #return Reglement
*/
public function setTexteLegislatif($texteLegislatif)
{
$this->texteLegislatif = $texteLegislatif;
return $this;
}
/**
* Get texteLegislatif
*
* #return string
*/
public function getTexteLegislatif()
{
return $this->texteLegislatif;
}
/**
* Set contenuText
*
* #param string $contenuText
* #return Reglement
*/
public function setContenuText($contenuText)
{
$this->contenuText = $contenuText;
return $this;
}
/**
* Get contenuText
*
* #return string
*/
public function getContenuText()
{
return $this->contenuText;
}
/**
* Add articles
*
* #param \GestionEnvironnementale\ISO14001Bundle\Entity\Article $articles
* #return Reglement
*/
public function addArticle(\GestionEnvironnementale\ISO14001Bundle\Entity\Article $articles)
{
$this->articles[] = $articles;
return $this;
}
/**
* Remove articles
*
* #param \GestionEnvironnementale\ISO14001Bundle\Entity\Article $articles
*/
public function removeArticle(\GestionEnvironnementale\ISO14001Bundle\Entity\Article $articles)
{
$this->articles->removeElement($articles);
}
}
And here is my Article entity source code:
<?php
namespace GestionEnvironnementale\ISO14001Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Article
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="GestionEnvironnementale\ISO14001Bundle\Entity\ArticleRepository")
*/
class Article
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="GestionEnvironnementale\ISO14001Bundle\Entity\Reglement", inversedBy="articles")
* #ORM\JoinColumn(nullable=false)
*/
private $reglement;
/**
* #var string
*
* #ORM\Column(name="exigenceArticle", type="string", length=255)
*/
private $exigenceArticle;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set exigenceArticle
*
* #param string $exigenceArticle
* #return Article
*/
public function setExigenceArticle($exigenceArticle)
{
$this->exigenceArticle = $exigenceArticle;
return $this;
}
/**
* Get exigenceArticle
*
* #return string
*/
public function getExigenceArticle()
{
return $this->exigenceArticle;
}
/**
* Set reglement
*
* #param \GestionEnvironnementale\ISO14001Bundle\Entity\Reglement $reglement
* #return Article
*/
public function setReglement(\GestionEnvironnementale\ISO14001Bundle\Entity\Reglement $reglement)
{
$this->reglement = $reglement;
return $this;
}
/**
* Get reglement
*
* #return \GestionEnvironnementale\ISO14001Bundle\Entity\Reglement
*/
public function getReglement()
{
return $this->reglement;
}
public function addReglement(Reglement $reglement)
{
if (!$this->reglements->contains($reglement)) {
$this->reglements->add($reglement);
}
}
}
and here is my reglement form :
<?php
namespace GestionEnvironnementale\ISO14001Bundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ReglementType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('domaineApplication')
->add('texteLegislatif')
->add('contenuText')
->add('articles', 'collection', array(
'type' => new ArticleType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
));
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'GestionEnvironnementale\ISO14001Bundle\Entity\Reglement'
));
}
/**
* #return string
*/
public function getName()
{
return 'gestionenvironnementale_iso14001bundle_reglementtype';
}
}
and my article form :
<?php
namespace GestionEnvironnementale\ISO14001Bundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ArticleType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('exigenceArticle')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'GestionEnvironnementale\ISO14001Bundle\Entity\Article'
));
}
/**
* #return string
*/
public function getName()
{
return 'gestionenvironnementale_iso14001bundle_article';
}
}
And here is part of my ReglementController source code:
public function ajouterReglementAction()
{
$reglement = new Reglement();
$form = $this->createForm(new ReglementType(), $reglement);
$request = $this->getRequest();
if ($request->getMethod() == 'POST') {
$form->bind($request);
if ($form->isValid()) {
$reglement->getArticles()->clear();
$em = $this->getDoctrine()->getManager();
$em->persist($reglement);
$em->flush();
foreach ($form->get('articles')->getData() as $ac) {
$ac->setReglement($reglement);
$em->persist($ac);
}
$em->flush();
$this->get('session')->getFlashBag()->add('info', 'Reglement bien enregistré');
return $this->redirect( $this->generateUrl('reglement_voir', array('id' => $reglement->getId())));
}
}
return $this->render('ISO14001Bundle:Reglements:ajouter.html.twig', array(
'form' => $form->createView()
));
}
finnally the form view source code :
<div class="well">
<form method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<br/> <input type="submit" value="Envoyer" class="btn btn-primary" />
</form>
<script src="{{ asset('js/jquery-2.1.1.min.js') }}"></script>
<script type="text/javascript">
$(document).ready(function() {
var $container = $('div#gestionenvironnementale_iso14001bundle_reglementtype_articles');
var $lienAjout = $('Ajouter un article');
$container.append($lienAjout);
$lienAjout.click(function(e) {
ajouterArticle($container);
e.preventDefault();
return false;
});
var index = $container.find(':input').length;
if (index == 0) {
ajouterArticle($container);
} else {
$container.children('div').each(function() {
ajouterLienSuppression($(this));
});
}
function ajouterArticle($container) {
var $prototype = $($container.attr('data-prototype').replace(/__name__label__/g, 'Article n°' + (index+1))
.replace(/\$\$name\$\$/g, index));
ajouterLienSuppression($prototype);
$container.append($prototype);
index++;
}
function ajouterLienSuppression($prototype) {
$lienSuppression = $('Supprimer');
$prototype.append($lienSuppression);
$lienSuppression.click(function(e) {
$prototype.remove();
e.preventDefault();
return false;
});
}
});
I hope you can help me ;)
hi everyone I found the solution of my problem ^_^
the error is in line 27 of form view, there must be __name__ instead of $$name$$ as follows :
function ajouterArticle($container) {
var $prototype = $($container.attr('data-prototype').replace(/__name__label__/g, 'Article n°' + (index+1))
.replace(/__name__/g, index));
ajouterLienSuppression($prototype);
$container.append($prototype);
index++;
}
good luck ;)
I need to save user with his profile fields. I establish relations. User has one Profile, Profile has one User. I add fields to register form. But when I saving object, I get error,
Undefined index: profile in /var/www/Symfony/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php line 2714 . Where my mistake, please help me. I am newbie)
Here is the code:
namespace Acme\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Profile
*
* #ORM\Table(name="acme_profile")
* #ORM\Entity
*/
class Profile
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="fio", type="string", length=255)
*/
protected $fio;
/**
* #ORM\Column(name="birthDate", type="date")
*/
protected $birthDate;
/**
* #ORM\OneToOne(targetEntity="Acme\UserBundle\Entity\User", cascade={"persist"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getFio()
{
return $this->fio;
}
/**
* #param $fio
* #return $this
*/
public function setFio($fio)
{
$this->fio = $fio;
return $this;
}
/**
* #return mixed
*/
public function getBirthDate()
{
return $this->birthDate;
}
/**
* #param $birthDate
* #return $this
*/
public function setBirthDate($birthDate)
{
$this->birthDate = $birthDate;
return $this;
}
/**
* #return mixed
*/
public function getUser()
{
return $this->user;
}
/**
* #param User $user
* #return $this
*/
public function setUser(User $user = null)
{
$this->user = $user;
return $this;
}
}
namespace Acme\UserBundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="acme_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Acme\UserBundle\Entity\Profile", cascade={"persist"})
*/
protected $profile;
public function __construct()
{
parent::__construct();
}
public function setProfile(Profile $profile = null)
{
$this->profile = $profile;
return $this;
}
public function getProfile()
{
return $this->profile;
}
}
namespace Acme\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// add your custom field
$builder->add('profile', new ProfileType());
}
public function getParent()
{
return 'fos_user_registration';
}
public function getName()
{
return 'acme_user_registration';
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\UserBundle\Entity\User'
));
}
}
You should use the MappedBy and InversedBy as specified in the documentation here : http://docs.doctrine-project.org/en/2.0.x/reference/association-mapping.html#owning-side-and-inverse-side
/**
* #ORM\OneToOne(targetEntity="Acme\UserBundle\Entity\Profile",
mappedBy="user"))
*/
and the other side you use :
/**
* #ORM\OneToOne(targetEntity="Acme\UserBundle\Entity\User", inversedBy="profile" cascade={"persist"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
I'm trying to create a form to insert "questions" that can have one or more "answers". So I want to add an embed form for the Answer entity.
It seems to work because I see the Question form, but only the string "Answers" is displayed at the bottom of my form, not the fields.
Here is my controller action :
public function addQuestionAction(Category $category)
{
$question = new Question();
$form = $this->createForm(new QuestionType(), $question);
$request = $this->get('request');
if ($request->getMethod() === 'POST') {
$form->bind($request);
$question->setCategory($category);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($question);
$em->flush();
return $this->redirect($this->generateUrl('mycategory_questiondisplay',
array('id' => $question->getId())));
}
}
return $this->render('MyCategoryBundle:Question:add.html.twig',
array(
'form' => $form->createView(),
));
}
My QuestionType form :
<?php
namespace My\CategoryBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class QuestionType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('description')
->add('answers', 'collection', array(
'type' => new AnswerType(),
'allow_add' => true)
);
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Iel\CategoryBundle\Entity\Question'
));
}
/**
* #return string
*/
public function getName()
{
return 'my_categorybundle_question';
}
}
My Question Entity :
<?php
namespace My\CategoryBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Question
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="My\CategoryBundle\Entity\QuestionRepository")
*/
class Question
{
/**
* #ORM\OneToMany(targetEntity="My\CategoryBundle\Entity\Answer",
mappedBy="question", cascade={"persist", "remove"})
*/
private $answers;
public function __construct()
{
$this->answers = new
\Doctrine\Common\Collections\ArrayCollection();
}
public function addAnswer(\My\CategoryBundle\Entity\Answer
$answer)
{
$this->answers[] = $answer;
$answers->setQuestion($this);
return $this;
}
public function removeAnswer(\My\CategoryBundle\Entity\Answer $answer)
{
$this->answers->removeElement($answer);
}
public function getAnswers()
{
return $this->answers;
}
public function setAnswers($answer)
{
$this->answer = $answer;
return $this;
}
/**
* #ORM\ManyToOne(targetEntity="My\CategoryBundle\Entity\Category",
inversedBy="question")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
* })
*/
private $category;
/**
* Set category
*
#param My\CategoryBundle\Entity\Category $category
*/
public function setCategory(\My\CategoryBundle\Entity\Category $category)
{
$this->category = $category;
}
/**
* Get category
*
#return My\CategoryBundle\Entity\Category
*/
public function getCategory()
{
return $this->category;
}
/**
* Remove categories
**
#param My\CategoryBundle\Entity\Category $categories
*/
public function removeCategory(\My\CategoryBundle\Entity\Category $category)
{
$this->categories->removeElement($category);
}
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="titre", type="string", length=255)
*/
private $titre;
/**
* #var string
*
* #ORM\Column(name="description", type="text")
*/
private $description;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set titre
*
* #param string $titre
* #return Question
*/
public function setTitre($titre)
{
$this->titre = $titre;
return $this;
}
/**
* Get titre
*
* #return string
*/
public function getTitre()
{
return $this->titre;
}
/**
* Set description
*
* #param string $description
* #return Question
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
}
And finally, the Answer entity :
<?php
namespace My\CategoryBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Answer
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="My\CategoryBundle\Entity\AnswerRepository")
*/
class Answer
{
/**
* #ORM\ManyToOne(targetEntity="My\CategoryBundle\Entity\Question",
inversedBy="answer")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="question_id", referencedColumnName="id")
* })
*/
private $question;
/**
* Set question
*
#param My\CategoryBundle\Entity\Question $question
*/
public function setQuestion(\My\CategoryBundle\Entity\Question $question)
{
$this->question = $question;
}
/**
* Get question
*
#return My\CategoryBundle\Entity\Question
*/
public function getQuestion()
{
return $this->question;
}
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="answer", type="text", nullable=true)
*/
private $answer;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set answer
*
* #param string $answer
* #return Answer
*/
public function setAnswer($answer)
{
$this->answer = $answer;
return $this;
}
/**
* Get answer
*
* #return string
*/
public function getAnswer()
{
return $this->answer;
}
}
I'm really not able to find what's wrong in my form...
The problem is that your question does not have any answers yet. Hence no forms. Try
$question = new Question();
$question->addAnswer(new Answer());
That will show an empty answer form.
Look in the cookbook to see how to use javascript to add answers dynamically from within the browser.