6

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]
              ...
6
  • First you have to correct entity mappings doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/… and setup correct association management methods doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/… than try to persist a product with translations in a controller and if it's work than you can move on to use forms. Commented Jun 2, 2016 at 22:18
  • @1ed I think you were refering to the mistake i made with the ProductTranslation entity. I forgot to add the many-to-one annotation for product. However, the result is still the same. Commented Jun 2, 2016 at 22:34
  • Did you just make a change to the mappings? If so you'll need to run php app/console doctrine:schema:update --force. Otherwise, I have no other suggestion. The error message doesn't show much. What happens when you use the "app_dev.php" appended to your URL? Commented Jun 2, 2016 at 23:06
  • @AlvinBunk I have run this command and I am in development mode. But maybe, you could help me how i should set this up in general. Commented Jun 2, 2016 at 23:07
  • Can you check the var/logs/dev.log file and see if there is anything there when you get the error? Commented Jun 2, 2016 at 23:09

3 Answers 3

5
+25

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?

Sign up to request clarification or add additional context in comments.

9 Comments

My mistake, I had already spotted this and corrected it. However, I forgot to update the question. Could you please take a look.
I have taken a look at your edit. Do you mean you get the same error even after the changes?
Yes I get the same error. The error message suggests that it is a array problem, but I don't understand why it gets converted into a string. I think the problem is in the productcontroller after the validation. The object gets converted into a string.
Any help specific to my case or and explanation how you would do this is much appreciated.
okey, I am trying to reread your code more carefully. Just wanted to tell you additionnaly that this part of Symfony (Embedding collections in forms) is not an easy part. I remember I have spent a lot of time in a project before creating a stable solution especially when it concerns dynamic adding of child entities with JS. I will be back to give my thoughs. Good luck
|
2

I was looking through your controller and I notice that you are persisting ProductTranslation, however in the ProductTranslation entity you are missing the annotation for cascade={"persist"} on the relationship to the Product entity. It needs to be specified on the entity you are persisting if you want it to save related entities.

1 Comment

I made a mistake. In current version I have cascade={"persist"}, but this doesn't change the error.
2
$product->getProductTranslations()->add($product);
$productTranslation->setProduct($product);

Idk what you want to do here, but I believe that you need to use:

$product->addProductTranslation($productTranslation);
$productTranslation->setProduct($product);

$product->getProductTranslations() returns an ArrayCollection of class 'ProductTranslation' and you are merging that array with the value of type 'Product'.

It's a little inconsistent, If I'm wrong could you to say me what you are doing in that sentence?

Thanks!

1 Comment

Thank you for your input. Don't worry about my code. The problem remains if i persist the data. I have made a var_dump of my product variable. I hope it helps to solve my problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.