I have some entites with common relations and attributes. So, I want to simplify my schema using inheritance mapping.
I created a BaseData mappedsuperclass, and make my other entities expand it. This BaseData class has the common relations I need in each entity.
It works with many-to-one relation, like
/**
* #ORM\MappedSuperclass
*/
class BaseData
{
/**
* #ORM\ManyToOne(targetEntity="Service")
* #ORM\JoinColumn(name="service_id", referencedColumnName="id")
*/
protected $service;
But it become a little bit more tricky with self-referencing.
For instance, since I want to create a parent reference, I tried that :
/**
* #ORM\MappedSuperclass
*/
class BaseData
{
/**
* #ORM\ManyToOne(targetEntity="BaseData")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
protected $parent;
Obviously, it lead to a TableNotFoundException when I try to query this entity : QLSTATE[42S02]: Base table or view not found: 1146 Table 'project.base_data' doesn't exist.
So, I tried AssociationOverrides, but it seems that doesn't allow to change the Target Entity.
So, is there a way to build some self-reference on a MappedSuperclass ? And by the way, does it even make sense ?
Many thanks in advance !
Update
Here is the anwser :
I defined the protected $parent and protected $children in my BaseData mappedSuperClass as planned. I annotated them with other information I need. eg :
/**
* #ORM\MappedSuperclass
*/
class BaseData
{
/**
* #Datagrid\Column(field="parent.id", title="datagrid.parent_id", visible=false, safe=false)
* #Serializer\Expose
* #Serializer\Groups({"foo"})
*/
protected $parent;
/**
* #Serializer\Expose
* #Serializer\Groups({"elastica"})
*/
protected $children;
Then, I add the ORM relation with the event loadClassMetadata.
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
// the $metadata is all the mapping info for this class
$classMetadata = $eventArgs->getClassMetadata();
$reflObj = new \ReflectionClass($classMetadata->name);
if($reflObj) {
if ($reflObj->isSubclassOf('CoreBundle\Entity\BaseData')) {
$fieldMapping = array(
'targetEntity' => $classMetadata->name,
'fieldName' => 'parent',
'inversedBy' => 'children',
'JoinColumn' => array(
'name' => 'parent_id',
'referencedColumnName' => 'id',
'nullable' => true,
'onDelete' => 'SET NULL',
),
);
$classMetadata->mapManyToOne($fieldMapping);
$fieldMapping = array(
'fieldName' => 'children',
'targetEntity' => $classMetadata->name,
'mappedBy' => 'parent',
);
$classMetadata->mapOneToMany($fieldMapping);
}
}
}
Register the event, and that's it.
Now, every class which extends the BaseData superClass get the relation. For instance, php app/console doctrine:generate:entities MyBundle will generates the following code inside the SubClass entity :
/**
* Set parent
*
* #param \MyBundle\Entity\Subclass $parent
*
* #return Subclass
*/
public function setParent(\MyBundle\Entity\Subclass $parent = null)
{
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* #return \MyBundle\Entity\Subclass
*/
public function getParent()
{
return $this->parent;
}
/**
* Add child
*
* #param \MyBundle\Entity\Subclass $child
*
* #return Subclass
*/
public function addChild(\MyBundle\Entity\Subclass $child)
{
$this->children[] = $child;
return $this;
}
/**
* Remove child
*
* #param \MyBundle\Entity\Subclass $child
*/
public function removeChild(\MyBundle\Entity\Subclass $child)
{
$this->children->removeElement($child);
}
/**
* Get children
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getChildren()
{
return $this->children;
}
You can remove the mapping #ORM\ManyToOne(targetEntity="BaseData") and create an event listener on the event loadClassMetadata. (I didn't test the following code, this is just a starting point) Something like this:
class TestEvent
{
public function loadClassMetadata(\Doctrine\ORM\Event\LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
$class = $classMetadata->getName();
$fieldMapping = array(
'fieldName' => 'parent',
'targetEntity' => $class,
);
$classMetadata->mapManyToOne($fieldMapping);
}
}
One important thing to notice is that a listener will be listening for all entities in your application.
See Doctrine docs about events
And How to register event listener in symfony2
Related
I'm trying to create a ManyToMany relation beetwin services of a company.
Each service had N parents services and N children services.
I looked at the doctrine documentation here : Many-To-Many, Self-referencing and I implemented it as followed :
Here is my service entity :
<?
namespace AppBundle\Entity;
class Service
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
*/
private $parents;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", inversedBy="parents")
* #ORM\JoinTable(name="app_services_hierarchy",
* joinColumns={#ORM\JoinColumn(name="parent_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="enfant_id", referencedColumnName="id")}
* )
*/
private $enfants;
public function __construct()
{
$this->enfants = new ArrayCollection();
$this->parents = new ArrayCollection();
}
public function getId(){
return $this->id;
}
//--------------------------------------------------Enfants
public function getEnfants(){
return $this->enfants;
}
public function setEnfants($enfant){
$this->enfants = $enfant;
}
public function addEnfant(Service $s){
$this->enfants[] = $s;
return $this;
}
public function removeEnfant(Service $s){
$this->enfants->removeElement($s);
}
//--------------------------------------------------Parents
public function getParents(){
return $this->parents;
}
public function setParents($parents){
$this->parents = $parents;
}
public function addParent(Service $s){
$this->parents[] = $s;
return $this;
}
public function removeParent(Service $s){
$this->parents->removeElement($s);
}
}
And here is my edit function( Controller.php) :
public function editAction(Request $request, $id)
{
$service = $this->getDoctrine()->getRepository(Service::class)->find($id);
$form = $this->createForm(ServiceType::class, $service);
$form ->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager ->persist($service);
dump($service);
$entityManager ->flush();
}
return $this->render('AppBundle:Service:edit.html.twig', array(
'form' => $form->createView(),
));
}
And the generated form looks like :
PROBLEM :
My problem is that the childrens are updated but not the parents. I can see the parents in the $service variable when I dump() it in my controler but the only ones updated in my database table (app_services_hierarchie) are the children.
The difference between $parents and $enfants in your code is that the service you are looking at is the Owning side in case of your $enfants mapping, but not in the case of your $parents mapping.
Doctrine will not store the $parents unless you tell it to do so via cascade={"persist"}.
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
*/
This is basically the same anwer given in the post linked by #GregoireDucharme.
Edit: after some research, apparently this problem cannot be solved using cascade. According to the Doctrine documentation:
Doctrine will only check the owning side of an association for changes.
So what you have to do is tell your $parents to also update the $children property.
public function addParent(Service $s){
$this->parents[] = $s;
$s->addEnfant($this);
return $this;
}
public function removeParent(Service $s){
$this->parents->removeElement($s);
$s->removeEnfant($this);
}
In your form, make sure to specify the following:
->add('parents', 'collection', array(
'by_reference' => false,
//...
))
(I haven't spellchecked any of the code above, so tread carefully.)
If 'by_reference' is set to true, addParent and removeParent will not be called.
Credit goes to this blog post by Anny Filina.
It also states that you can remove the cascade option from your $parents property, but you probably should add cascade={"persist","remove"} to your $enfants property.
In a Symfony2 application using the Sonata Admin bundle, I have two entities:
CorporateAttributes
CorporateAttributesApi
Related in Doctrine like so:
CorporateAttributes ←one-to-many→ CorporateAttributesApi
My Sonata Admin class for CorporateAttributes contains the following:
in AppBundle/Admin/CorporateAttributesAdmin.php
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper) {
$formMapper
->add('apis', 'sonata_type_collection',
['required' => false, 'label' => 'API Clients'],
['edit'=>'inline','inline'=>'table']
)
;
}
This adds a "Add new" button to the CorporateAttributes form where I can add and edit CorporateAttributesApi's related to the CorporateAttributes object for which the user is editing.
However, this only works for an existing CorporateAttributes object.
If I'm trying to add a new CorporateAttributes, clicking the "Add New" button gives the following error in the console:
Failed to load resource: the server responded with a status of 500 (Internal Server Error)
http://localhost/app_dev.php/admin/core/append-form-field-element?code=sonata.admin.corporateattributes&elementId=s55fc29157eeee_apis&uniqid=s55fc29157eeee
I suspect it has something to do with the fact that CorporateAttributesApi needs a CorporateAttributes id that it references, but I'm not sure how to make it play nice.
Here is the other relevant code:
in AppBundle/Admin/CorporateAttributesApiAdmin.php:
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper) {
$formMapper
->add('corporate_attributes', null, ['required' => true])
->add('group_name', 'choice', [
'choices' => ['a', 'b', 'c'],
'required' => false,
])
;
}
And the entities with doctrine2 annotations:
in AppBundle/Entity/CorporateAttributes.php:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* CorporateAttributes
*
*
* #ORM\Entity
* #ORM\Table("drupal_wiredb_corporate_attributes")
*/
class CorporateAttributes
{
/**
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="CorporateAttributesApi", mappedBy="corporate_attributes", cascade={"persist"}, orphanRemoval=true))
*/
protected $apis;
public function getId() {
return $this->id;
}
/**
* Add apis
*
* #param \AppBundle\Entity\CorporateAttributesApi $apis
* #return CorporateAttributes
*/
public function addApi(\AppBundle\Entity\CorporateAttributesApi $api)
{
$this->apis[] = $api;
$api->setCorporateAttributes($this);
return $this;
}
/**
* Remove apis
*
* #param \AppBundle\Entity\CorporateAttributesApi $apis
*/
public function removeApi(\AppBundle\Entity\CorporateAttributesApi $api)
{
$this->apis->removeElement($api);
$api->setCorporateAttributes(null);
}
/**
* Get apis
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getApis()
{
return $this->apis;
}
/**
* Constructor
*/
public function __construct()
{
$this->apis = new \Doctrine\Common\Collections\ArrayCollection();
}
}
in AppBundle/Entities/CorporateAttributesApi.php:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* CorporateAttributesApi
*
*
* #ORM\Entity
* #ORM\Table("drupal_wiredb_corporate_attributes_api")
*/
class CorporateAttributesApi
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="CorporateAttributes", inversedBy="apis")
* #ORM\JoinColumn(name="attribute_id", referencedColumnName="id")
*/
protected $corporate_attributes;
/**
* #ORM\Id
* #ORM\Column(name="group_name", type="string", length=128, options={"default":""})
*/
protected $group_name = '';
public function __toString() {
if (empty($this->corporate_attributes) && empty($this->api_user)) {
return 'New Corporate Attributes - API User Join';
}
else {
return (string)$this->corporate_attributes . ' | ' . (string)$this->api_user . ' | ' . $this->group_name;
}
}
/**
* Set group_name
*
* #param string $groupName
* #return CorporateAttributesApi
*/
public function setGroupName($groupName)
{
$this->group_name = $groupName;
return $this;
}
/**
* Get group_name
*
* #return string
*/
public function getGroupName()
{
return $this->group_name;
}
/**
* Set corporate_attributes
*
* #param \AppBundle\Entity\CorporateAttributes $corporateAttributes
* #return CorporateAttributesApi
*/
public function setCorporateAttributes(\AppBundle\Entity\CorporateAttributes $corporateAttributes)
{
$this->corporate_attributes = $corporateAttributes;
return $this;
}
/**
* Get corporate_attributes
*
* #return \AppBundle\Entity\CorporateAttributes
*/
public function getCorporateAttributes()
{
return $this->corporate_attributes;
}
}
I would try to modify your AppBundle/Admin/CorporateAttributesApiAdmin.php file in following way:
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper) {
$formMapper
->add('corporate_attributes', null, ['required' => true])
->add('group_name', 'choice', [
'choices' => ['a', 'b', 'c'],
'required' => false,
])
;
// If this is sonata_type_collection inside CorporateAttributes form,
// then we don't need 'corporate_attributes' field as it would be the parent entity
if ($this->getRoot() instanceof \AppBundle\Admin\CorporateAttributesAdmin) {
$formMapper->remove('corporate_attributes');
}
}
public function getNewInstance()
{
$object = parent::getNewInstance();
// Here we specify the 'corporate_attributes'
if ($this->getRoot()->getSubject() instanceof \AppBundle\Entity\CorporateAttributes) {
$object->setCorporateAttributes($this->getRoot()->getSubject());
}
return $object;
}
You also may want to try modify AppBundle/Admin/CorporateAttributesAdmin.php to set by_reference=false in form field definition:
// Fields to be shown on create/edit forms
// AppBundle/Admin/CorporateAttributesAdmin.php
protected function configureFormFields(FormMapper $formMapper) {
$formMapper
->add('apis', 'sonata_type_collection',
['required' => false, 'label' => 'API Clients', 'by_reference' => false],
['edit'=>'inline','inline'=>'table']
)
;
}
Here is documentation for by_reference option: http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference
Here is the situation, I have a form in which I need an entity field type. Inside the BenefitGroup entity I have a BenefitGroupCategory selection.
My buildform is:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('BenefitGroupCategories', 'entity', array(
'class' => 'AppBundle:BenefitGroupCategory',
'property' => 'name',
'label' => false,
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('c')
->orderBy('c.name', 'ASC');
},))
->add('benefitsubitems', 'collection', array('type' => new BenefitSubItemFormType(), 'allow_add' => true, 'label' => false,));
}
It's almost a typical product-category relationship. A BenefitGroup can have only one category and a category can belong to many BenefitGroups (the only complication, not implemented yet, but that's the reason I need the query builder, is that all will depend on another parameter (project) so that some categories will be the default ones (always available), others will be available only for specific projects (see below the reference to project in the BenefitGroupCategory entity)).
You'll notice another field, benefitsubitems, which is not relevant for the question at hand.
As far I understand it, from the Doctrine perspective, I have to set up a One-To-Many, Unidirectional with Join Table.
The two entities are:
<?php
// src/AppBundle/Entity/BenefitGroup.php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Entity\BenefitGroupRepository")
* #ORM\Table(name="benefit_groups")
*/
class BenefitGroup
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="BenefitItem", cascade={"persist"}, inversedBy="BenefitGroups")
*/
protected $benefitItem;
/**
* #ORM\oneToMany(targetEntity="BenefitSubItem", mappedBy="benefitGroup")
*/
protected $BenefitSubItems;
/**
* #ORM\ManyToMany(targetEntity="BenefitGroupCategory")
* #ORM\JoinTable(name="BenefitGroup_BenefitGroupCategory", joinColumns={#ORM\JoinColumn(name="BenefitGroup_id", referencedColumnName="id")}, inverseJoinColumns={#ORM\JoinColumn(name="BenefitGroupCategory_id", referencedColumnName="id", unique=true)})
*/
protected $BenefitGroupCategories;
// HERE I HAVE SOME IRRELEVANT GETTERS AND SETTERS
/**
* Constructor
*/
public function __construct()
{
$this->BenefitSubItems = new ArrayCollection();
$this->BenefitGroupCategories = new ArrayCollection();
}
/**
* Add BenefitGroupCategories
*
* #param \AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories
* #return BenefitGroup
*/
public function addBenefitGroupCategory(\AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories)
{
$this->BenefitGroupCategories[] = $benefitGroupCategories;
return $this;
}
/**
* Remove BenefitGroupCategories
*
* #param \AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories
*/
public function removeBenefitGroupCategory(\AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories)
{
$this->BenefitGroupCategories->removeElement($benefitGroupCategories);
}
/**
* Get BenefitGroupCategories
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getBenefitGroupCategories()
{
return $this->BenefitGroupCategories;
}
}
You'll also notice another entity, BenefitItem, which is the "father" of BenefitGroup.
And
<?php
// src/AppBundle/Entity/BenefitGroupCategory.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="benefit_group_category")
* #UniqueEntity(fields={"name", "project"}, ignoreNull=false, message="Duplicated group category for this project")
*/
class BenefitGroupCategory
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=50)
*/
protected $name;
/**
* #ORM\ManyToOne(targetEntity="Project")
*/
protected $project;
// HERE I HAVE SOME IRRELEVANT GETTERS AND SETTERS
}
In the controller (you'll see several embedded collections, which work ok) I have:
/**
* #Route("/benefit/show/{projectID}", name="benefit_show")
*/
public function showAction(Request $request, $projectID)
{
$id=4; //the Id of the CVC to look for
$storedCVC = $this->getDoctrine()
->getRepository('AppBundle:CVC')
->find($id);
$form = $this->createForm(new CVCFormType(), clone $storedCVC);
$form->handleRequest($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
//$benefitGroupCategoryRepository = $this->getDoctrine()->getRepository('AppBundle:BenefitGroupCategory');
$formCVC = $form->getData();
$em->persist($formCVC);
foreach ($formCVC->getBenefitItems() as $formBI)
{
$newBI = new BenefitItem();
$newBI->setCVC($formCVC);
$newBI->setComment($formBI->getComment());
$em->persist($newBI);
foreach ($formBI->getBenefitGroups() as $formBG)
{
$newBG = new BenefitGroup();
$newBG->setBenefitItem($newBI);
$newBG->setBenefitGroupCategories($formBG->getBenefitGroupCategories());
$em->persist($newBG);
foreach ($formBG->getBenefitSubItems() as $formSI)
{
$newSI = new BenefitSubItem();
$newSI->setBenefitGroup($newBG);
$newSI->setComment($formSI->getComment());
$em->persist($newSI);
}
}
}
$em->flush();
}
return $this->render('benefit/show.html.twig', array(
'form' => $form->createView(),
));
}
The problem is: in visualization it visualizes correctly the form (even though it does not retrieve correctly the category. I have a choice of categories, which is ok, but it does not retrieve the right one. Do I have to set the default value in the form?
The problem gets way worse when I sumbit the form it's supposed to create a new entity (notice the clone) with all the nested ones. The problem is that it crashes saying:
Neither the property "BenefitGroupCategories" nor one of the methods
"addBenefitGroupCategory()"/"removeBenefitGroupCategory()",
"setBenefitGroupCategories()", "benefitGroupCategories()", "__set()" or
"__call()" exist and have public access in class
"AppBundle\Entity\BenefitGroup".
The "beauty" is that even if I comment completeley the (nasty) part inside the "isValid" it behaves exactly the same.
I'm lost :(
About the cloning you have to unset the id of the cloned entity, look here: https://stackoverflow.com/a/14158815/4723525
EDIT:
Yes, but PHP just do shallow copy, you have to clone other objects. Look at Example #1 Cloning an object in http://php.net/manual/en/language.oop5.cloning.php. You have to clone your objects by defining __clone method (for Doctrine lower than 2.0.2 you have to do this by calling own method after cloning because proxy defines it's own __clone method). So for example:
function __clone() {
$oldCollection = $this->collection;
$this->collection = new ArrayCollection();
foreach ($oldCollection as $oldElement) {
$newElement = clone $oldElement;
// additional actions for example setting this object to owning side
$newElement->setParent($this);
$this->collection->add($newElement);
}
}
Context : I am building my little TodoList bundle (which is a good exercice to go deep progressively with Symfony2), the difficulty comes with recursivity : each Task can has children and parent, so I used Gedmo Tree.
I have a collection of tasks each having a sub collection of children, children collection has prototype enabled so I can display a new sub task form when clicking "add sub task".
I wanted the default name of the subtask to be "New Sub Task" instead of "New Task" set in Task constructor, so I figured out how to pass a custom instance for the prototype and took some care for preventing infinite loop.
So I am almost done and my new task is added with the name I set when saving...
Problem : I am not able to persist the parent task to the new sub task, the new task persist the name well, but not the parentId, I probably forgot somewhere with Doctrine, here is some relevant parts :
// Entity Task
/**
* #Gedmo\Tree(type="nested")
* #ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="task")
*/
class Task {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
protected $created;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank(message="Name must be not empty")
*/
protected $name = 'New Task';
//....
/**
* #Gedmo\TreeLeft
* #ORM\Column(name="lft", type="integer")
*/
private $lft;
/**
* #Gedmo\TreeLevel
* #ORM\Column(name="lvl", type="integer")
*/
private $lvl;
/**
* #Gedmo\TreeRight
* #ORM\Column(name="rgt", type="integer")
*/
private $rgt;
/**
* #Gedmo\TreeRoot
* #ORM\Column(name="root", type="integer", nullable=true)
*/
private $root;
/**
* #Gedmo\TreeParent
* #ORM\ManyToOne(targetEntity="Task", inversedBy="children")
* #ORM\JoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
*/
protected $parent = null;//
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $parentId = null;
/**
* #Assert\Valid()
* #ORM\OneToMany(targetEntity="Task", mappedBy="parent", cascade={"persist", "remove"})
* #ORM\OrderBy({"status" = "ASC", "created" = "DESC"})
*/
private $children;
public function __construct(){
$this->children = new ArrayCollection();
}
/**
* Set parentId
*
* #param integer $parentId
* #return Task
*/
public function setParentId($parentId){
$this->parentId = $parentId;
return $this;
}
/**
* Get parentId
*
* #return integer
*/
public function getParentId(){
return $this->parentId;
}
/**
* Set parent
*
* #param \Dmidz\TodoBundle\Entity\Task $parent
* #return Task
*/
public function setParent(\Dmidz\TodoBundle\Entity\Task $parent = null){
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* #return \Dmidz\TodoBundle\Entity\Task
*/
public function getParent(){
return $this->parent;
}
/**
* Add children
*
* #param \Dmidz\TodoBundle\Entity\Task $child
* #return Task
*/
public function addChild(\Dmidz\TodoBundle\Entity\Task $child){
$this->children[] = $child;
return $this;
}
/**
* Remove child
*
* #param \Dmidz\TodoBundle\Entity\Task $child
*/
public function removeChild(\Dmidz\TodoBundle\Entity\Task $child){
$this->children->removeElement($child);
}
}
// TaskType
class TaskType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options){
$builder
->add('name', null, ['label' => false])
->add('notes', null, ['label' => 'Notes'])
->add('status', 'hidden')
->add('parentId', 'hidden')
;
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($builder){
$record = $event->getData();
$form = $event->getForm();
if(!$record || $record->getId() === null){// if prototype
$form->add('minutesEstimated', null, ['label' => 'Durée', 'attr'=>['title'=>'Durée estimée en minutes']]);
}elseif($record && ($children = $record->getChildren())) {
// this is where I am able to customize the prototype default values
$protoTask = new Task();
$protoTask->setName('New Sub Task');
// here I am loosely trying to set the parentId I want
// so the prototype form input has the right value
// BUT it goes aways when INSERT in mysql, the value is NULL
$protoTask->setParentId($record->getId());
$form->add('sub', 'collection', [// warn don't name the field 'children' or it will conflict
'property_path' => 'children',
'type' => new TaskType(),
'allow_add' => true,
'by_reference' => false,
// this option comes from a form type extension
// allowing customizing prototype default values
// extension code : https://gist.github.com/jumika/e2f0a5b3d4faf277307a
'prototype_data' => $protoTask
]);
}
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver){
$resolver->setDefaults([
'data_class' => 'Dmidz\TodoBundle\Entity\Task',
'label' => false,
]);
}
public function getParent(){ return 'form';}
}
// my controller
/**
* #Route("/")
* #Template("DmidzTodoBundle:Task:index.html.twig")
*/
public function indexAction(Request $request){
$this->request = $request;
$repo = $this->doctrine->getRepository('DmidzTodoBundle:Task');
$em = $this->doctrine->getManager();
//__ list of root tasks (parent null)
$query = $repo->createQueryBuilder('p')
->select(['p','FIELD(p.status, :progress, :wait, :done) AS HIDDEN field'])
->addOrderBy('field','ASC')
->addOrderBy('p.id','DESC')
->andWhere('p.parent IS NULL')
->setParameters([
'progress' => Task::STATUS_PROGRESS,
'wait' => Task::STATUS_WAIT,
'done' => Task::STATUS_DONE
])
->setMaxResults(20)
->getQuery();
$tasks = $query->getResult();
//__ form building : collection of tasks
$formList = $this->formFactory->createNamed('list_task', 'form', [
'records' => $tasks
])
->add('records', 'collection', [
'type'=>new TaskType(),
'label'=>false,
'required'=>false,
'by_reference' => false,
])
;
//__ form submission
if ($request->isMethod('POST')) {
$formList->handleRequest($request);
if($formList->isValid()){
// persist tasks
// I thought persisting root tasks will persist their children relation
foreach($tasks as $task){
$em->persist($task);
}
$em->flush();
return new RedirectResponse($this->router->generate('dmidz_todo_task_index'));
}
}
return [
'formList' => $formList->createView(),
];
}
As mentionned in the comments in TaskType, the form prototype of the new sub task has the right value for parentId which is posted, BUT the value is gone and NULL on INSERT in db (looking at the doctrine log).
So do you think it is the right way of doing, and then what thing I forgot for persisting correctly the parent task of the new sub task ?
On your child setting you should set the parent when adding, like so..
/**
* Add children
*
* #param \Dmidz\TodoBundle\Entity\Task $children
* #return Task
*/
public function addChild(\Dmidz\TodoBundle\Entity\Task $children){
$this->children->add($children);
$children->setParent($this);
return $this;
}
/**
* Remove children
*
* #param \Dmidz\TodoBundle\Entity\Task $children
*/
public function removeChild(\Dmidz\TodoBundle\Entity\Task $children){
$this->children->removeElement($children);
$children->setParent(null);
}
When your prototype adds and deletes a row it calls addChild and removeChild but it doesn't call the setParent in the associated child.
This way any child that is added or removed/deleted get automatically set in the process.
Also you could change the $children to $child as it makes grammatical sense and it's really bugging me because I am a child(ren).
It seems weird to me that you try using the parentId field as a simple column, whereas it is a relation column. Theoretically, you should not:
$task->getParentId(); //fetching a DB column's value
but instead:
$task->getParent()->getId(); //walking through relations to find an object's attribute
However, if you really need this feature to avoid loading the full parent object and just get its ID, your setParentId method should be transparent (although, as mentionned, I'm not sure using the same DB field is valid):
public function setParent(Task $t = null) {
$this->parent = $t;
$this->parentId = null === $t ? null : $t->getId();
return $this;
}
Back to your issue: in the TaskType class, you should call:
$protoTask->setParent($record);
instead of:
$protoTask->setParentId($record->getId());
The reason:
you tell Doctrine parentId is a relation field (in the $parent attribute declaration), therefore Doctrine expects an object of the proper type
you also tell Doctrine to map this relation field directly to an attribute (the $parentId attribute declaration), I'm neither convinced this is valid, nor convinced this is good practice, but I guess you did some research before going for this structure
you set $parentId, but $parent has not been set (i.e. null), so Doctrine must erase the $parentId value with the $parent value: your code is proof that Doctrine handles attributes first, then computes relations ;)
Keep in mind Doctrine is an Object Relational Mapper, not a simple query helper: mapper is what it does (mapping persistence layer with your code), relational is how it does it (one-to-many and the like), object is what it does it on (therefore not directly using IDs).
Hope this helps!
I'm learning SF2 - really impressed with the job done, faced my first real issue I can't solve myself.
I have two entities: Post and Tag. Shortened code below:
class Tag
{
/**
* #ORM\ManyToMany(targetEntity="Post", mappedBy="tags", cascade={"persist"})
*/
private $posts;
public function __construct()
{
$this->posts = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #param \My\AppBundle\Entity\Snippet $posts
* #return Tag
*/
public function addSnippet(\My\AppBundle\Entity\Post $posts)
{
$this->posts[] = $posts;
return $this;
}
/**
* #param \My\AppBundle\Entity\Snippet $snippets
*/
public function removeSnippet(\My\AppBundle\Entity\Post $posts)
{
$this->posts->removeElement($posts);
}
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getSnippets()
{
return $this->posts;
}
}
class Post
{
/**
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="posts", cascade={"persist"})
* #ORM\JoinTable(name="posts_tags",
* joinColumns={#ORM\JoinColumn(name="post_id", referencedColumnName="id", unique=true, onDelete="cascade")},
* inverseJoinColumns={#ORM\JoinColumn(name="tag_id", referencedColumnName="id", unique=true, onDelete="cascade")}
* )
*/
private $tags;
public function __construct()
{
$this->tags = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #param \My\AppBundle\Entity\Tag $tags
* #return Snippet
*/
public function addTag(\My\AppBundle\Entity\Tag $tags)
{
$this->tags[] = $tags;
return $this;
}
/**
* #param \My\AppBundle\Entity\Tag $tags
*/
public function removeTag(\My\AppBundle\Entity\Tag $tags)
{
$this->tags->removeElement($tags);
}
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
}
As you can see I have M:M relation between two entities.
I have also a form to add Post with embedded Tag collection:
$builder
->add('title')
->add('tags', 'collection', array(
'type' => new \My\AppBundle\Form\TagType(),
'allow_add' => true,
'by_reference' => false,
'prototype' => true
))
;
TagType form class:
$builder->add('name');
Everything works as expected. Except one thing: if there's a Tag object with the following name, I'm getting SQLSTATE[23000]: Integrity constraint violation MySQL error which is obvious. If I apply unique validation constraint I can add a tag to post (if it already exists in database).
It's obvious I need to check if following tag does exist in database and add it only if does not, but... how to do it Symfony way?
Any suggestions appreciated!
You can use UniqueEntity to handle this. I can't see your annotations on your tags class, or your declaration of 'name' but if you add something like the below it should give you a unique validation constraint based on name with an optional message to throw back.
/**
* #ORM\Entity
* #UniqueEntity(fields="name", message="This tag name already exists")
*/
class Tag...
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
protected $name;