Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 embedded form doesn't persist data to database

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]
              ...
like image 706
Mr. Radical Avatar asked Jun 02 '16 21:06

Mr. Radical


1 Answers

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?

like image 105
Adib Aroui Avatar answered Sep 19 '22 23:09

Adib Aroui