I am creating an E-Commerce Bundle with Symfony2 and Doctrine2. I am applying EAV approach for the product features and product values for unlimited features. For this, I have three basic entities: Product, FeatureKind and FeatureValues.
The Problem is the I need the FeatureType as labels and it's various values as a choice field in the product form. I have managed to get the featurekind and associated values in the product form but I don't know how to turn them into choice fields.
Following are all three Entities, Controller and Form Code and the result of my code.
Note: I have removed the extra things from code to keep it short.
Product.php
namespace Webmuch\ProductBundle\Entity;
/**
* @ORM\Table()
* @ORM\Entity
*/
class Product
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* @ORM\ManyToMany(targetEntity="FeatureKind", inversedBy="product", cascade={"persist"})
* @ORM\JoinTable(name="product_featurekind")
**/
private $featurekind;
}
FeatureKind.php
namespace Webmuch\ProductBundle\Entity;
/**
* @ORM\Table(name="feature_kind")
* @ORM\Entity
*/
class FeatureKind
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(name="name", type="string", length=50)
*/
protected $name;
/**
* @ORM\ManyToMany(targetEntity="FeatureValue")
* @ORM\JoinTable(name="feature_kind_value",
* joinColumns={@ORM\JoinColumn(name="kind_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="value_id", referencedColumnName="id", unique=true)}
* )
**/
protected $values;
}
FeatureValue.php
namespace Webmuch\ProductBundle\Entity;
/**
* @ORM\Table()
* @ORM\Entity
*/
class FeatureValue
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(name="value", type="string", length=100)
*/
protected $value;
}
ProductController.php
public function newAction(Request $request)
{
$entity = new Product();
$em = $this->getDoctrine()->getEntityManager();
$features = $em->getRepository('ProductBundle:FeatureKind')->findAll();
foreach($features as $feature)
{
$featurekind = new FeatureKind();
$featurekind->setTitle($feature->getTitle());
foreach($feature->getValue() as $value ){
$featurekind->getValue()->add($value);
}
$entity->getFeaturekind()->add($featurekind);
}
$form = $this->createForm(new ProductType(), $entity);
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('product_show', array(
'id' => $entity->getId()
)));
}
}
return $this->render('ProductBundle:Product:new.html.twig', array(
'form' => $form->createView()
));
}
ProductType.php
namespace Webmuch\ProductBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class ProductType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('featurekind', 'collection', array('type' => new FeatureKindType()))
->getForm();
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Webmuch\ProductBundle\Entity\Product',
'required' => true
);
}
public function getName()
{
return 'product';
}
}
FeatureKindType.php
namespace Webmuch\ProductBundle\Form;
class FeatureKindType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('title')
->add('value','collection', array(
'type' => new FeatureValueType(),
'allow_add'=>true))
->getForm();
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Webmuch\ProductBundle\Entity\FeatureKind',
);
}
public function getName()
{
return 'featurekind';
}
}
EDIT:
I after a few days of work, I am now stuck with a simple array of features and their respective multiple values:
Array
(
[Color] => Array
(
[Red] => Red
[Green] => Green
)
[Size] => Array
(
[Large] => Large
[Medium] => Medium
[Small] => Small
)
[Sleeve Style] => Array
(
[Half Sleeved] => Half Sleeved
[Full Sleeved] => Full Sleeved
[Cut Sleeves] => Cut Sleeves
)
)
I have tried to create the form as follows: $this->choices contains the array.
$builder
->add('name')
->add('slug')
->add('active')
;
foreach ($this->choices as $choice) {
$builder->add('featurekind', 'choice', array(
'required' => 'false',
'choices' => $choice,
'empty_value' => 'Choose an option',
'empty_data' => null
));
}
$builder->getForm();
The above doesn't work on the property $featurekind. I get the error:
Notice: Object of class Doctrine\Common\Collections\ArrayCollection could not be converted to int in /vagrant/project/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php line 457
Although if the form field is attached to any un-associated property, for example: $name, it still creates only one form field for the last iteration of the loop.
I am out of options.
What you want to do cannot be done with your current structure. Let me try to explain: FeatureKind has a one to many relationship with FeatureValue. This means that you can have a "color" kind which can have values "red","pink" etc.. This is fine. But your product entity has a collection of FeatureKind objects, so it can have a list like "Color", "Size", etc... BUT (this is the most important part) it has no way of asiginig a specific Value to any of these Kinds: there is no property which holds the specific value for each kind. I hope you could undestand this, it is a bit difficult to explain.
What you need to do:
Define your FeatureValue and FeatureKind classes just as they are.
Define a NEW entity which handles an association between a kind and a value for a product:
namespace Webmuch\ProductBundle\Entity;
/**
* @ORM\Table()
* @ORM\Entity
*/
class FeatureKindValue
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ManyToOne(targetEntity="Product", inversedBy="features")
**/
private $product;
/**
* @ORM\ManyToOne(targetEntity="FeatureKind")
**/
protected $kind;
/**
* @ORM\ManyToOne(targetEntity="FeatureValue")
**/
protected $value;
}
This entity handles pairs of kind:value, for example color:red
Finally, your product entity has a property of this new type:
namespace Webmuch\ProductBundle\Entity;
/**
* @ORM\Table()
* @ORM\Entity
*/
class Product
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* @ORM\OneToMany(targetEntity="FeatureKindValue", mappedBy="product")
**/
private $features;
}
Then, in order to present the form as you want, do something similar to the instructions given in the answer to this stackoverflow question
This kind of stuff can be tricky, but you can use this approach in your FeatureKindType
class:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (DataEvent $event) use ($builder) {
/* @var FormBuilderInterface $builder */
$form = $event->getForm();
/* @var FeatureKind $data */
$data = $event->getData();
if ($data !== null) {
$form->add(
$builder->getFormFactory()->createNamed(
'values',
'entity',
null,
array(
'label' => $data->getName(),
'class' => 'WebmuchProductBundle:FeatureValue',
)
)
);
}
}
);
}
Mind you, I have not tried to post the form and save the entities in your case, but the form now diplays the name of the FeatureKind
as label and a dropdown select containing the respective FeatureKindValues
.
I use this approach in a project of mine, and it works for me.
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