Assign many items to one category - OneToMany Relation
and
Symfony form not saving entity with ManyToMany relation
Based on the two questions above I have an list with many checkboxes for all items. So it does not make any sense to have 2 Routes/Methods: One for assign and another for removing the items. To make it logically correct, assign items and remove items have to be in one method - this method down under is only working for assigning and not for removing one ore more items. In a real application assign and remove items in one step is the only way it makes sense (because of the checkboxes).
/**
* Assign items to category and remove items from the list
*
* #Route("/{id}/assign", name="category_assign")
* #Template("CheckoutItemBundle:Category:assign.html.twig")
* #param Request $request
* #param $id
* #return array|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function assignAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('CheckoutItemBundle:Category')->find($id);
if (!$entity)
{
throw $this->createNotFoundException('Unable to find Category entity.');
}
$tokenStorage = $this->container->get('security.token_storage');
$form = $this->createForm(new CategoryItemType($tokenStorage), $entity, array(
'method' => 'POST',
));
$form->add('submit', 'submit', array('label' => 'Save'));
$form->handleRequest($request);
if ($form->isValid())
{
$data = $form->getData();
foreach($data->getItems() as $item)
{
$item->setCategory($entity);
$em->persist($item);
}
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('category'));
}
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
Form:
<?php
namespace Checkout\Bundle\ItemBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Doctrine\ORM\EntityRepository;
class CategoryItemType extends AbstractType
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$currentUser = $this->tokenStorage->getToken()->getUser();
$builder
->add('items', 'entity', array(
'label' => 'Items',
'required' => false,
'class' => 'CheckoutItemBundle:Item',
'property' => 'name',
'expanded' => true,
'multiple' => true,
'query_builder' => function (EntityRepository $er) use ($currentUser) {
return $er->createQueryBuilder('c')
->where('c.user = :user')
->setParameter('user', $currentUser);
},
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Checkout\Bundle\ItemBundle\Entity\Category'
));
}
/**
* #return string
*/
public function getName()
{
return 'checkout_bundle_itembundle_category_items';
}
}
Entity Category:
class Category
{
/**
* #var integer
*
* #ORM\Column(type="guid")
* #ORM\Id
* #ORM\GeneratedValue(strategy="UUID")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Checkout\Bundle\ItemBundle\Entity\Item", mappedBy="category", cascade={"persist", "remove"})
**/
private $items;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="description", type="text")
*/
private $description;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
**/
private $user;
/**
* Get id
*
* #return integer
*/
Here the complete one: http://laravel.io/bin/bEEr5 (It's just to long, to paste it here.)
Entity Item:
class Item
{
/**
* #var integer
*
* #ORM\Column(type="guid")
* #ORM\Id
* #ORM\GeneratedValue(strategy="UUID")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="description", type="text", nullable=true)
*/
private $description = null;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="Checkout\Bundle\ItemBundle\Entity\ItemCharakter", mappedBy="item", cascade={"persist", "remove"})
**/
private $itemCharakters;
/**
* #ORM\ManyToOne(targetEntity="Checkout\Bundle\ItemBundle\Entity\Category", inversedBy="items")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
**/
private $category;
/**
* #ORM\ManyToMany(targetEntity="Checkout\Bundle\ItemBundle\Entity\Tax", inversedBy="items")
* #ORM\JoinTable(name="Items_Taxes")
*/
private $taxes;
/**
* #ORM\ManyToMany(targetEntity="Checkout\Bundle\ItemBundle\Entity\Modifier", inversedBy="items")
* #ORM\JoinTable(name="Items_Modifiers")
*/
private $modifiers;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
**/
private $user;
/**
* #var \DateTime $created_at
*
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
private $created_at;
/**
* #var \DateTime $updated_at
*
* #Gedmo\Timestampable(on="update")
* #ORM\Column(type="datetime")
*/
private $updated_at;
the complete entity: http://laravel.io/bin/XyyxB
.
First scenario:
Assign Items to Category:
[ ] Item 1
[ ] Item 2
assign all ====>
Assign Items to Category:
[X] Item 1
[X] Item 2
Second:
Assign Items to Category:
[ ] Item 1 // Remove this item 1, after it was assign
[X] Item 2 // Stay assigned
I'm not on the owning side here. I'm using Symfony 2.6.7. In this example it is an OneToMany Relation, but in the future I need it also for ManytoMany Relation.
Any idea, how this is possible for OneToMany and ManyToMany, when I'm not on the owning side? :-)
I also followed this guide for the adding items part: http://symfony.com/doc/current/book/doctrine.html#entity-relationships-associations - may it help to clarify.
Related
I have 2 entities:
Category and Article with the following relation:
Each Article has One Category.
Each Category may have Many Article.
The problem :
I want to add new category using this form :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
->add('submit', SubmitType::class, [
'label' => 'Add a new category'
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Category::class,
]);
}
Here's the error I get when I enter a string value in the field name:
Name Error This value should be of type array|IteratorAggregate.
I've never used ArrayCollection / Relation in symfony but I've tried to change the TextType::class to CollectionType::class it gives me a white square and I can't type anything.
In a more generic question :
How could I validate values in a form without having the error : This value should be of type array|IteratorAggregate.
Category
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Assert\Unique(message="Cette catégorie existe déjà")
*/
private $name;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param $name
* #return Collection|null
*/
public function setName($name): ?Collection
{
return $this->name = $name;
}
Article
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="text")
*/
private $title;
/**
* #ORM\Column(type="text")
*/
private $content;
/**
* #ORM\Column(type="datetime")
*/
private $createdAt;
/**
* #ORM\Column(type="string", nullable=true)
*/
private $image;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $editedAt;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Category")
*/
private $category;
I am using Symfony 3.4.7 . I work with 3 linked entites, Article, Categorie and ArticlesCategories. ArticlesCategories is the relation table.
I would like to add and edit articles. I wish I could Add / Edit articles, given that an article may have several Categories and vice versa. I have attributes specific to the relationship in the relation table , that's why I created the relation entity.
This is the code of an Articles :
/**
* Articles
*
* #ORM\Table(name="articles")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ArticlesRepository")
*/
class Articles
{
/**
* #var string
*
* #ORM\Column(name="code_article", type="string", length=10)
* #ORM\Id
*/
private $codeArticle;
/**
* #var string
*
* #ORM\Column(name="description", type="text", nullable=true)
*/
private $description;
/**
* #var ArticlesCategories
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\ArticlesCategories", mappedBy="codeArticle")
*/
private $articlesCategories;
// getters et setters normaux
...
/**
* Add articlesCategorie
*
* #param ArticlesCategories $articleCategorie
*
* #return Articles
*/
public function addArticlesCategorie(ArticlesCategories $articleCategorie){
$this->articlesCategories[] = $articleCategorie;
$articleCategorie->setCodeArticle($this);
return $this;
}
/**
* remove articlesCategorie
*
* #param ArticlesCategories $articlesCategorie
*/
public function removeArticlesCategorie(ArticlesCategories $articlesCategorie){
$this->articlesCategories->removeElement($articlesCategorie);
}
/**
* Get articlesCategories
*
* #return Collection
*/
public function getArticlesCategories(){
return $this->articlesCategories;
}
public function __toString()
{
return $this->codeArticle;
}
public function __construct()
{
$this->articlesCategories = new ArrayCollection();
}
This is the code of the relation ArticlesCategories:
/**
* ArticlesCategories
*
* #ORM\Table(name="articles_categories")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ArticlesCategoriesRepository")
*/
class ArticlesCategories
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Articles", inversedBy="articlesCategories")
* #ORM\JoinColumn(referencedColumnName="code_article", nullable=false)
*/
private $codeArticle;
/**
* #var string
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Categories")
* #ORM\JoinColumn(referencedColumnName="reference", nullable=false)
*/
private $codeCategorie;
/**
* #var string
*
* #ORM\Column(name="critere_rech_1", type="string", length=45, nullable=true)
*/
private $critereRech1;
/**
* #var string
*
* #ORM\Column(name="critere_rech_2", type="string", length=45, nullable=true)
*/
private $critereRech2;
And my entite Categories has nothing specific.
I generate automatically the crud of my entitie Articles, then I edit the class ArticlesType to have all attributes of my relation ArticlesCategories who are displays. To do this edition I use CollectionType.
This the code of my Form ArticlesType :
class ArticlesType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('codeArticle')
->add('description')
->add('ecotaxe')
->add('qteMaxCde')
->add('publication')
->add('designation')
->add('taxonomie')
->add('referenceStock')
->add('articleRegroupement')
->add('articleAssocie1')
->add('articleAssocie2')
->add('articleAssocie3')
->add('seuilDegressif')
->add('tauxDegressif')
->add('articlesCategories', CollectionType::class, array(
'entry_type' => ArticlesCategoriesType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
'label' => 'test',
'attr' => array('class' => 'collection-articlesCategories'),
'auto_initialize' => true
));
}
public function getName(){
return 'Articles';
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Articles'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_articles';
}
}
When I go on the Article edition page that was generates by Symfony, I get the right display.
Edition page
But When I click on the button "Edit", I get this error : Could not determine access type for property "articlesCategories" in class "AppBundle\Entity\Articles".
I don't see where is my mistake.
I hope I am clear.
Thank you for help.
Try putting the following:
public function setArticlesCategories(...) {
...
}
it's not a typical approach where "user adds article". This approach is a try to build nicely configurable, and data validated (by forms on frontend) table in database contains data based from other tables - something I'm trying to call "Dictonary tables", and store ID in main table of of vchar representation in other tables
This approach gives me a table which is very fast to process data on it.
Unfortunately, I encountered some difficulties using EntityType in forms
Adding ORM relation to entity (ConfigTable.php) causes writing null into database (POST have all values properly seted)
UPDATE 2018.02.15
annotation
#ORM\ManyToOne(targetEntity="AppBundle\Entity\yourentity", inversedBy="mappedByFieldName")
#ORM\JoinColumn(name="field_in_current_entity_declared_as_ORM#/Column")
protected $entityOne
#ORM\JoinColum is nt working properly here. Setting "name" attribute pointing to existing field can't create proper foreign keys.
If JoinColumn is not setted at all - FK are pointing to your current annotated field wits suffix _id
ie entity_one_id
and variables are saved from form,
but if in JoinColumn you set *name="pointing_to_field_annotaded_as_column_for_doctrine"*
during saving data to database you will get error of that object
*YourBundle/Entity/EntityName - cannot be converted to INT*
DefaultController.php
/**
* #param Request $request
* #Route("/add", name="action_configtable_add")
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function addFieldConfigTableAction(Request $request)
{
$form = $this->createForm(ConfigTableFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($data);
$em->flush();
return $this->redirectToRoute('homepage');
}
return $this->render(
':Forms/ConfigTable:add.config.table.item.html.twig',
[
'cdata' => $form->createView(),
]
);
}
MainEntity - in which I would like to store ID from dictonary entities (DictonaryOne.php, OtherDictonary.php)
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ConfigTable
* #package AppBundle\Entity
* #ORM\Entity(repositoryClass="AppBundle\Repository\ConfigTableRepository")
* #ORM\Table(name="config_table")
*/
class ConfigTable
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/protected $id;
/**
* #ORM\Column(type="integer", nullable=true )
*/protected $dictonaryOne;
/**
* #ORM\Column(type="integer", nullable=true )
*/protected $dictonaryTwo;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\DictonaryOne", inversedBy="configTableOne", cascade={"persist"})
* #ORM\JoinColumn(name="dictonary_one")
*/
protected $dictOne;
//usual getters & setters
}
DictonaryOne Entity (binded by relation, cant save it's id into database in ConfigTable)
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Class DictonaryOne
* #package AppBundle\Entity
* #ORM\Entity(repositoryClass="AppBundle\Repository\DictonaryOneRepository")
* #ORM\Table(name="dictonary_one")
*/
class DictonaryOne
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/protected $id;
/**
* #var string
* #ORM\Column(type="string", nullable=true)
*/
protected $name;
/**
* #var string
* #ORM\Column(type="string", nullable=true)
*/
protected $description;
/**
* #var boolean
* #ORM\Column(type="boolean", options={"default":1})
*/
protected $isActive;
/**
* #var int
* #ORM\Column(type="integer", options={"default":"1"})
*/
protected $orderField;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\ConfigTable", mappedBy="dictOne")
*/
protected $configTableOne;
public function __construct()
{
$this->configTableOne = new ArrayCollection();
}
//usual getters & setters and:
/**
* #return mixed
*/
public function getConfigTableOne()
{
return $this->configTableOne;
}
/**
* #param mixed $configTableOne
*/
public function setConfigTableOne(ConfigTable $configTableOne)
{
$this->configTableOne = $configTableOne;
}
}
OtherDictonary Entity: (not binded by relation, data are stored in database corectly after submit)
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class OtherDictonary
* #package AppBundle\Entity
* #ORM\Entity()
* #ORM\Table(name="other_dictonary")
*/
class OtherDictonary
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/protected $id;
/**
* #var string
* #ORM\Column(type="string")
*/
protected $name;
/**
* #var string
* #ORM\Column(type="string")
*/
protected $description;
/**
* #var boolean
* #ORM\Column(type="boolean")
*/
protected $isActive;
/**
* #var int
* #ORM\Column(type="integer")
*/
protected $orderField;
//usual getters & setters
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
public function __toString()
{
return (string)$this->id;
}
}
ConfigTableFormType.php
namespace AppBundle\Form;
use AppBundle\Entity\DictonaryOne;
use AppBundle\Entity\OtherDictonary;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ConfigTableFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'dictonaryOne',
EntityType::class,
[
'class'=> DictonaryOne::class,
'label' => 'label from dictonary 1',
'choice_value' => 'id',
'choice_label' => 'description',
'expanded' => false,
'multiple' => false,
// 'mapped' => false, //not working
]
)
->add(
'dictonaryTwo',
EntityType::class,
[
'class' => OtherDictonary::class,
'label' => 'label from other dictonary ',
'choice_value' => 'id',
'choice_label' => 'description',
'query_builder' => function (\Doctrine\ORM\EntityRepository $er) {
return $er->createQueryBuilder('d2')
->andWhere('d2.isActive = 1')
->orderBy('d2.description', 'ASC');
},
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => 'AppBundle\Entity\ConfigTable',
]
);
}
public function getBlockPrefix()
{
return 'app_bundle_config_table_form_type';
}
}
in ORM annotation, reference JoinColumn(name="xxx") is pointless.
try don't use notation:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\DictonaryOne", inversedBy="configTableOne", cascade={"persist"})
* #ORM\JoinColumn(name="dictonary_one")
*/
protected $dictOne;
use:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\DictonaryOne", inversedBy="configTableOne", cascade={"persist"})
*/
protected $dictOne;
foreign keys are formed well by symfony. Checked versions:
Symfony 2.8 - 3.3.16,
"doctrine/doctrine-bundle": "^1.6", (1.8.1)
"doctrine/orm": "^2.5", (v2.5.14)
I have a form which save queries. This is organized in the way of a people who ask is saved also this create a threat where is saving the id_question and emailperson. The issue is that the same person does another question. Logically, this report a primary key error:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'drj00003#ujaen.es' for key 'PRIMARY'
How do I fix it? In the constructor of class I cannot check if this exists and all registers are generated automatically in cascade without form.
It should be easy but I can't solve this.
class HiloType extends AbstractType{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('consultanteemail', new ConsultanteType(), array ('label' => false))
->add('personalemail', 'entity', array(
'label' => 'Personal de Soporte',
'class' => 'GuiasDocentes\AppBundle\Entity\Personal',
'property' => 'Enunciado',
'by_reference' => 'false',
'query_builder' => function(PersonalRepository $pr) {
$query= $pr->createQueryBuilder('u')
;
return $query;
},
'empty_value' => 'Elige un perfil de consulta:',
))
;
}
...
class Hilo{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var \GuiasDocentes\AppBundle\Entity\Personal
*
* #ORM\ManyToOne(targetEntity="Personal", inversedBy="hilos", cascade ={"ALL"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="personalEmail", referencedColumnName="email")
* })
*/
private $personalemail;
/**
* #var \GuiasDocentes\AppBundle\Entity\Consultante
*
* #ORM\ManyToOne(targetEntity="Consultante", inversedBy="hilos", cascade ={"ALL"} )
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="consultanteEmail", referencedColumnName="email")
* })
*/
private $consultanteemail;
/* Customized code */
/**
* #ORM\OneToMany(targetEntity="GuiasDocentes\AppBundle\Entity\Consulta", mappedBy="hiloid")
* #Assert\Valid()
*/
private $consultas;
public function __construct(){
$this->consultas = new ArrayCollection();
}
public function setConsultas (Consulta $consulta){
$this->hilos[]=$consulta;
}
public function addConsulta (\GuiasDocentes\AppBundle\Entity\Consulta $consulta){
$this->hilos[] = $consulta;
}
/* End customized code */
...
class Consultante{
/**
* #var string
*
* #ORM\Column(name="email", type="string", length=50, nullable=false)
* #ORM\Id
*/
private $email;
/**
* #var string
*
* #ORM\Column(name="nombre", type="string", length=30, nullable=true)
*/
private $nombre;
/**
* #var string
*
* #ORM\Column(name="apellidos", type="string", length=50, nullable=true)
*/
private $apellidos;
/* Customized code */
/**
* #ORM\OneToMany(targetEntity="GuiasDocentes\AppBundle\Entity\Hilo", mappedBy="consultanteemail")
* #Assert\Valid()
*/
private $hilos;
public function __construct(){
$this->hilos = new ArrayCollection();
}
public function setHilos (Hilo $hilo){
$this->hilos[]=$hilo;
}
public function addHilo (\GuiasDocentes\AppBundle\Entity\Hilo $hilo){
$this->hilos[] = $hilo;
}
public function setEmail($email)
{
$this->email = $email;
return $this;
}
/* End customize code */
...
public function contactoAction (Request $request){
$session = $request->getSession();
$perfil = $session->get('perfil');
$consultaHasAsignatura = new ConsultaHasAsignatura();
$form = $this->createForm(new ConsultaHasAsignaturaType(), $consultaHasAsignatura);
if ($request->isMethod('POST')){
$form->handleRequest($request);
if($form->isValid()){
$em = $this->getDoctrine()->getManager();
$em->persist($consultaHasAsignatura);
$em->flush();
return $this->redirectToRoute('guias_docentes_app_homepage');
}
}
return $this->render('GuiasDocentesAppBundle:FAQ:contacto.html.twig', array('perfil'=>$perfil,
'form' => $form->createView()
));
}
I'm using Symfony 2.1 and Doctrine 2.
I'm dealing with 2 main entities : Place and Feature, with a ManyToMany relationship between them.
There's many features in the database, and to group them by theme the Feature is also related to a FeatureCategory entity with a ManyToOne relationship.
Here's the code of the different entities :
The Place entity
namespace Mv\PlaceBundle\Entity;
…
/**
* Mv\PlaceBundle\Entity\Place
*
* #ORM\Table(name="place")
* #ORM\Entity(repositoryClass="Mv\PlaceBundle\Entity\Repository\PlaceRepository")
* #ORM\HasLifecycleCallbacks
*/
class Place
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
* #Assert\NotBlank
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="\Mv\MainBundle\Entity\Feature")
* #ORM\JoinTable(name="places_features",
* joinColumns={#ORM\JoinColumn(name="place_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="feature_id", referencedColumnName="id")}
* )
*/
private $features;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Place
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Add features
*
* #param \Mv\MainBundle\Entity\Feature $features
* #return Place
*/
public function addFeature(\Mv\MainBundle\Entity\Feature $features)
{
$this->features[] = $features;
echo 'Add "'.$features.'" - Total '.count($this->features).'<br />';
return $this;
}
/**
* Remove features
*
* #param \Mv\MainBundle\Entity\Feature $features
*/
public function removeFeature(\Mv\MainBundle\Entity\Feature $features)
{
$this->features->removeElement($features);
}
/**
* Get features
*
* #return Doctrine\Common\Collections\Collection
*/
public function getFeatures()
{
return $this->features;
}
public function __construct()
{
$this->features = new \Doctrine\Common\Collections\ArrayCollection();
}
The Feature Entity :
namespace Mv\MainBundle\Entity;
…
/**
* #ORM\Entity
* #ORM\Table(name="feature")
* #ORM\HasLifecycleCallbacks
*/
class Feature
{
use KrToolsTraits\PictureTrait;
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="label", type="string", length=255)
* #Assert\NotBlank()
*/
protected $label;
/**
* #ORM\ManyToOne(targetEntity="\Mv\MainBundle\Entity\FeatureCategory", inversedBy="features", cascade={"persist"})
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set label
*
* #param string $label
* #return Feature
*/
public function setLabel($label)
{
$this->label = $label;
return $this;
}
/**
* Get label
*
* #return string
*/
public function getLabel()
{
return $this->label;
}
/**
* Set category
*
* #param Mv\MainBundle\Entity\FeatureCategory $category
* #return Feature
*/
public function setCategory(\Mv\MainBundle\Entity\FeatureCategory $category = null)
{
$this->category = $category;
return $this;
}
/**
* Get category
*
* #return Mv\MainBundle\Entity\FeatureCategory
*/
public function getCategory()
{
return $this->category;
}
}
The FeatureCategory entity :
namespace Mv\MainBundle\Entity;
...
/**
* #ORM\Entity
* #ORM\Table(name="feature_category")
*/
class FeatureCategory
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="code", type="string", length=255)
* #Assert\NotBlank()
*/
protected $code;
/**
* #ORM\Column(name="label", type="string", length=255)
* #Assert\NotBlank()
*/
protected $label;
/**
* #ORM\OneToMany(targetEntity="\Mv\MainBundle\Entity\Feature", mappedBy="category", cascade={"persist", "remove"}, orphanRemoval=true)
* #Assert\Valid()
*/
private $features;
public function __construct()
{
$this->features = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set code
*
* #param string $code
* #return Feature
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* Get code
*
* #return string
*/
public function getCode()
{
return $this->code;
}
/**
* Set label
*
* #param string $label
* #return Feature
*/
public function setLabel($label)
{
$this->label = $label;
return $this;
}
/**
* Get label
*
* #return string
*/
public function getLabel()
{
return $this->label;
}
/**
* Add features
*
* #param \Mv\MainBundle\Entity\Feature $features
*/
public function addFeatures(\Mv\MainBundle\Entity\Feature $features){
$features->setCategory($this);
$this->features[] = $features;
}
/**
* Get features
*
* #return Doctrine\Common\Collections\Collection
*/
public function getFeatures()
{
return $this->features;
}
/*
* Set features
*/
public function setFeatures(\Doctrine\Common\Collections\Collection $features)
{
foreach ($features as $feature)
{
$feature->setCategory($this);
}
$this->features = $features;
}
/**
* Remove features
*
* #param Mv\MainBundle\Entity\Feature $features
*/
public function removeFeature(\Mv\MainBundle\Entity\Feature $features)
{
$this->features->removeElement($features);
}
/**
* Add features
*
* #param Mv\MainBundle\Entity\Feature $features
* #return FeatureCategory
*/
public function addFeature(\Mv\MainBundle\Entity\Feature $features)
{
$features->setCategory($this);
$this->features[] = $features;
}
}
Feature table is already populated, and users won't be able to add features but only to select them in a form collection to link them to the Place.
(The Feature entity is for the moment only linked to Places but will be later related to others entities from my application, and will contain all the features available for all entities)
In the Place form I need to display checkboxes of the features available for a Place, but I need to display them grouped by category.
Example :
Visits (FeatureCategory - code VIS) :
Free (Feature)
Paying (Feature)
Languages spoken (FeatureCategory - code LAN) :
English (Feature)
French (Feature)
Spanish (Feature)
My idea
Use virtual forms in my PlaceType form, like this :
$builder
->add('name')
->add('visit', new FeatureType('VIS'), array(
'data_class' => 'Mv\PlaceBundle\Entity\Place'
))
->add('language', new FeatureType('LAN'), array(
'data_class' => 'Mv\PlaceBundle\Entity\Place'
));
And create a FeatureType virtual form, like this :
class FeatureType extends AbstractType
{
protected $codeCat;
public function __construct($codeCat)
{
$this->codeCat = $codeCat;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('features', 'entity', array(
'class' => 'MvMainBundle:Feature',
'query_builder' => function(EntityRepository $er)
{
return $er->createQueryBuilder('f')
->leftJoin('f.category', 'c')
->andWhere('c.code = :codeCat')
->setParameter('codeCat', $this->codeCat)
->orderBy('f.position', 'ASC');
},
'expanded' => true,
'multiple' => true
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'virtual' => true
));
}
public function getName()
{
return 'features';
}
}
With this solution I get what I want but the bind process doesn't persist all the features. Instead of grouping them, it only keeps me and persist the last group "language", and erases all the previouses features datas. To see it in action, if I check the 5 checkboxes, it gets well into the Place->addFeature() function 5 times, but the length of the features arrayCollection is successively : 1, 2, 1, 2, 3.
Any idea on how to do it another way ? If I need to change the model I'm still able to do it.
What is the best way, reusable on my future other entities also related to Feature, to handle this ?
Thank you guys.
I think your original need is only about templating.
So you should not tweak the form and entity persistence logic to get the desired autogenerated form.
You should go back to a basic form
$builder
->add('name')
->add('features', 'entity', array(
'class' => 'MvMainBundle:Feature',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('f')
//order by category.xxx, f.position
},
'expanded' => true,
'multiple' => true
));
And tweak your form.html.twig