I have a form which works as I expect it to (code below) and I can populate this with one row from a database via Doctrine.
However, I'm struggling to see how to repeat the elements of this form to edit all rows (e.g. 20 of them) in one form; with the hope of updating all fields of all rows in a single form on a single page.
For example:
name[0]
description[0]
url[0]
name[1]
description[1]
url[1]
name[2]
description[2]
url[2]
I've see the use of collection's suggested but they don't seem to suit repeating all of the form from what I can understand.
Form:
namespace Acme\Bundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'name',
'text',
array(
'label' => 'Company Name'
)
)
->add(
'description',
'text',
array(
'label' => 'Company Description'
)
)
->add(
'url',
'text',
array(
'label' => 'Company URL'
)
)
->add(
'Update Companies',
'submit'
);
}
public function getName()
{
return 'ContactType';
}
}
Entity:
<?php
namespace Acme\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Contacts
*
* #ORM\Table(options={"engine":"MyISAM", "collate":"utf8_general_ci", "charset":"utf8"})
* #ORM\Entity
*/
class Contacts
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="text", nullable=true)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="description", type="text", nullable=true)
*/
private $description;
/**
* #var string
*
* #ORM\Column(name="url", type="text", nullable=true)
*/
private $url;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Contacts
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
* #return Contacts
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set url
*
* #param string $url
* #return Contacts
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get url
*
* #return string
*/
public function getUrl()
{
return $this->url;
}
}
I think the collections will suit your need.
Create a form class which have one field, the collection of your ContactType :
class MyFormClassType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'myField',
'collection',
array(
'type' => new ContactType ()
)
);
}
public function getName()
{
return 'my_class';
}
}
With this you can add, in the same form, a collection of Contact !
Read More, from the doc
Related
I have 2 entities, Exercise and Product, they have a One to One relationship (the owning side is Product), and I have an ExerciseFormType and a ProductFormType.
What I want I to be able to create an exercise, and the product associated with it at the same time.
My idea was to add the the builder of exerciseFormType, 'product' and its type would be a ProductFormType. The problem is that the product needs the exercise_id, and the exercise isn't persisted yet so it doesn't have one. Here's what I have so far (it works only for creating a product when the exercise exists).
class ExerciseType extends AbstractType {
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name')
->add('content')
->add('language')
->add('level')
->add('skillsRequired')
->add('skillsTargetted')
->add('tags')
->add('product', ProductType::class, array(
'required' => false,
'exercise'=> $builder->getData()
)
);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver){
$resolver->setDefaults(array(
'data_class' => 'SchoolBundle\Entity\Exercise'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix(){
return 'schoolbundle_exercise';
}
class ProductType extends AbstractType {
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('visibility')->add('price', MoneyType::class);
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$product = $event->getData();
$form = $event->getForm();
$field = (!$product || null === $product->getId()) ? 'publicationDate' : 'updateDate';
$form->add($field, DateTimeType::class, array('data' => new \DateTime()));
});
$data = array(
'data' => $options['exercise'],
'class' => Exercise::class,
'choice_label' => 'getName'
);
$builder->add('exercise', EntityType::class, $data);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver){
$resolver->setDefaults(array(
'data_class' => 'MarketBundle\Entity\Product'
));
$resolver->setRequired(array(
'exercise'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix(){
return 'marketbundle_product';
}
}
In exercise I have that
<?php
/**
* exercise
*
* #ORM\Table(name="exercise")
* #ORM\Entity(repositoryClass="SchoolBundle\Repository\ExerciseRepository")
*/
class Exercise {
/**
* #var int
* #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="content", type="string", length=255)
*/
private $content;
/**
* #var string
* #ORM\Column(name="language", type="string", length=255)
*/
private $language;
/**
* #var int
* #ORM\Column(name="level", type="integer")
*/
private $level;
/**
* #var string
* #ORM\Column(name="skills_required", type="string", length=255)
*/
private $skillsRequired;
/**
* #var string
* #ORM\Column(name="skills_targetted", type="string", length=255)
*/
private $skillsTargetted;
/**
* #var string
* #ORM\Column(name="tags", type="string", length=255)
*/
private $tags;
/**
* #ORM\OneToMany(targetEntity="Session", mappedBy="exercise")
*/
private $sessions;
/**
* #ORM\OneToMany(targetEntity="ExerciseIO", mappedBy="exercise")
*/
private $exerciseIOs;
/**
* #ORM\OneToOne(targetEntity="MarketBundle\Entity\Product", mappedBy="exercise", cascade={"persist"}))
*/
private $product;
/**
* #ORM\ManyToOne(targetEntity="Professor", inversedBy="createdExercises")
* #ORM\JoinColumn(name="creator_id", referencedColumnName="id", nullable=false)
*/
private $creator;
/**
* #ORM\ManyToMany(targetEntity="Professor", mappedBy="boughtExercises")
*/
private $owners;
public function __construct(){
$this->sessions = new ArrayCollection();
$this->exerciseIOs = new ArrayCollection();
$this->owners = new ArrayCollection();
}
/**
* Get creator
* #return Professor
*/
public function getCreator(){
return $this->creator;
}
/**
* Set creator
* #param Professor $creator
* #return Exercise
*/
public function setCreator($creator){
$this->creator = $creator;
return $this;
}
/**
* Get owner
* #return ArrayCollection
*/
public function getOwners(){
return $this->owners;
}
/**
* Get product
* #return Product
*/
public function getProduct(){
return $this->product;
}
/**
* #param Product $product
* #return Exercise
*/
public function setProduct($product){
$this->product = $product;
return $this;
}
/**
* Get exerciseIOs
* #return ArrayCollection
*/
public function getExerciseIOs(){
return $this->exerciseIOs;
}
/**
* Get sessions
* #return ArrayCollection
*/
public function getSessions(){
return $this->sessions;
}
/**
* Get id
* #return int
*/
public function getId(){
return $this->id;
}
/**
* Set name
* #param string $name
* #return Exercise
*/
public function setName($name){
$this->name = $name;
return $this;
}
/**
* Get name
* #return string
*/
public function getName(){
return $this->name;
}
/**
* Set content
* #param string $content
* #return Exercise
*/
public function setContent($content){
$this->content = $content;
return $this;
}
/**
* Get content
* #return string
*/
public function getContent(){
return $this->content;
}
/**
* Set language
* #param string $language
* #return Exercise
*/
public function setLanguage($language){
$this->language = $language;
return $this;
}
/**
* Get language
* #return string
*/
public function getLanguage(){
return $this->language;
}
/**
* Set level
* #param string $level
* #return Exercise
*/
public function setLevel($level){
$this->level = $level;
return $this;
}
/**
* Get level
* #return string
*/
public function getLevel(){
return $this->level;
}
/**
* Set skillsRequired
* #param string $skillsRequired
* #return Exercise
*/
public function setSkillsRequired($skillsRequired){
$this->skillsRequired = $skillsRequired;
return $this;
}
/**
* Get skillsRequired
* #return string
*/
public function getSkillsRequired(){
return $this->skillsRequired;
}
/**
* Set skillsTargetted
* #param string $skillsTargetted
* #return Exercise
*/
public function setSkillsTargetted($skillsTargetted){
$this->skillsTargetted = $skillsTargetted;
return $this;
}
/**
* Get skillsTargetted
* #return string
*/
public function getSkillsTargetted(){
return $this->skillsTargetted;
}
/**
* Set tags
* #param string $tags
* #return Exercise
*/
public function setTags($tags){
$this->tags = $tags;
return $this;
}
/**
* Get tags
* #return string
*/
public function getTags(){
return $this->tags;
}
}
And in Product
/**
* Product
*
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="\MarketBundle\Repository\ProductRepository")
*/
class Product {
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var bool
* #ORM\Column(name="visibility", type="boolean")
*/
private $visibility;
/**
* #var float
* #ORM\Column(name="price", type="float")
*/
private $price;
/**
* #var \DateTime
* #ORM\Column(name="publication_date", type="datetimetz")
*/
private $publicationDate;
/**
* #var \DateTime
* #ORM\Column(name="update_date", type="datetimetz", nullable=true)
*/
private $updateDate;
/**
* #ORM\OneToMany(targetEntity="ProductComment", mappedBy="product")
*/
private $productComments;
/**
* #ORM\OneToOne(targetEntity="\SchoolBundle\Entity\Exercise", inversedBy="product")
* #ORM\JoinColumn(name="exercise_id", referencedColumnName="id", nullable=false)
*/
private $exercise;
public function __construct(){
$this->productComments = new ArrayCollection();
}
/**
* Get exercise
* #return Exercise
*/
public function getExercise(){
return $this->exercise;
}
/**
* Set exercise
* #param Exercise $exercise
* #return Product
*/
public function setExercise($exercise){
$this->exercise = $exercise;
return $this;
}
/**
* Get productComments
* #return ArrayCollection
*/
public function getProductComments(){
return $this->productComments;
}
/**
* Get id
* #return int
*/
public function getId(){
return $this->id;
}
/**
* Set visibility
* #param boolean $visibility
* #return Product
*/
public function setVisibility($visibility){
$this->visibility = $visibility;
return $this;
}
/**
* Get visibility
* #return bool
*/
public function getVisibility(){
return $this->visibility;
}
/**
* Set price
* #param float $price
* #return Product
*/
public function setPrice($price){
$this->price = $price;
return $this;
}
/**
* Get price
* #return float
*/
public function getPrice(){
return $this->price;
}
/**
* Set publicationDate
* #param \DateTime $publicationDate
* #return Product
*/
public function setPublicationDate($publicationDate){
$this->publicationDate = $publicationDate;
return $this;
}
/**
* Get publicationDate
* #return \DateTime
*/
public function getPublicationDate(){
return $this->publicationDate;
}
/**
* Set updateDate
* #param \DateTime $updateDate
* #return Product
*/
public function setUpdateDate($updateDate){
$this->updateDate = $updateDate;
return $this;
}
/**
* Get updateDate
* #return \DateTime
*/
public function getUpdateDate(){
return $this->updateDate;
}
}
I've said it works for an existing exercise (and no product).
When I try to create both at the same time I get
Entities passed to the choice field must be managed. Maybe persist them in the entity manager?
It's probably from the $builder->getData(), because in that case I only passed to the form a "new Exercice()". Here's the controller action
public function newAction(Request $request){
$exercise = new Exercise();
$form = $this->createForm(ExerciseType::class, $exercise);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$exercise->setCreator($this->getUser());
$em = $this->getDoctrine()->getManager();
$em->persist($exercise);
$em->flush();
return $this->redirectToRoute('exercise_show', array('id' => $exercise->getId()));
}
(Also this is another question but when I disable a formType, it is rendered, and the value appears but it's not submitted. I would actually prefer a hiddenFormType but I need the formatting of the DateTimeFormType)
Ok, this is the solution I've made once with Symfony2.8, I hope it can helps you.
/**
* #ORM\Entity
* #ORM\Table(name="picture")
*/
class Picture
{
/**
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Host",
* inversedBy="pictureId"
* )
* #ORM\JoinColumn(name="avatar_host_id", referencedColumnName="id", nullable=true)
*/
private $avatarHostId;
}
/**
* #ORM\Entity
* #ORM\Table(name="host")
*/
class Host
{
/**
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Picture",
* mappedBy="avatarHostId", cascade={"persist", "remove", "merge"}
* )
*/
private $pictureId;
}
class HostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('pictureId', PictureFileType::class);
}
}
class PictureFileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('assetFile', FileType::class, array(
'label' => 'Picture File',
'required' => false,
'validation_groups' => array('creation')
));
}
}
First, I recommend moving away from using integers as IDs. There are some issues around them, including that you can't really generate one at will in this kind of scenario. I would recommend Uuids along with ramsey/uuid, which also has a Doctrine mapping.
When you've got a situation where you need an ID before it's been persisted and the Uuid generated, generate the Uuid yourself. For instance, in a contrived example
class Order
{
/**
* #var UuidInterface
*
* #ORM\Id
* #ORM\Column(type="uuid")
* #ORM\GeneratedValue(strategy="NONE")
* #ORM\CustomIdGenerator(class=UuidGenerator::class)
*/
private $id;
...
/**
* Constructor.
*
* #param UuidInterface $id
*/
public function __construct(?UuidInterface $id = null)
{
$this->id = $id ?? Uuid::uuid4();
}
...
}
class OrderController
{
...
public function orderProductAction(): JsonResponse
{
...
// We need this ahead of time, because
// maybe this is Async and we need to
// return the order id. This happens a
// lot with commands.
$generatedId = Uuid::uuid4();
// Here it's going to use the given ID,
// if not given, the Order class will
// generate it.
$order = new Order($generatedId, ...);
// Stuff happens, persisted... maybe we
// don't have the persisted Order object...
return $this->json([
'order' => [
'id' => $generatedId->toString(),
],
]);
}
...
}
The meat of it is providing the generated Uuid in the constructor (which alternately generates it if null). Do not add a setter. Here's a more involved example:
public function createAction(Request $request): JsonResponse
{
$request_data = \json_decode($request->getContent(), true);
$tenant_data = $request_data['data'];
// Pre-generate our Uuid for the Tenant.
$generated_id = Uuid::uuid4();
/** #var CreateTenant $create_tenant_cmd */
$create_tenant_cmd = new CreateTenant($generated_id);
$create_tenant_cmd->setName($tenant_data['name']);
$create_tenant_cmd->setDomainName($tenant_data['domain_name']);
$create_tenant_cmd->setOwnerUsername($tenant_data['owner_username']);
$create_tenant_cmd->setOwnerPassword($tenant_data['owner_password']);
$create_tenant_cmd->setOwnerFirstName($tenant_data['owner_first_name']);
$create_tenant_cmd->setOwnerLastName($tenant_data['owner_last_name']);
$create_tenant_cmd->setOwnerEmail($tenant_data['owner_email']);
if (!$this->isGranted('perform', $create_tenant_cmd)) {
throw new AccessDeniedException('Access denied to create tenant.');
}
// Here, we should expect NO side-effects, the command
// can't return anything. So no Tenant object with
// generated Uuid...
$this->get('command_bus')->handle($create_tenant_cmd);
/* #var Serializer $serializer */
$serializer = $this->get('jms_serializer');
$serializer_context = SerializationContext::create()->setGroups([
'tenant_meta',
]);
$tenant = $this->get('app.repository.tenant')->find($generated_id);
return $this->json(\json_decode($serializer->serialize(
$tenant,
'json',
$serializer_context
)));
}
That's the ID question; the second part of your question is now easily resolved, that being giving the Uuid to the FormType instead:
$form = $this->createForm(OrderProductType::class, $product, [
'product' => $product,
'generatedId' => $generatedId,
]);
class OrderProductType extends AbstractType implements DataMapperInterface
{
/** #var UuidInterface */
protected $generatedId;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->generatedId = $options['generatedId'];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'data_class' => OrderProduct::class,
])
->setRequired(['generatedId'])
;
}
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
$data = new OrderProduct($this->generatedId);
...
}
}
Of course, if you need it as a field, use it with the field type of your choice instead of storing it as a variable, and then get it from that field:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product_id', HiddenType::class, [
'product_id' => $options['generatedId']->toString(),
])
...
;
}
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
// Note use of Uuid::fromString().
$data = new OrderProduct(Uuid::fromString(
$forms['productId']->getData()
));
...
}
This is one of the downsides of not delegating the process of interaction to a delegate object like a command, instead of generating an entity; you're stuck constructing your object before you need it, in whole, or perverting the design to make it fit. It also makes working with assertions harder as well (you only get one set, the entity's).
I have symfony form with choice field. I want to load choices from my entity class static method. can i use data or CallbackChoiceLoader? what is the best practise?
this is my field:
class CarType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type', ChoiceType::class, [
// This choices I would like to load from:
// 'choices' => $car::getTypes(),
'choices' => [
'Critical' => 'critical',
'Medium' => 'medium',
'Info' => 'info',
],
'label' => 'Type'
])
->add('name', TextType::class, [
'label' => 'Name'
])
->add('save', SubmitType::class, [
'label' => 'Save'
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Car'
]);
}
public function getName()
{
return 'car';
}
}
This is my entity:
/**
* #ORM\Entity
* #ORM\Table(name="car")
*/
class Car
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string $type
*
* #Assert\NotBlank()
* #ORM\Column(name="type")
*/
private $type;
/**
* #var string $name
*
* #Assert\NotBlank()
* #ORM\Column(name="name")
*/
private $name;
/**
* #var \DateTime $created
*
* #ORM\Column(name="created", type="datetime")
*/
private $created;
private static $types = ['main', 'custom'];
public function __construct()
{
$this->created = new \DateTime('now', new \DateTimeZone('Europe/Ljubljana'));
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get types
*
* #return array
*/
public function getTypes()
{
return self::$types;
}
/**
* Set type
*
* #param string $type
*
* #return Car
*/
public function setType($type)
{
if (in_array($type, $this->getTypes())) {
$this->type = $type;
}
return $this;
}
/**
* Get type
*
* #return string
*/
public function getType()
{
return $this->type;
}
/**
* Set name
*
* #param string $name
*
* #return Car
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set created
*
* #param \DateTime $created
*
* #return Car
*/
public function setCreated($created)
{
$this->created = $created;
return $this;
}
/**
* Get created
*
* #return \DateTime
*/
public function getCreated()
{
return $this->created;
}
How and what can i use to load choices from $car::getTypes() method like i commented in form so that the choices are loaded dynamicly based on values in getTypes entity method?
Edit: This is option 1. Option 2 below more directly answers the question.
The preferred method of creating a choice form field from an entity is to use the EntityType field. Here is an example from an application that requires an ethnicity field. Below that is the Ethnicity entity.
form field:
->add('ethnicity', EntityType::class, array(
'label' => 'Ethnicity:',
'class' => 'AppBundle:Ethnicity',
'choice_label' => 'abbreviation',
'expanded' => false,
'placeholder' => 'Select ethnicity',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('e')
->orderBy('e.abbreviation', 'ASC')
;
},
))
Ethnicity entity:
/**
* Ethnicity.
*
* #ORM\Table(name="ethnicity")
* #ORM\Entity
*/
class Ethnicity
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="ethnicity", type="string", length=45, nullable=true)
*/
protected $ethnicity;
/**
* #var string
*
* #ORM\Column(name="abbr", type="string", length=45, nullable=true)
*/
protected $abbreviation;
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Member", mappedBy="ethnicity", cascade={"persist"})
*/
protected $members;
/**
* Constructor.
*/
public function __construct()
{
$this->members = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id.
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set ethnicity.
*
* #param string $ethnicity
*
* #return Ethnicity
*/
public function setEthnicity($ethnicity)
{
$this->ethnicity = $ethnicity;
return $this;
}
/**
* Get ethnicity.
*
* #return string
*/
public function getEthnicity()
{
return $this->ethnicity;
}
/**
* Set abbreviation.
*
* #param string $abbreviation
*
* #return Ethnicity
*/
public function setAbbreviation($abbreviation)
{
$this->abbreviation = $abbreviation;
return $this;
}
/**
* Get abbreviation.
*
* #return string
*/
public function getAbbreviation()
{
return $this->abbreviation;
}
/**
* Add members.
*
* #param \AppBundle\Entity\Member $members
*
* #return Ethnicity
*/
public function addMember(\AppBundle\Entity\Member $members)
{
$this->members[] = $members;
return $this;
}
/**
* Remove members.
*
* #param \AppBundle\Entity\Member $members
*/
public function removeMember(\Truckee\ProjectmanaBundle\Entity\Member $members)
{
$this->members->removeElement($members);
}
/**
* Get members.
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getMembers()
{
return $this->members;
}
/**
* #var bool
*
* #ORM\Column(name="enabled", type="boolean", nullable=true)
*/
protected $enabled;
/**
* Set enabled.
*
* #param bool $enabled
*
* #return enabled
*/
public function setEnabled($enabled)
{
$this->enabled = $enabled;
return $this;
}
/**
* Get enabled.
*
* #return bool
*/
public function getEnabled()
{
return $this->enabled;
}
}
Option 2:
If you really want to do that, then here's an approach:
Modify your static property $types such that the choices are values in an array, e.g., $types = array(1 => 'main', 2 => 'custom')
In your controller, add a use statement for the Car entity.
In controller action:
$car = new Car();
$types = $car->getTypes;
Use the answer to Passing data to buildForm() in Symfony 2.8/3.0 to see how to pass $types to your form.
I was following the symfony2 tutorial on how to create an embedded form collection but wasn't able to implement it since it only creates a junction table.
According to doctrine2 documentation:
"Why are many-to-many associations less common? Because frequently you want to associate additional attributes with an association, in which case you introduce an association class. Consequently, the direct many-to-many association disappears and is replaced by one-to-many/many-to-one associations between the 3 participating classes."
Here are some snippets of my code:
src/AppBundle/Entity/Ingredient.php
/**
* Defines the properties of the Ingredient entity to represent the portal ingredients.
*
* #author furious_snail
*
* #ORM\Entity()
* #ORM\Table(name="ingredients")
* #UniqueEntity("name")
*/
class Ingredient
{
/**
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="IngredientCategory")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=true)
*/
private $category;
/**
* #ORM\Column(type="string", unique=true)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="IngredientNutrient", mappedBy="ingredient", cascade={"persist", "remove"})
*/
private $nutrientsPer100G;
public function __construct()
{
$this->substitute = new ArrayCollection();
$this->nutrientsPer100G = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
/**
* #param mixed $nutrientsPer100G
*/
public function setNutrientsPer100G($nutrientsPer100G)
{
$this->nutrientsPer100G = $nutrientsPer100G;
}
/**
* #return array
*/
public function getNutrientsPer100G()
{
return $this->nutrientsPer100G;
}
}
src/AppBundle/Entity/IngredientNutrient.php
/**
* #ORM\Entity()
* #ORM\Table(name="ingredient_nutrient")
*/
class IngredientNutrient
{
/**
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="Ingredient", inversedBy="nutrientsPer100G")
* #ORM\JoinColumn(name="ingredient_id", referencedColumnName="id", nullable=true)
*/
protected $ingredient;
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="Nutrient")
* #ORM\JoinColumn(name="nutrient_id", referencedColumnName="id", nullable=true)
*/
protected $nutrient;
/**
* #ORM\Column(type="float")
*/
private $quantity;
/**
* #ORM\ManyToOne(targetEntity="Unit")
* #ORM\JoinColumn(name="unit_id", referencedColumnName="id", nullable=true)
*/
private $unit;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getIngredient()
{
return $this->ingredient;
}
/**
* #param mixed $ingredient
*/
public function setIngredient($ingredient)
{
$this->ingredient = $ingredient;
}
/**
* #return mixed
*/
public function getNutrient()
{
return $this->nutrient;
}
/**
* #param mixed $nutrient
*/
public function setNutrient($nutrient)
{
$this->nutrient = $nutrient;
}
/**
* #return mixed
*/
public function getQuantity()
{
return $this->quantity;
}
/**
* #param mixed $quantity
*/
public function setQuantity($quantity)
{
$this->quantity = $quantity;
}
/**
* #return mixed
*/
public function getUnit()
{
return $this->unit;
}
/**
* #param mixed $unit
*/
public function setUnit($unit)
{
$this->unit = $unit;
}
}
src/AppBundle/Form/Type/IngredientNutrientType.php
class IngredientNutrientType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nutrient', 'entity', array(
'class' => 'AppBundle\Entity\Nutrient',
'choice_label' => 'name',
'label' => 'Nutrient',
))
->add('quantity', null, array('label' => 'Cantitate'))
->add('unit', 'entity', array(
'class' => 'AppBundle\Entity\Unit',
'choice_label' => 'unit',
'label' => 'Unitate de masura'
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\IngredientNutrient',
));
}
public function getName()
{
return 'app_ingredient_nutrient';
}
}
src/AppBundle/Form/Type/IngredientType.php
class IngredientType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', null, array('label' => 'Name'))
->add('nutrients_per_100_g', 'collection', array(
'type' => new IngredientNutrientType(),
'allow_add' => true,
'label' => 'Nutrient quantity per 100g',
'options' => array('label' => false),
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Ingredient',
));
}
public function getName()
{
return 'app_ingredient';
}
}
This works, I do get an embedded form collection but the issue is that the ingredient_id in the ingredient_nutrient table is null. How do I make it to fill the table with the right ID?
These are the fields I get on the page:
Name:
Nutrient:
Quantity:
Unit:
The idea is that if I have IngredientNutrient form tied with Ingredient form the user shouldn't have to specify ingredient name twice.
Thank you.
To start with, you need to "cross reference" your entities:
public function setNutrientsPer100G($nutrientsPer100G)
{
$this->nutrientsPer100G = $nutrientsPer100G;
$nutrientsPer100G->setIngrediant($this);
}
Make sure both sides of the relation are being set. That will take care of the null id issues.
The other problem is that you are using collections in your Ingrediant/Nutrient entities but your set methods are not using the array operators.
I have an Employee class which has a OneToMany relation to the PhoneNumber class, and I'm using form builders, and using prototypes to embed multiple phone numbers into the New Employee form, with javascript.
From dumping my employee variable, I see that each submitted PhoneNumber is represented as an array, when I suppose it should be converted to a PhoneNumber object when the submitted data is processed.
My entitites are:
/**
* Employee
*
* #ORM\Table(uniqueConstraints={#UniqueConstraint(name="employee_username_idx", columns={"username"})})
* #ORM\Entity(repositoryClass="Acme\BambiBundle\Entity\EmployeeRepository")
*/
class Employee
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var array
*
* #OneToMany(targetEntity="PhoneNumber", mappedBy="employee", cascade={"persist","remove"}, fetch="EAGER")
**/
private $phoneNumbers;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Constructor
*/
public function __construct()
{
$this->phoneNumbers = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add phoneNumbers
*
* #param \Acme\BambiBundle\Entity\PhoneNumber $phoneNumbers
* #return Employee
*/
public function addPhoneNumber(\Acme\BambiBundle\Entity\PhoneNumber $phoneNumbers)
{
$phoneNumbers->setEmployee($this);
$this->phoneNumbers[] = $phoneNumbers;
return $this;
}
/**
* Remove phoneNumbers
*
* #param \Acme\BambiBundle\Entity\PhoneNumber $phoneNumbers
*/
public function removePhoneNumber(\Acme\BambiBundle\Entity\PhoneNumber $phoneNumbers)
{
$this->phoneNumbers->removeElement($phoneNumbers);
}
/**
* Get phoneNumbers
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPhoneNumbers()
{
return $this->phoneNumbers;
}
// ...
}
and
/**
* PhoneNumber
*
* #ORM\Table()
* #ORM\Entity
*/
class PhoneNumber
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="type", type="text", nullable=true)
*/
private $type;
/**
* #var string
*
* #ORM\Column(name="number", type="text")
*/
private $number;
/**
* #var Employee
*
* #ManyToOne(targetEntity="Employee", inversedBy="phoneNumbers")
* #JoinColumn(name="employee_id", referencedColumnName="id", onDelete="CASCADE")
**/
private $employee;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set number
*
* #param string $number
* #return PhoneNumber
*/
public function setNumber($number)
{
$this->number = $number;
return $this;
}
/**
* Get number
*
* #return string
*/
public function getNumber()
{
return $this->number;
}
/**
* Set employee
*
* #param \Acme\BambiBundle\Entity\Employee $employee
* #return PhoneNumber
*/
public function setEmployee(\Acme\BambiBundle\Entity\Employee $employee = null)
{
$this->employee = $employee;
return $this;
}
/**
* Get employee
*
* #return \Acme\BambiBundle\Entity\Employee
*/
public function getEmployee()
{
return $this->employee;
}
/**
* Set type
*
* #param string $type
* #return PhoneNumber
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* Get type
*
* #return string
*/
public function getType()
{
return $this->type;
}
}
My form types are:
class EmployeeType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('phoneNumbers', 'collection', array(
'type' => new PhoneNumberType(),
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'prototype_name' => 'phoneNumberPrototype',
'by_reference' => false,
))
// ...
->add('save', 'submit', array('attr' => array('class' => 'btn btnBlue')));
}
public function getName() {
return 'employee';
}
}
and
class PhoneNumberType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('type', 'text', array('required' => false, 'label' => 'Type (optional)'))
->add('number', 'text');
}
public function getName() {
return 'phoneNumber';
}
}
Are there any things I could try to solve the problem? Is there anything that I am obviously doing wrong?
The solution was in a detail that I didn't know about - setting a data_class in the PhoneNumberType class:
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'Acme\BambiBundle\Entity\PhoneNumber',
));
}
Without this, the code that processes the data from the submitted form does not know that each phone number should be converted to a PhoneNumber class, and thus they stay as arrays.
Please bear in mind that I am using Symfony 2.6. In Symfony 2.7 the above function is called configureOptions and has a slightly different definition:
public function configureOptions(OptionsResolver $resolver) {
i'm looking for nice way to persist 2 objects to db via doctrine in symfony 2.3
class CatController extends Controller
{
/**
* Creates a new Cat entity.
*
*/
public function createAction(Request $request)
{
$entity = new Cat();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity); <-- Split it or what ?
$em->flush();
return $this->redirect($this->generateUrl('cat_show', array('id' => $entity->getId())));
}
return $this->render('ViszmanCatBundle:Cat:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
when form is validated i can get post data and create 2 objects with that data but i think there should be clearer way to do this, above code is working not as i wanted, it only inserts foreign key to related entity when i do this:
class CatType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text')
->add('meetings','collection',
array('type' => new MeetingType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
)
);
}
class MeetingType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$plDays = array('Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota', 'Niedziela');
$builder
->add('meetingDay', 'choice', array('choices' => $plDays))
->add('meetingTime', 'time',)
->add('cat', 'entity', array('class' => 'ViszmanCatBundle:Cat', 'property' => 'name'))
;
}
entities: Cat
namespace Viszman\CatBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Cat
*
* #ORM\Table()
* #ORM\Entity
*/
class Congregation
{
/**
* #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;
/**
* #ORM\OneToMany(targetEntity="Viszman\CatBundle\Entity\Member", mappedBy="cat")
*/
private $members;
/**
* #ORM\OneToMany(targetEntity="Viszman\CatBundle\Entity\Meeting", mappedBy="cat", cascade={"persist"})
*/
private $meetings;
public function __construct(){
$this->members = new \Doctrine\Common\Collections\ArrayCollection();
$this->meetings = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Cat
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Add members
*
* #param \Viszman\CatBundle\Entity\Member $members
* #return Cat
*/
public function addMember(\Viszman\CatBundle\Entity\Member $members)
{
$this->members[] = $members;
return $this;
}
/**
* Remove members
*
* #param \Viszman\CatBundle\Entity\Member $members
*/
public function removeMember(\Viszman\CatBundle\Entity\Member $members)
{
$this->members->removeElement($members);
}
/**
* Get members
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getMembers()
{
return $this->members;
}
/**
* Add meetings
*
* #param \Viszman\CationBundle\Entity\Meeting $meetings
* #return Cat
*/
public function addMeeting(\Viszman\CatBundle\Entity\Meeting $meetings)
{
$this->meetings[] = $meetings;
return $this;
}
/**
* Remove meetings
*
* #param \Viszman\CatBundle\Entity\Meeting $meetings
*/
public function removeMeeting(\Viszman\CatBundle\Entity\Meeting $meetings)
{
$this->meetings->removeElement($meetings);
}
/**
* Get meetings
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getMeetings()
{
return $this->meetings;
}
namespace Viszman\CatBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Meeting
*
* #ORM\Table()
* #ORM\Entity
*/
class Meeting
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer
*
* #ORM\Column(name="meeting_day", type="smallint")
*/
private $meetingDay;
/**
* #var \DateTime
*
* #ORM\Column(name="meeting_time", type="time")
*/
private $meetingTime;
/**
* #ORM\ManyToOne(targetEntity="Viszman\CatBundle\Entity\Cat", inversedBy="meetings")
* #ORM\JoinColumn(name="cat_id", referencedColumnName="id")
*/
private $cat;
public function __construct()
{
$this->created = new \DateTime();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set meetingDay
*
* #param integer $meetingDay
* #return Meeting
*/
public function setMeetingDay($meetingDay)
{
$this->meetingDay = $meetingDay;
return $this;
}
/**
* Get meetingDay
*
* #return integer
*/
public function getMeetingDay()
{
return $this->meetingDay;
}
/**
* Set cat
*
* #param \Viszman\CatBundle\Entity\Cat $cat
* #return Member
*/
public function setCat(\Viszman\CatBundle\Entity\Cat $cat = null)
{
$this->cat = $cat;
return $this;
}
/**
* Get cat
*
* #return \stdClass
*/
public function getCat()
{
return $this->cat;
}
/**
* Set meetingTime
*
* #param \DateTime $meetingTime
* #return Meeting
*/
public function setMeetingTime($meetingTime)
{
$this->meetingTime = $meetingTime;
return $this;
}
/**
* Get meetingTime
*
* #return \DateTime
*/
public function getMeetingTime()
{
return $this->meetingTime;
}
this generate embedded form with unwanted data, meaning in Meeting section i need to choice Cat, but i dont want to, what i want is that meeting is on default attached to Cat on create, update. Do i need to change something in Cat or Meeting Entity? I don't know if i'm clear, sorry for my poor english
You need to remove this line in the MeetingType:
->add('cat', 'entity', array('class' => 'ViszmanCatBundle:Cat', 'property' => 'name'))
Then in yout Controller persist your Cat entity and yout Meeting entity (which you can find using cat's getMeetings method).
If you want both to be persisted in one shot, take a look at the cascade operation for Doctrine entities.