Today we are going to embed EAV(Entity Attribute Value) model using Symfony2 Collection Form component. Collection Form in Symfony2 means that a specific form field needs to be used to take multiple values by duplicating the field as many times as needed.
The above explained feature of Symfony2 was used to create dynamic generation & removal of specific form fields through collections for one of our client’s projection. In Symfony2 Collection Form, you must specify the type of the field and notify the form that it is the collection of the particular field.
This is done when you want to customize the Collection Form to the particular data that was submitted by the user.
There is a requirement that on selecting a particular category form (add products) button, the list of all subcategories defined in the category should be displayed. So that the user can select one or multiple subcategory(s) form list of a particular category. Doctrine2 helps generate model classes from already existing databases.
One category can be mapped with many subcategories. For the form registration, the user can select as many categories and its corresponding subcategories.
For using Doctrine2 ORM, you’ll need to add the Doctrine2 metadata like using the One-To-Many relationship annotation, many subcategories can be mapped into one category. The user selects his choice of category and hence, it’s subcategories while filling up the form.
Yeah! EAV Model can be quite confusing, but it’s not that hard. You’ll get it right!
Concept:
First of all, we created three entities namely TrademarkQuestionnaire, Category, Subcategory. Here “TrademarkQuestionnaire” -> “Entity”, “Category” -> “Attribute” and “SubCategory” -> “Value”.
We create a Category.php and SubCategory.php. Category and SubCategory has one-to-many relationship. Then we combine both of these Category and SubCategory in another entity class called CategorySubCategory.php. In this CategorySubCategory class Category and CategorySubCategory is mapped by one-to-many bidirectional relationship and CategorySubCategory and SubCategory class has one-to-many bidirectional relationship. Now in our main Entity class Trademarkquesionnaire.php we map Trademarkquesionnaire class with combined class entity CategorySubCategory class with many-to-many bidirectional relationship. Thus, we are able to embed EAV system using Symfony2 Collection Form.
Description
Entities
Steps for creating the category entity and mapping of different fields of this entity with others:
Create a category entity as follows: Category.php
- We declared field such as name in this class.
- Further we did the mapping of this entity with another entity SubCategory in one-to -many relationship.
- Again, we did the mapping of this entity with another entity named as CategorySubCategories in one-to-many relationship.
- This entity is further mapped with another entity Trademarkquesionnaire in many-to-one relationship.
- We define two function _toString() and _construct() for converting object into string and to initialize the components respectively.
/*** @var string ** @ORM\Column(name="name", type="string", length=255) */ private $name; /** ** @ORM\OneToMany(targetEntity="Subcategory", mappedBy="category", cascade={"persist", "remove"}) **/ private $subcategory; /** * @ORM\OneToMany(targetEntity="Webmuch\TrademarkBundle\Entity\CategorySubCategory", mappedBy="category") **/ private $categorySubCategories; /** * @ORM\ManyToMany(targetEntity="Webmuch\TrademarkBundle\Entity\TrademarkQuestionnaire", mappedBy="categories") **/ private $trademarks;public function __toString(){return (string)$this->name;} /** * Constructor */ public function __construct(){ $this->subcategory = new ArrayCollection(); }
We then generated the getter and setter method for the above fields by running the given commands on the Symfony2 console:
$ php app/console doctrine:generate:entities /webmuch/trademarkBundle/Entity/Category $ php app/console doctrine:schema:update --force
Create the subcategory entity as follows: Subcategory.php.
- We declared the field ‘productName’ which is of type string.
- Then we map the SubCategory entity with the previously created entity category in many-to-one relationship.
- SubCategory entity is further mapped with CategorySubCategories entity in many-to-many relationship.
- We define function _toString() function to convert objects into string.
/** * @var string** @ORM\Column(name="productName", type="string", length=255) */ private $productName; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="subcategory") * @ORM\JoinColumn(name="category_id", referencedColumnName="id") **/ private $category; /** * @ORM\ManyToMany(targetEntity="Webmuch\TrademarkBundle\Entity\CategorySubCategory", mappedBy="subCategories") **/ private $categorySubCategories; public function __toString(){ return (String)$this->productName; }
- Now generate the get() and set() methods for the above defined fields by compiling the following commands
$ php app/console doctrine:generate:entities /webmuch/TrademarkBundle/Entity/SubCategory $ php app/console doctrine:schema:update --force
Create the subcategory entity as follows: CategorySubCategory.php
First, we map this entity with category entity in many-to-one relationship. Again, we map this entity with subcategories entity in many-to-many relationship. This particular entity is mapped again with Trademarkquesionnaire entity in many-to-many relationship. Now,define the two function _toString() function to convert objects into string .
/** * @ORM\ManyToOne(targetEntity="Webmuch\TrademarkBundle\Entity\Category", inversedBy="categorySubCategories") * @ORM\JoinColumn(name="category_id", referencedColumnName="id") **/ private $category; /** * @ORM\ManyToMany(targetEntity="Webmuch\TrademarkBundle\Entity\Subcategory", inversedBy="categorySubCategories",cascade={"persist"}) * @ORM\JoinTable(name="categorySubCategories_subcategories") **/ private $subCategories; /** * @ORM\ManyToMany(targetEntity="Webmuch\TrademarkBundle\Entity\TrademarkQuestionnaire", mappedBy="categorySubCategories") **/ private $trademarks; public function __toString(){ return (String)$this->id; } //Here we generate the get() and set() methods for the above define field by running the following commands on console. $ php app/console doctrine:generate:entities /webmuch/TrademarkBundle/Entity/ CategorySubCategory $ php app/console doctrine:schema:update –force
Create the subcategory entity as follows: trademarkQuestionnaire.php
We map the Trademarkquestionnaire entity with another entity CategorySubategory in many-to-many relationship. We define two function _toString() function to convert objects into string.
After defining the functions we generated the get() and set() methods for the above defined field by running the following commands on console.
Symfony2 Collection Form is to manage a group of similar items. In this file various actions were created through CRUD generation among which we modified the CreateAction() and UpdateAction() methods.
/** * @ORM\ManyToMany(targetEntity="Webmuch\TrademarkBundle\Entity\CategorySubCategory", inversedBy="trademarks", cascade={ "persist", "remove" }) * @ORM\JoinTable(name="trademarks_categorySubCategories") **/ private $categorySubCategories;
Both the methods, before final submissions of data, the Subcategory entity’s fields were called to enter the values of subcategory for each category. As we have done one to many mapping of category entity with subcategory entity.
$ php app/console doctrine:generate:entities /webmuch/TrademarkBundle/Entity/TrademarkQuestionnaire $ php app/console doctrine:schema:update --force
Forms
- CategoryType.php
We import the following statement on the top of file to inherit SubcategoryType form in this fileuse Webmuch\TrademarkBundle\Form\SubcategoryType; ->add('subcategory','collection',array('type' => new SubcategoryType(), 'allow_add' => true, 'allow_delete' => true, 'options' => array('label' => false), ))
In build form function we add collection of subcategory and allow the add and delete functionality to be true, which is by default false.
- SubcategoryType.php
SubcategoryType.php->add('productName')
- CategorySubCategoryType.php
It is the most important part of the blog, because it is the actual form by which we are able to combine the Category(attribute) and SubCategory(value) in Trademarkquesionnaire(entity) form where we select a category(attribute), the relevant subcategories(values) are displayed.
First we import the following statement on top of the file to use the fields of subcategory entity. In bulidform function we add two EventListners for PRE_SET_DATA and PRE_SUBMIT . We define two functions onPreSubmit() and onPreSetData() to get the values to be stored for different categories and subcategories. Also, we define a function Addelements() to various subcategories to single category.
This function also arrange various subcategories into ascending order.
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Webmuch\TrademarkBundleBundle\Entity\Category; use Webmuch\TrademarkBundle\Entity\SubCategory; public function buildForm(FormBuilderInterface $builder, array $options) { $builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData')); $builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit')); } protected function addElements(FormInterface $form, Category $category = null) { $em = $this->getEntityManager(); // Add the Category element $form->add('Category', 'entity', array( 'data' => $category, 'empty_value' => '-- Select a Category first --', 'class' => 'WebmuchTrademarkBundle:Category', 'mapped' => true) ); // SubCategorys are empty, unless we actually supplied a Category $subCagories = array(); if ($category) { // Fetch the SubCategorys from specified Category $repo = $em->getRepository('WebmuchTrademarkBundle:SubCategory'); $subCagories = $repo->findByCategory($category, array('productName' => 'asc')); } // Add the SubCategorys element $form->add('subCategories', 'entity', array( 'empty_value' => '-- Select a Category first --', 'class' => 'WebmuchTrademarkBundle:SubCategory', 'choices' => $subCagories, 'multiple' => true, )); } function onPreSubmit(FormEvent $event) { $em = $this->getEntityManager(); $form = $event->getForm(); $data = $event->getData(); // Note that the data is not yet hydrated into the entity. if(isset($data['Category'])){ $category = $em->getRepository('WebmuchTrademarkBundle:Category')->find($data['Category']); $this->addElements($form, $category); } } function onPreSetData(FormEvent $event) { $em = $this->getEntityManager(); $data = $event->getData(); if($data instanceof \Webmuch\TrademarkBundle\Entity\CategorySubCategory ) { $category = $data->getCategory(); } else { $category = $event->getData(); } $this->addElements($event->getForm(), $category); }
Then after we will have to manage to make a system such that after selecting a category, the relevant subcategories are displayed. This can be achieved by writing the Ajax function and passing the Ajax url to the controller method.
We have need to create a controller method to fetch the subcategories by category in AJAX call .
/** * Displays a form to create a ajaxCall Product entity. * * @Route("/subCategoriesAjaxCall", name="category_subCategories_ajax_call") * @Method("GET") * @Template() */ public function ajaxAction(Request $request) { /*if (! $request->isXmlHttpRequest()) { throw new NotFoundHttpException(); }*/ $em = $this->getDoctrine()->getManager(); // Get the province ID $id = $request->query->get('category'); $category = $em->getRepository("WebmuchTrademarkBundle:Category")->findOneBy(array('id' => $id)); $result = array(); // Return a list of vendors, based on the selected province $repo = $em->getRepository('WebmuchTrademarkBundle:SubCategory'); $subCategories = $repo->findBy(array('category' => $category)); // Fetching the title of each feature values foreach ($subCategories as $subCategory) { $result[$subCategory->getProductName()] = $subCategory->getId(); } //print_r($vendors);die(); return new JsonResponse($result); } }
- TrademarkQuestionnaireType.php
In these form type, we import the following statement on the top of file. In bulidform() function, we add Collection Form of categorySubCategory so that the User can select many Subcategory for a single Category.
use Webmuch\TrademarkBundle\Form\CategorySubCategoryType; ->add('categorySubCategories','collection',array( 'type' => new CategorySubCategoryType(), 'allow_add' => true, 'allow_delete' => true, ))
Controller
After creating the above mentioned entities we write the controllers of these entities.
$php app/console doctrine:generate:crud
- CategoryController.php
In this Controller we have to import the following statement on the top of the file :
use Doctrine\Common\Collections\ArrayCollection; // create action public function Create Action(Request $request){ if ($form->isValid()) { $subcategories = $form->getData()->getSubCategory(); $em = $this->getDoctrine()->getManager(); foreach($subcategories as $subcategory){ $subcategory->setCategory($entity); } $em->persist($entity); $em->flush(); return $this->redirect($this->generateUrl('category_show', array('id' => $entity->getId( )))); } } // Update Action public function updateAction(Request $request, $id) { $em = $this->getDoctrine()->getManager(); $entity = $em->getRepository('WebmuchTrademarkBundle:Category')->find($id); $originalSubCategories = new ArrayCollection(); foreach($entity->getSubCategory() as $subcategory){ $originalSubCategories->add($subcategory); } if (!$entity) { throw $this->createNotFoundException('Unable to find Category entity.'); } $deleteForm = $this->createDeleteForm($id); $editForm = $this->createEditForm($entity); $editForm->handleRequest($request); if($editForm->isValid()){ foreach ($originalSubCategories as $subcategory) { if (false === $entity->getSubCategory()->contains($subcategory)) { $em->remove($subcategory); } } $subcategories = $editForm->getData()->getSubCategory(); $em = $this->getDoctrine()->getManager(); foreach($subcategories as $subcategory){ $subcategory->setCategory($entity); $em->persist($subcategory); $em->flush(); } $em->persist($entity); $em->flush(); return $this->redirect($this->generateUrl('category_edit', array('id' => $id))); }
HTML TWIG FILES
- Category
//index.html.twig (No modifications were made to the default code)
//new.html.twig
We called a class of type subcategory.
Created a Subcategory collectionHolder to store various Subcategories of a particular category into this Collection Form.
Create a link for adding many Subcategories for a particular category. This is achived through addTagForm() method.
Create a link for deleting Subcategories for a particular category. This is achived through addTagForm Delete link() method.
Css Block
{{ form_start(form) }} {{ form_widget(form.name) }} <ul class="subcategory" data-prototype="{{ form_widget(form.subcategory.vars.prototype)|e }}"></ul> {{ form_rest(form) }}
Javascript Block
{% block javascripts %} {{ parent() }} //call the parent class <script type="text/javascript"> var $subCategoryCollectionHolder = $('ul.subcategory'); //set the all data object in bundle // setup an "add a tag" link var $addSubCategoryLink = $('<a href="#" class="add_tag_link">Add Subcategory</a>'); var $newSubCategoryLink = $('<li></li>').append($addSubCategoryLink); var childFormLength = '{{ form.subcategory|length }}'; jQuery(document).ready(function() { // Get the ul that holds the collection of subcategory $subCategoryCollectionHolder = $('ul.subcategory'); // add a delete link to all of the existing tag form li elements $subCategoryCollectionHolder.find('li').each(function() { addTagFormDeleteLink($(this)); }); // add the "add a tag" anchor and li to the subcategory ul $subCategoryCollectionHolder.append($newSubCategoryLink); // add new form on click on add new link $addSubCategoryLink.on('click', function(e) { // prevent the link from creating a "#" on the URL e.preventDefault(); // add a new tag form (see the next code block) addTagForm($subCategoryCollectionHolder, $newSubCategoryLink); childFormLength++; // call the subcagories in use childFormLength++ }); }); function addTagForm($subCategoryCollectionHolder, $newSubCategoryLink) { // Get the data-prototype explained earlier var prototype = $subCategoryCollectionHolder.data('prototype'); // Replace '__name__' in the prototype's HTML to // instead be a number based on how many items we have var newForm = prototype.replace(/__name__/g, childFormLength); //childFormLength++; // Display the form in the page in an li, before the "Add a tag" link li var $newFormLi = $('<li></li>').append(newForm); $newSubCategoryLink.before($newFormLi); addTagFormDeleteLink($newFormLi); } function addTagFormDeleteLink($tagFormLi) { var $removeFormA = $('<a href="#">delete this Subcategory</a>'); $tagFormLi.append($removeFormA); $removeFormA.on('click', function(e) { // prevent the link from creating a "#" on the URL e.preventDefault(); // remove the li for the tag form $tagFormLi.remove(); }); } </script>
Show.html.twig(No modifications were made to the default code.)
Edit.html.Twig (New file is added in the new.html.twig file)
- CategorySubCategory
Index.html.twig(No modifications were mode to the default code.)
In this file, inside the javascript tags, document ready() function is called for enabling the Subcategories to be added to the Category for every loading of the page.
Css block
{% block body -%} <h1>CategorySubCategory creation</h1> {{ form(form) }} <ul class="record_actions"> <li><a href="{{ path('categorysubcategory') }}">Back to the list<</a></li> </ul> {% endblock %} {% block javascripts %} {{ parent() }} <script type="text/javascript"> $(document).ready(function () { $('#webmuch_trademarkbundle_categorysubcategory_Category').change (function(){ var val = $(this).val(); $.ajax({type: "get", url: "{{ url('category_subCategories_ajax_call') }}?category= " + val, success: function(data) { // Remove current options $('#webmuch_trademarkbundle_categorysubcategory_subCategories').html(''); //Rendor new options $.each(data, function(k, v) { alert($('#webmuch_trademarkbundle_categorysubcategory_subCategories').append('<option value="' + v + '">' + k + '</option>')); }); } }); return false; }); }); </script> {% endblock %}
Show.html.twig (No modifications were made to the default code)
Edit.html.Twig (No modifications were made to the default code)
- TrademarkQuestionnaire
In new.html.twig file, we add a new button loaded as add + to select categories and its respective Subcategories. For this purpose inside the javascript tags. We created a collectionHolder variable and added to it, CategorySubCategoryAddValueLink. Also, we added a delete link to the collectionHolder to delete any existing SubCategory and Categories. For both these functionalities, we defined two separate functions categorySubCategories AddValueForm and CategorySubCategory AddTagFormDeleteLink().
<script type="text/javascript"> var collectionHolder = $('ul.categorySubCategories'); var valueCount = '{{ form.categorySubCategories|length }}'; // setup an "add a value" link var $addValueLink = $('<a href="#" class="btn btn-primary btn-small">add+</a>'); var $newLinkLi = $('<li></li>').append($addValueLink); jQuery(document).ready(function() { $collectionHolder = $('ul.value'); // add a delete link to all of the existing value form li elements $collectionHolder.find('li').each(function() { addTagFormDeleteLink($(this)); }); // add the "add a value" anchor and li to the categorySubCategories ul collectionHolder.append($newLinkLi); // action performed on clicked $addValueLink.on('click', function(e) { // prevent the link from creating a "#" on the URL e.preventDefault(); // add a new value form (see next code block) addValueForm(collectionHolder, $newLinkLi); valueCount++; }); }); function addValueForm(collectionHolder, $newLinkLi) { //Get the exixting count of value // Get the data-prototype we explained earlier var prototype = collectionHolder.attr('data-prototype-categorySubCategories'); // Replace '__name__' in the prototype's HTML to // instead be a number based on the current collection's length. var newForm = prototype.replace(/__name__/g, valueCount); // Display the form in the page in an li, before the "Add a value" link li var $newFormLi = $('<li></li>').append(newForm); $newLinkLi.before($newFormLi); // add a delete link to the new form addTagFormDeleteLink($newFormLi); } function addTagFormDeleteLink($valueFormLi) { var $removeFormA = $('<a href="#" class="btn btn-danger btn-small">del-</a>'); $valueFormLi.append($removeFormA); $removeFormA.on('click', function(e) { // prevent the link from creating a "#" on the URL e.preventDefault(); // remove the li for the value form $valueFormLi.remove(); }); } </script> <script type="text/javascript"> $('#select').on('change', 'select:not(select[multiple=multiple])', function() { var $cSelect = $(this); var $nextMultiSel = $("#"+$cSelect.attr('id').slice(0,-8)+"subCategories").css("height","100"); console.log("clicked at ?"); $.ajax({ type: "get", url: "{{ url('category_subCategories_ajax_call') }}?category=" + $cSelect.val(), success: function(data) { // Remove current options console.log("fetched"); $nextMultiSel.html(''); //Rendor new options $.each(data, function(k, v) { $nextMultiSel.append('<option value="' + v + '">' + k + '</option>'); }); } }); return false; }); </script> {% endblock %}
Well, I hope you’ve embeded your EAV model in Symfony2 Collection Form. Hope this helped!
Please do share the tutorial if you appreciate it at all. And for any doubts, please mention your query in the comments section below.
Happy Reading!