Cross reference in Symfony2 form not work as expected - symfony

This is my first question here, so please excuse any mistakes - I'll try to avoid them the next time. ;-)
I've written a custom RegistrationFormType for the FOSUserBundle. This form handles - in addition to the default fields of the bundle - a PlayerType. This PlayerType itself again contains a PlayerSkillsType. Here the classes:
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilder $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('player', new PlayerType());
}
public function getName()
{
return 'signup_form';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\AcmeBundle\Entity\User',
);
}
}
class PlayerType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('firstname');
$builder->add('lastname');
$builder->add('age');
$builder->add('playerSkills', new PlayerSkillsType());
}
public function getName()
{
return 'player_form';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\AcmeBundle\Entity\Player',
);
}
}
class PlayerSkillsType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('tackling');
$builder->add('passing');
$builder->add('shooting');
}
public function getName()
{
return 'playerSkills_form';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\AcmeBundle\Entity\PlayerSkills',
);
}
}
/**
* #ORM\Entity
*/
class Player
{
/**
* #ORM\OneToOne(targetEntity="PlayerSkills", cascade={"persist"})
*
* #var PlayerSkills
*/
private $playerSkills;
}
/**
* #ORM\Entity
*/
class PlayerSkills
{
/**
* #ORM\OneToOne(targetEntity="Player", cascade={"persist"})
*
* #var Player
*/
private $player;
}
(I've left out getters and setters and unimportant properties and methods.)
This is working fine so far, the form is shown and persisted. Now, my problem is, that after persisting the data, the PlayerSkills entity in the data is missing the reference back to the Player entity.
I think it's something that I need to tell the PlayerSkillsType that it shall also add the reference in the form builder..? Or maybe this is issue in the Doctrine annotations?
Any hint is very appreciated! :-)

The problem could come from the initialization of your data and/or doctrine mapping.
The form will create the data_class if none is passed using $form->setData.
When you submit the form and bind data, It will call $player>setPlayerSkills($playerSkill),
but it won't call $playerSkill->setPlayer($player);
Depending of the owning side of your oneToOne association, you should call one of the two methods so that Doctrine will be aware of this association ( http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/association-mapping.html#owning-side-and-inverse-side ).
Try to modify your annotation mapping in PlayerSkills to introduce the inversedBy information also ( http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/association-mapping.html#one-to-one-bidirectional ).
It should be something like this:
/**
* #ORM\OneToOne(targetEntity="Player", mappedBy="playerSkills", cascade={"persist"})
*
* #var Player
*/
private $player;
Same thing for the Player class:
/**
* #ORM\OneToOne(targetEntity="PlayerSkills", inversedBy="player" cascade={"persist"})
*
* #var PlayerSkills
*/
private $playerSkills;
Last, you can code your methods to automatically synchronize inverse side, as explained here: http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/association-mapping.html#picking-owning-and-inverse-side .

Related

How to create a symfony2 form collection with fixed subforms

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.

Symfony2: Inserting collection form data

There is a one-to-one relationship between User and Company entity.
When initializing (creating) the user's company, userID should be bind to Company user field as a foreign key. But instead of that, i get this error message:
Property "id" is not public in class
"Website\CompanyBundle\Entity\User". Maybe you should create the
method "setId()"?
Why Symfony wants to create new User when this form is about Company entity, and User entity is just a collection that should provide user's ID.
Here is my code:
Company.php entity:
namespace Website\CompanyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="Website\CompanyBundle\Entity\Repository\CompanyRepository")
* #ORM\Table(name="company")
*/
class Company
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="User", inversedBy="company")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
/**
* #ORM\Column(type="string")
*/
protected $name;
}
CompanyType.php
class CompanyType extends AbstractType
{
private $security;
public function __construct(SecurityContext $security)
{
$this->security= $security;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$user = $this->securityContext->getToken()->getUser();
$builder
->add('user', new UserType($security))
->add('company_name')
->add('company_address')
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Website\CompanyBundle\Entity\Company'
));
}
public function getName()
{
return 'user';
}
}
UserRelationType.php
class UserRelationType extends AbstractType
{
private $user;
public function __construct(SecurityContext $security){
$this->user = $security->getToken()->getUser();
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', 'hidden', array('data' => $this->user->getId()))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Website\CompanyBundle\Entity\User'
));
}
public function getName()
{
return 'user';
}
}
User.php entity
namespace Website\CompanyBundle\Entity;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\OneToOne(targetEntity="Company", mappedBy="user")
*/
protected $company;
}
You map entity to form in UserRelationType. On save it try to set id on user entity. You must specify data transformer or use entity type if you need to select existed user.
If you want to set current user, better do it in event listener like pre_persist
your properties in your entities are protected but you have not created getters/setters for them. ( or you have not pasted your full code )
Therefore the form-builder is not able to access your user's properties.
At least the public function setId($id) in User.php is definitely missing!
That's why the exception is thrown saying:
Maybe you should create the method "setId()"?
Create getters and settes for every property using ...
app/console doctrine:generate:entities
or create them by hand...
User.php
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
return $this;
}
// ...
I solved this problem without adding collections and just using Doctrine transitive persistence like described in this page: https://doctrine-orm.readthedocs.org/en/latest/reference/working-with-associations.html#transitive-persistence-cascade-operations
CompanyController.php
public function createIndex(Request $request){
$user = $this->getId();
$company = new Company();
if($user instanceof User){
$company->setUser($user);
}
$request = $this->getRequest();
$createForm = $this->createForm(new CompanyType(), $company);
if('POST' === $request->getMethod())
{
$createForm->bindRequest($request);
if($createForm->isValid())
{
$em = $this->getDoctrine()->getManager();
$em->persist($company);
$em->flush();
}
}
}
Thank you all for your help

