I have an entity Product. My product can have multiple names in different languages. A name in french, a name in english, etc. I don't want to use an automatic translation.
The user will have to write the names in the Product form and select the corresponding language. He can add so many names as he wants thanks to an Add button.
All the languages are created by the admin user (in another form). So, Language is also an Entity which have a name (ex: English) and a code (ex: EN).
So, ProductType
is my primary form, and ProductNameType
is my "collection" form.
When a user creates a new product with 2 names for example (one in french and another in english), the product is saved in my database and 2 names are also created and saved in another table.
At this point, everything works well. My addAction()
is good, the product and the corresponding names are saved in database.
But, I have a problem on my editAction()
, when I show my form pre-filled. Only the last added product name is present in my collection array...
I don't understand what I'm doing wrong. The names are in my database, the product too, so why I get only the last name in the ArrayCollection ?
Entity Product.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* @ORM\Table(name="modele")
* @ORM\Entity(repositoryClass="ProductRepository")
* @UniqueEntity(fields="code", message="Product code already exists")
*/
class Product
{
/**
* @ORM\Column(name="Modele_Code", type="string", length=15)
* @ORM\Id
* @Assert\NotBlank()
* @Assert\Length(max=15, maxMessage="The code cannot be longer than {{ limit }} characters")
*/
private $code;
/**
* @ORM\OneToMany(targetEntity="ProductNames", mappedBy="product", cascade={"persist", "remove"})
*/
private $names;
/**
* Constructor
*/
public function __construct()
{
$this->names = new ArrayCollection();
}
/**
* Set code
*
* @param string $code
*
* @return Product
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* Get code
*
* @return string
*/
public function getCode()
{
return $this->code;
}
/**
* Get names
*
* @return ArrayCollection
*/
public function getNames()
{
return $this->names;
}
/**
* Add names
*
* @param ProductNames $names
*
* @return Product
*/
public function addName(ProductNames $names)
{
$names->setCode($this->getCode());
$names->setProduct($this);
if (!$this->getNames()->contains($names)) {
$this->names->add($names);
}
return $this;
}
/**
* Remove names
*
* @param ProductNames $names
*/
public function removeName(ProductNames $names)
{
$this->names->removeElement($names);
}
}
Entity ProductNames.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* @ORM\Table(name="modele_lib")
* @ORM\Entity(repositoryClass="ModelTextsRepository")
* @UniqueEntity(fields={"code","language"}, message="A name in this language already exists for this product")
*/
class ProductNames
{
/**
* @ORM\Column(name="Modele_Code", type="string", length=15)
* @ORM\Id
*/
private $code;
/**
* @ORM\ManyToOne(targetEntity="Product", inversedBy="names")
* @ORM\JoinColumn(name="Modele_Code", referencedColumnName="Modele_Code")
*/
private $product;
/**
* @ORM\Column(name="Langue_Code", type="string", length=2)
*/
private $language;
/**
* @ORM\Column(name="Modele_Libelle", type="string", length=50)
* @Assert\NotBlank()
*/
private $name;
/**
* Set code
*
* @param string $code
*
* @return ProductNames
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* Get code
*
* @return string
*/
public function getCode()
{
return $this->code;
}
/**
* Set product
*
* @param Product $product
*
* @return ProductNames
*/
public function setProduct(Model $product)
{
$this->product = $product;
return $this;
}
/**
* Get product
*
* @return Product
*/
public function getProduct()
{
return $this->product;
}
/**
* Set language
*
* @param string $language
*
* @return ProductNames
*/
public function setLanguage($language)
{
$this->language = $language;
return $this;
}
/**
* Get language
*
* @return string
*/
public function getLanguage()
{
return $this->language;
}
/**
* Set name
*
* @param string $name
*
* @return ProductNames
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
}
Form 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\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
// use Doctrine\ORM\EntityRepository;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$recordId = $options['data']->getCode(); // product code
// default options for names
$namesOptions = array(
'entry_type' => ProductNamesType::class,
'entry_options' => array('languages' => $options['languages']),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'label' => false,
'by_reference' => false
);
// case edit product
if (!empty($recordId)) {
$namesOptions['entry_options']['edit'] = true;
}
$builder
->add('code', TextType::class, array(
'attr' => array(
'size' => 15,
'maxlength' => 15,
'placeholder' => 'Ex : LBSKIN'
),
))
->add('names', CollectionType::class, $namesOptions)
->add('save', SubmitType::class, array(
'attr' => array('class' => 'button-link save'),
'label' => 'Validate'
)
);
// Edit case : add delete button
if (!empty($recordId)) {
$builder->add('delete', SubmitType::class, array(
'attr' => array('class' => 'button-link delete'),
'label' => 'Delete'
));
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product',
'languages' => null
));
}
}
Form ProductNamesType.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\ChoiceType;
use Ivory\CKEditorBundle\Form\Type\CKEditorType;
use Doctrine\ORM\EntityRepository;
class ProductNamesType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Language codes list
$choices = array();
foreach ($options['languages'] as $lang) {
$code = $lang->getCode();
$choices[$code] = $code;
}
$builder
->add('name', TextType::class)
->add('language', ChoiceType::class, array(
'label' => 'Language',
'placeholder' => '',
'choices' => $choices
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\ProductNames',
'languages' => null,
'edit' => false
));
}
}
ProductController.php (see editAction
to find my problem).
If I print $form->getData()
or $product->getNames()
in addAction()
after form submit, I get all my data, everything is ok for me.
namespace AppBundle\Controller;
use AppBundle\Form\ProductType;
use AppBundle\Entity\Product;
use AppBundle\Entity\ProductNames;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\Common\Collections\ArrayCollection;
class ProductController extends Controller
{
/**
* @Route("/products/add", name="product_add")
*/
public function addAction(Request $request) {
// build the form
$em = $this->getDoctrine()->getManager();
$languages = $em->getRepository('AppBundle:Language')->findAllOrderedByCode();
$product = new Product();
$form = $this->createForm(ProductType::class, $product, array(
'languages' => $languages
));
// handle the submit
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// save the product
$em->persist($product);
foreach($product->getNames() as $names){
$em->persist($names);
}
$em->flush();
/*** here, everything is working ***/
// success message
$this->addFlash('notice', 'Product has been created successfully !');
// redirection
return $this->redirectToRoute('product');
}
// show form
return $this->render('products/form.html.twig', array(
'form' => $form->createView()
));
}
/**
* @Route("/products/edit/{code}", name="product_edit")
*/
public function editAction($code, Request $request) {
// get product from database
$em = $this->getDoctrine()->getManager();
$product = $em->getRepository('AppBundle:Product')->find($code);
$languages = $em->getRepository('AppBundle:Language')->findAllOrderedByCode();
// product doesn't exist
if (!$product) {
throw $this->createNotFoundException('No product found for code '. $code);
}
$originalNames = new ArrayCollection();
/*** My PROBLEM IS HERE ***/
// $product->getNames() returns only one name : the last added
foreach ($product->getNames() as $names) {
$originalNames->add($names);
}
// My form shows only one "name block" with the last name added when the user created the product.
// build the form with product data
$form = $this->createForm(ProductType::class, $product, array(
'languages' => $languages
));
// form POST
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// ...
}
// show form
return $this->render('products/form.html.twig', array(
'form' => $form->createView(),
'product_code' => $code
));
}
}
The problem may be with your ProductNames entity. You have marked code
as primary key (with @ORM\Id
), a Product will have multiple ProductNames and they all cannot have code
as primary key since a primary key needs to be unique. I would suggest using a composite primary key by adding @ORM\Id
annotation to langauge.
class ProductNames
{
/**
* @ORM\Column(name="Modele_Code", type="string", length=15)
* @ORM\Id
*/
private $code;
/**
* @ORM\Column(name="Langue_Code", type="string", length=2)
* @ORM\Id
*/
private $language;
// ...
}
You'll have to update/re-create your database for the composite key to take effect.
Hope this helps.