I hope you could help me. I am using Symfony 2.x and Doctrine 2.x and I would like to create one form consisting of two entities. By filling out this one form, I want to persist the data to two doctrine entities.
For simplicity I have made an example. A multi-lingual webshop needs to have a name and product description in English and French. I want to use one form to create a new product. This create form will include data from the Product entity (id; productTranslations; price, productTranslations) and also from the ProductTranslation entity (id; name; description; language, product). The resulting create product form has the following fields (Name; Description; Language (EN/FR); Price).
The Product and ProductTranslation entity are related to each other through a bidirectional one-to-many relationship. The owning site of the relation is the ProductTranslation.
After the form is submitted I want to persist the data to both entities (Product and ProductTranslation). Here is it where things go wrong. I cannot persist the data.
Thusfar, i have tried the following:
Product Entity:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Product
*
* @ORM\Table(name="product")
* @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
*/
class Product
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="price", type="decimal", precision=10, scale=0)
*/
private $price;
/**
* @ORM\OneToMany(targetEntity="AppBundle\Entity\ProductTranslation", mappedBy="product")
*/
private $productTranslations;
public function __construct()
{
$this->productTranslations = new ArrayCollection();
}
/**
* Get id
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set price
*
* @param string $price
*
* @return Product
*/
public function setPrice($price)
{
$this->price = $price;
return $this;
}
/**
* Get price
*
* @return string
*/
public function getPrice()
{
return $this->price;
}
/**
* Set productTranslations
*
* @param \stdClass $productTranslations
*
* @return Product
*/
public function setProductTranslations($productTranslations)
{
$this->productTranslations = $productTranslations;
return $this;
}
/**
* Get productTranslations
*
* @return \stdClass
*/
public function getProductTranslations()
{
return $this->productTranslations;
}
/**
* Add productTranslation
*
* @param \AppBundle\Entity\ProductTranslation $productTranslation
*
* @return Product
*/
public function addProductTranslation(\AppBundle\Entity\ProductTranslation $productTranslation)
{
$this->productTranslations[] = $productTranslation;
return $this;
}
/**
* Remove productTranslation
*
* @param \AppBundle\Entity\ProductTranslation $productTranslation
*/
public function removeProductTranslation(\AppBundle\Entity\ProductTranslation $productTranslation)
{
$this->productTranslations->removeElement($productTranslation);
}
}
ProductTranslation Entity:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* ProductTranslation
*
* @ORM\Table(name="product_translation")
* @ORM\Entity(repositoryClass="AppBundle\Repository\ProductTranslationRepository")
*/
class ProductTranslation
{
/**
* @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="description", type="text")
*/
private $description;
/**
* @var string
*
* @ORM\Column(name="language", type="string", length=5)
*/
private $language;
/**
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Product", inversedBy="productTranslations",cascade={"persist"})
* @ORM\JoinColumn(name="product_translation_id", referencedColumnName="id")
*
*/
private $product;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* @param string $name
*
* @return ProductTranslation
*/
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 ProductTranslation
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set language
*
* @param string $language
*
* @return ProductTranslation
*/
public function setLanguage($language)
{
$this->language = $language;
return $this;
}
/**
* Get language
*
* @return string
*/
public function getLanguage()
{
return $this->language;
}
/**
* Set product
*
* @param \AppBundle\Entity\Product $product
*
* @return ProductTranslation
*/
public function setProduct(\AppBundle\Entity\Product $product = null)
{
$this->product = $product;
return $this;
}
/**
* Get product
*
* @return \AppBundle\Entity\Product
*/
public function getProduct()
{
return $this->product;
}
}
ProductType:
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
class ProductType extends AbstractType {
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('productTranslations', ProductTranslationType::class, array('label' => false, 'data_class' => null));
$builder
->add('price', MoneyType::class)
;
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product'
));
}
}
ProductTranslationType:
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class ProductTranslationType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
->add('description', TextareaType::class )
->add('language', ChoiceType::class, array('choices' => array('en' => 'EN', 'fr' => 'FR')))
;
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\ProductTranslation'
));
}
}
ProductController:
<?php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use AppBundle\Entity\Product;
use AppBundle\Form\ProductType;
use AppBundle\Entity\ProductTranslation;
/**
* Product controller.
*
*/
class ProductController extends Controller {
/**
* Creates a new Product entity.
*
*/
public function newAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$product = new Product();
$productTranslation = new ProductTranslation();
$form = $this->createForm('AppBundle\Form\ProductType', $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$product->getProductTranslations()->add($product);
$productTranslation->setProduct($product);
$em->persist($productTranslation);
$em->flush();
return $this->redirectToRoute('product_show', array('id' => $product->getId()));
}
return $this->render('product/new.html.twig', array(
'product' => $product,
'form' => $form->createView(),
));
}
}
Error:
Warning: spl_object_hash() expects parameter 1 to be object, string given
500 Internal Server Error - ContextErrorException
I have looked at the cookbook for help: http://symfony.com/doc/current/book/forms.html#embedded-forms, however i have been unable to get it working.
Update 1
I haven't found an answer to my question yet. Following the comments below I took a look at the associations. I have made adjustments to the ProductController, which enables me to test if data gets inserted in the database the correct way. The data was inserted correctly, but I am unable to insert it trough the form. Hopefully someone can help me.
ProductController:
<?php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use AppBundle\Entity\Product;
use AppBundle\Form\ProductType;
/**
* Creates a new Product entity.
*
*/
public function newAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$product = new Product();
$productTranslation = new ProductTranslation();
/* Sample data insertion */
$productTranslation->setProduct($product);
$productTranslation->setName('Product Q');
$productTranslation->setDescription('This is product Q');
$productTranslation->setLanguage('EN');
$product->setPrice(95);
$product->addProductTranslation($productTranslation);
$em->persist($product);
$em->persist($productTranslation);
$em->flush();
/* End sample data insertion */
$form = $this->createForm('AppBundle\Form\ProductType', $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$product->getProductTranslations()->add($product);
$productTranslation->setProduct($product);
$em->persist($productTranslation);
$em->flush();
return $this->redirectToRoute('product_show', array('id' => $product->getId()));
}
return $this->render('product/new.html.twig', array(
'product' => $product,
'form' => $form->createView(),
));
}
I get the following error message now:
Expected value of type "Doctrine\Common\Collections\Collection|array" for association field "AppBundle\Entity\Product#$productTranslations", got "string" instead.
Update 2
A var_dump() from variable product in ProductController newAction before persisting the data shows:
object(AppBundle\Entity\Product)[493]
private 'id' => null
private 'price' => float 3
private 'productTranslations' =>
object(Doctrine\Common\Collections\ArrayCollection)[494]
private 'elements' =>
array (size=4)
'name' => string 'abc' (length=45)
'description' => string 'alphabet' (length=35)
'language' => string 'en' (length=2)
0 =>
object(AppBundle\Entity\ProductTranslation)[495]
...
The error is self explanatory; productTranslations
got to be an Array or an arrayCollection. It is a "string" instead.
So in constructor of Product:
public function __construct()
{
$this->activityTranslations = new ArrayCollection();
$this->productTranslations = new \Doctrine\Common\Collections\ArrayCollection();
}
For setter/getter you can use:
public function addProductTranslation(AppBundle\Entity\ProductTranslation $pt)
{
$this->productTranslations[] = $pt;
$pt->setProduct($this);
return $this;
}
public function removeProductTranslation(AppBundle\Entity\ProductTranslation $pt)
{
$this->productTranslations->removeElement($pt);
}
public function getProductTranslations()
{
return $this->productTranslations;
}
Edit: In YAML with Symfony2.3, Here is the object mapping configuration I am using (To emphasis where the cascade persist should be added).
//Product entity
oneToMany:
productTranslations:
mappedBy: product
targetEntity: App\Bundle\...Bundle\Entity\ProductTranslation
cascade: [persist]
// ProductTranslation entity
manyToOne:
product:
targetEntity: App\Bundle\..Bundle\Entity\Product
inversedBy: productTranslations
joinColumn:
name: product_id
type: integer
referencedColumnName: id
onDelete: cascade
Also, note that you no need setProductTranslation()
setter in Product entity since add
and remove
aim to replace it.
Edit2:
In Symfony2, here is how I handle forms with collections:
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('productPrice','number',array('required' => false))
->add('productTranslations', 'collection', array(
'type' => new ProducatTranslationType()
))
;
}
I don't know why you are not specifying collection in your formType. is it the new version of Symfony?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With