Symfony changing the form that a form render a ManyToOne Field

this is my very first question :S
I'm using Symfony2 and i'm having the following trouble
I have two entities related in a ManyToOne relation, I want to make a form for the followin entity
/**
* #ORM\Entity
* #ORM\Table(name="product")
*/
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $name;
/**
* #ORM\ManyToOne(targetEntity="Acme\ProductsBundle\Entity\ProductCategory", inversedBy="products")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
protected $productCategory;
}
So i did the following "ProductType"
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('productCategory')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\ProductsBundle\Entity\Product'
));
}
public function getName()
{
return 'acme_hellobundle_producttype';
}
}
And all works great when i render the form, but now i want to change the widget of the "productCategory" to a text widget, because the user need to write the number that is the primary key of the productCategory.
But when i do it, and complete the form, i got the following error.
Warning: spl_object_hash() expects parameter 1 to be object, string
given in
C:\xampp\htdocs\sym2\Symfony\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php
line 1358
Seems like the ORM fails reading a string of the PK, anyone have any little idea of what i must see to fix it. Thanks in advice :)
Your product entity has a relation to product category. So your form expects the category to be an entity and not a string. This is why you get the error expects parameter 1 to be object, string given.
To avoid this you can remove setDefaultOptions method. If you do so the form class will not know anymore that it is associated to a certain entity class. The pitfall of this is, that when you pass the entity to the form class it will not set the fields automatically.
However now you can enter the category id and handle it.
E.g.
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('productCategory');
}
public function getName()
{
return 'acme_hellobundle_producttype';
}
}
Now productCategory will be text widget automatically. However you will need to persist it on your own in the controller. But for this you might ask another question.
Notice, when you create the form, don't pass the product object. Have it like this
$form = $this->createForm(new ProductType(), array());

Create a textarea field in symfony2.0

How to create a textarea field in symfony2.0?Plesae provide an example also.I am new in symfony.
$builder->add('lyrics','textarea')
just add a new parameter 'textarea'
#DonCallisto
where you have to make form builder class,
class ArtistType extends AbstractType {
/**
* builds the form for a category
* #param FormBuilder $builder
* #param array $options
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('artistname')
->add('email')
->add('paypalemail')
->add('lyrics','textarea')
}
/**
* returns a unique name of the form
* #return string
*/
public function getName()
{ return 'artist_'; }
}

Symfony2 Forms and Polymorphic collections

Im playing around with Symfony2 and Im abit unsure how Symfony2 handles Polymorphic collections in the View component. It seems that i can create an entity with a collection of AbstractChildren, but not sure how to what to do with it inside a Form Type class.
For example, I have the following entity relationship.
/**
* #ORM\Entity
*/
class Order
{
/**
* #ORM\OneToMany(targetEntity="AbstractOrderItem", mappedBy="order", cascade={"all"}, orphanRemoval=true)
*
* #var AbstractOrderItem $items;
*/
$orderItems;
...
}
/**
* Base class for order items to be added to an Order
*
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({
* "ProductOrderItem" = "ProductOrderItem",
* "SubscriptionOrderItem " = "SubscriptionOrderItem "
* })
*/
class AbstractOrderItem
{
$id;
...
}
/**
* #ORM\Entity
*/
class ProductOrderItem extends AbstractOrderItem
{
$productName;
}
/**
* #ORM\Entity
*/
class SubscriptionOrderItem extends AbstractOrderItem
{
$duration;
$startDate;
...
}
Simple enough, but when im create a form for my order class
class OrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('items', 'collection', array('type' => AbstractOrderItemType()));
}
}
I am unsure how to handle this situation where you effectively need a different Form Type for each class of item in the collection?
I recently tackled a similar problem - Symfony itself makes no concessions for polymorphic collections, but it's easy to provide support for them using an EventListener to extend the form.
Below is the content of my EventListener, which uses a similar approach to Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener, the event listener which provides the collection form type's normal functionality:
namespace Acme\VariedCollectionBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class VariedCollectionSubscriber implements EventSubscriberInterface
{
protected $factory;
protected $type;
protected $typeCb;
protected $options;
public function __construct(FormFactoryInterface $factory, $type, $typeCb)
{
$this->factory = $factory;
$this->type = $type;
$this->typeCb = $typeCb;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'fixChildTypes'
);
}
public function fixChildTypes(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
// Go with defaults if we have no data
if($data === null || '' === $data)
{
return;
}
// It's possible to use array access/addChild, but it's not a part of the interface
// Instead, we have to remove all children and re-add them to maintain the order
$toAdd = array();
foreach($form as $name => $child)
{
// Store our own copy of the original form order, in case any are missing from the data
$toAdd[$name] = $child->getConfig()->getOptions();
$form->remove($name);
}
// Now that the form is empty, build it up again
foreach($toAdd as $name => $origOptions)
{
// Decide whether to use the default form type or some extension
$datum = $data[$name] ?: null;
$type = $this->type;
if($datum)
{
$calculatedType = call_user_func($this->typeCb, $datum);
if($calculatedType)
{
$type = $calculatedType;
}
}
// And recreate the form field
$form->add($this->factory->createNamed($name, $type, null, $origOptions));
}
}
}
The downside to using this approach is that for it to recognize the types of your polymorphic entities on submit, you must set the data on your form with the relevant entities before binding it, otherwise the listener has no way of ascertaining what type the data really is. You could potentially work around this working with the FormTypeGuesser system, but that was beyond the scope of my solution.
Similarly, while a collection using this system still supports adding/removing rows, it will assume that all new rows are of the base type - if you try to set them up as extended entities, it'll give you an error about the form containing extra fields.
For simplicity's sake, I use a convenience type to encapsulate this functionality - see below for that and an example:
namespace Acme\VariedCollectionBundle\Form\Type;
use Acme\VariedCollectionBundle\EventListener\VariedCollectionSubscriber;
use JMS\DiExtraBundle\Annotation\FormType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
/**
* #FormType()
*/
class VariedCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Tack on our event subscriber
$builder->addEventSubscriber(new VariedCollectionSubscriber($builder->getFormFactory(), $options['type'], $options['type_cb']));
}
public function getParent()
{
return "collection";
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired(array('type_cb'));
}
public function getName()
{
return "varied_collection";
}
}
Example:
namespace Acme\VariedCollectionBundle\Form;
use Acme\VariedCollectionBundle\Entity\TestModelWithDate;
use Acme\VariedCollectionBundle\Entity\TestModelWithInt;
use JMS\DiExtraBundle\Annotation\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
/**
* #FormType()
*/
class TestForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$typeCb = function($datum) {
if($datum instanceof TestModelWithInt)
{
return "test_with_int_type";
}
elseif($datum instanceof TestModelWithDate)
{
return "test_with_date_type";
}
else
{
return null; // Returning null tells the varied collection to use the default type - can be omitted, but included here for clarity
}
};
$builder->add('demoCollection', 'varied_collection', array('type_cb' => $typeCb, /* Used for determining the per-item type */
'type' => 'test_type', /* Used as a fallback and for prototypes */
'allow_add' => true,
'allow_remove' => true));
}
public function getName()
{
return "test_form";
}
}
In the example you have give, you would have to create different form class for those ProductOrder and SubscriptionOrder
class ProductOrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
//Form elements related to Product Order here
}
}
and
class SubsciptionOrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
//Form elements related SubscriptionOrder here
}
}
In your OrderType form class you add both these forms, like this
class OrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('product',new ProductOrderType())
$builder->add('subscription',new SubsciptionOrderType())
//Form elements related to order here
}
}
Now this adds the two forms SubsciptionOrderType,ProductOrderType to the main form OrderType . So later in the controller if you initialize this form you will get all the fields of the subscription and product forms with that of the OrderType.
I hope this answers your questions if still not clear please go through the documentation for embedding multiple forms here. http://symfony.com/doc/current/cookbook/form/form_collections.html

Resources