Symfony Form ManyToOne OneToMany - symfony

I have three entities, Block, BlockPlacement, BlockPosition:
class BlockEntity
{
private $bid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="block",
* cascade={"remove"})
*/
private $placements;
}
class BlockPlacementEntity
{
/**
* The id of the block postion
*
* #ORM\Id
* #ORM\ManyToOne(targetEntity="BlockPositionEntity", inversedBy="placements")
* #ORM\JoinColumn(name="pid", referencedColumnName="pid", nullable=false)
*/
private $position;
/**
* The id of the block
*
* #var BlockEntity
* #ORM\Id
* #ORM\ManyToOne(targetEntity="BlockEntity", inversedBy="placements")
* #ORM\JoinColumn(name="bid", referencedColumnName="bid", nullable=false)
*/
private $block;
private $sortorder;
}
class BlockPositionEntity
{
private $pid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="position",
* cascade={"remove"})
* #ORM\OrderBy({"sortorder" = "ASC"})
*/
private $placements;
}
So, you can see the relationship: Block < OneToMany > Placement < ManyToOne > Position.
Now I am trying to construct a form to create/edit a block:
$builder
->add($builder->create('placements', 'entity', [
'class' => 'Zikula\BlocksModule\Entity\BlockPositionEntity',
'choice_label' => 'name',
'multiple' => true,
'required' => false
]))
;
This gives me a good select box with multiple selections possible with a proper list of positions to choose from. But it does not show previous selections for placement (I am using existing data) e.g. marking positions as 'selected'. I have not tried creating a new Block yet, only editing existing data.
I suspect I will need to be using addModelTransformer() or addViewTransformer() but have tried some of this an cannot get it to work.
I've looked at the collection form type and I don't like that solution because it isn't a multi-select box. It requires JS and isn't as intuitive as a simple select element.
This seems like such a common issue for people. I've searched and found no common answer and nothing that helps.

Update: please look at this example repo
Update 2: i've updated the repo.
I did it with form event listeners and unmapped choice field.
Take a closer look at BlockType form type
Feel free to ask any questions about it.

OK - so in the end, I found a different way. #Stepan Yudin's answer worked, but is complicated (listeners, etc) and not quite like I was hoping.
So, I have the same three entities. BlockPlacement and BlockPosition remain the same (and so aren't reposted, see above) but I have made some changes to the BlockEntity:
class BlockEntity
{
private $bid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="block",
* cascade={"remove", "persist"},
* orphanRemoval=true)
*/
private $placements;
/**
* Get an ArrayCollection of BlockPositionEntity that are assigned to this Block
* #return ArrayCollection
*/
public function getPositions()
{
$positions = new ArrayCollection();
foreach($this->getPlacements() as $placement) {
$positions->add($placement->getPosition());
}
return $positions;
}
/**
* Set BlockPlacementsEntity from provided ArrayCollection of positionEntity
* requires
* cascade={"remove, "persist"}
* orphanRemoval=true
* on the association of $this->placements
* #param ArrayCollection $positions
*/
public function setPositions(ArrayCollection $positions)
{
// remove placements and skip existing placements.
foreach ($this->placements as $placement) {
if (!$positions->contains($placement->getPosition())) {
$this->placements->removeElement($placement);
} else {
$positions->removeElement($placement->getPosition()); // remove from positions to add.
}
}
// add new placements
foreach ($positions as $position) {
$placement = new BlockPlacementEntity();
$placement->setPosition($position);
// sortorder is irrelevant at this stage.
$placement->setBlock($this); // auto-adds placement
}
}
}
So you can see that the BlockEntity is now handling a positions parameter which doesn't exist in the entity at all. Here is the relevant form component:
$builder
->add('positions', 'Symfony\Bridge\Doctrine\Form\Type\EntityType', [
'class' => 'Zikula\BlocksModule\Entity\BlockPositionEntity',
'choice_label' => 'name',
'multiple' => true,
])
note that I have changed to Symfony 2.8 form style since my first post
This renders a multiple select element on the page which accepts any number of positions and converts them to an ArrayCollection on submit. This is then handled directly by the form's get/set position methods and these convert to/from the placement property. The cascade and orphanRemoval are important because they take care to 'clean up' the leftover entities.
because it is references above here is the BlockPlacement setBlock($block) method:
public function setBlock(BlockEntity $block = null)
{
if ($this->block !== null) {
$this->block->removePlacement($this);
}
if ($block !== null) {
$block->addPlacement($this);
}
$this->block = $block;
return $this;
}

Related

Symfony 5 Object Serialization with ManyToMany Relation Times Out

In my Symfony 5 application, I have an entity class Product which has two properties $categories and $bundles. The product class has a ManyToMany relation with both the properties. When I comment out either one of the properties the Product serialization works perfectly. But incase both properties are present the serialization times out.
The code excerpt from Product class.
class Product
{
/**
* #ORM\ManyToMany(targetEntity=ProductBundle::class, mappedBy="products")
*/
private $productBundles;
/**
* #ORM\ManyToMany(targetEntity=Category::class, mappedBy="products")
* #MaxDepth(1)
*/
private $categories;
}
The code for the serialization is below.
$products = $productRepository->findBySearchQuery($name);
$productsJson = $serializerInterface->serialize($products, 'json', [
ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
return $object->getId();
}
]);
I have tried using the #ORM/JoinTable annotation suggested on some other Stackoverflow answers and #MaxDepth as well but no luck. The code works if any of the properties are commented out. Would be grateful for any advice on this.
okay, 20 products is actually not much. so I guess you're outputting the same objects over and over again if you let the relations be serialized unhindered.
I actually don't know how to achieve this reliably with the serializer. But the standard ways would just be enough probably. I like serializing via the JsonSerializable interface on your entities like this (omitting the ORM stuff for brevity):
class Product implements \JsonSerializable {
public $name;
public $categories; // relation
// getters + setters omitted
// this implements \JsonSerializable
public function jsonSerialize() {
return [
'name' => $this->name,
'categories' => array_map(function($category) {
return $category->jsonSerializeChild();
}, $this->categories),
];
}
// this function effectively stops recursion by leaving out relations
public function jsonSerializeChild() {
return [
'name' => $this->name,
];
}
}
If you implement this on all your entities you can very effectively limit the depth of serialization to two (i.e. the "base" entities and their connected entities).
also, the symfony serializer will use the JsonSerializable interface if it's defined if your serializing to JSON. Obviously, this is not as elegant as some fancy annotation-based serialization or a "smart" serializer, that actually manages to stop ... but it'll probably work better...
Pointed out by #Jakumi the serializer was looping over and over the object properties $categories and $bundles. I avoided that by using the Serialization groups.
The product class
class Product
{
/**
* #ORM\ManyToMany(targetEntity=ProductBundle::class, mappedBy="products")
* #Groups("product_listing:read")
*/
private $productBundles;
/**
* #ORM\ManyToMany(targetEntity=Category::class, mappedBy="products")
* #Groups("product_listing:read")
*/
private $categories;
}
The category class
class Category
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups("product_listing:read")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups("product_listing:read")
*/
private $name;
}
The call to serializer
$products = $productRepository->findBySearchQuery($name);
$productsJson = $serializerInterface->serialize($products, 'json', ['groups' => 'product_listing:read']);
I hope this helps someone in future.

Typo3 Error: The ColumnMap for property is missing (m:n)

I am having the same issue that was posted here:
Typo3 Error: The ColumnMap for property is missing
...except I am using a m:n relational table. Unfortunately my error continues:
I'm using Typo3 version 8.7.19 and I'm developing an extention. The two tables "mitarbeiter" and "zusatzlich" are connectet with a m:n relation. I try to search for a field in the table "zusatzlich" in the repository of "mitarbeiter". The relation of both is necessary.
If I try to execute the following query I get the error "The ColumnMap for property "tx_khsjmitarbeiter_domain_model_zusatzlich" of class "...\Mitarbeiter" is missing."
$query = $this->createQuery();
$zu = [];
if($zusatz1 != ""){
$zu[] = $query->equals('tx_khsjmitarbeiter_domain_model_zusatzlich.zusatz', $zusatz1);
}
if(count($zu)>0){
$query->matching($query->logicalAnd( $zu ));
}
return $query->execute();
The relevant TCA code of the field "connection_id" in "mitarbeiter" which contains the UID of "zusatzlich":
'connection_id' => [
'exclude' => true,
'label' => 'LLL:EXT:khsj_mitarbeiter/Resources/Private/Language/locallang_db.xlf:tx_khsjmitarbeiter_domain_model_mitarbeiter.connection_id',
'config' => [
'type' => 'select',
'renderType' => 'selectCheckBox',
'foreign_table' => 'tx_khsjmitarbeiter_domain_model_zusatzlich',
'MM' => 'tx_khsjmitarbeiter_mitarbeiter_zusatzlich_mm',
],
],
This is the object model:
/**
* connectionId
*
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\..\Model\Zusatzlich>
* #cascade remove
*/
protected $connectionId = null;
/**
* Initializes all ObjectStorage properties
* Do not modify this method!
* It will be rewritten on each save in the extension builder
* You may modify the constructor of this class instead
*
* #return void
*/
protected function initStorageObjects()
{
$this->connectionId = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
}
/**
* Adds a Zusatzlich
*
* #param ..\Model\Zusatzlich $connectionId
* #return void
*/
public function addConnectionId(..\Model\Zusatzlich $connectionId)
{
$this->connectionId->attach($connectionId);
}
/**
* Removes a Zusatzlich
*
* #param \..\Model\Zusatzlich $connectionIdToRemove The Zusatzlich to be removed
* #return void
*/
public function removeConnectionId(\..\Model\Zusatzlich $connectionIdToRemove)
{
$this->connectionId->detach($connectionIdToRemove);
}
/**
* Returns the connectionId
*
* #return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\..\Model\Zusatzlich> connectionId
*/
public function getConnectionId()
{
return $this->connectionId;
}
/**
* Sets the connectionId
*
* #param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\..\Model\Zusatzlich> $connectionId
* #return void
*/
public function setConnectionId(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $connectionId)
{
$this->connectionId = $connectionId;
}
I can add and apply new zusatz items in the BE to any mitarbeiter item so I am confident it is set up properly in that respect.
However I also noticed that if I change this line:
$zu[] = $query->equals('tx_khsjmitarbeiter_domain_model_zusatzlich.zusatz', $zusatz1);
...to this...
$zu[] = $query->equals('ANYTHINGATALL.zusatz', $zusatz1);
I get the same error referencing ANYTHINGATALL instead of tx_khsjmitarbeiter_domain_model_zusatzlich
Can anybody point me in the right direction?
j4k3’s answer led me to the right direction but is missing the property of the related model “zusatzlich”. Thus it should be:
if ($zusatz1 != "") {
$zu[] = $query->contains('connection_id.zusatz', $zusatz1);
}
It will be transformed into an SQL LeftJoin with the correct table names, given your relations are properly defined in the TCA.
In case of a property with underscore (e.g. zusatz_xy), the lowerCamelCase version will also work (zusatzXy).
You need to supply a property that is described in the TCA as constraint operator, not a table column. As far as I can tell, your query constraint should be:
if($zusatz1 != ""){
$zu[] = $query->contains('connection_id', $zusatz1);
}

cascade persist option doesnt work on ajax submit(api)

I'm working with symfony 3.4.6 and fosrestbundle. I have three entities related as follow:
Embarque class
class Embarque{
//...
/**
* #var EmbarqueContenedor[]|ArrayCollection
*
* #Serializer\SerializedName("contenedores")
* #ORM\OneToMany(targetEntity="AppBundle\Entity\EmbarqueContenedor",mappedBy="embarque",cascade={"persist"})
*/
private $contenedores;
public function addEmbarqueContenedor($embarqueContenedor)
{
if (!$this->contenedores->contains($embarqueContenedor)) {
$this->contenedores->add($embarqueContenedor);
//$embarqueContenedor->setEmbarque($this);
}
}
public function removeEmbarqueContenedor($embarqueContenedor)
{
if ($this->contenedores->contains($embarqueContenedor)) {
$this->contenedores->removeElement($embarqueContenedor);
}
}
}
EmbarqueContenedor class
class EmbarqueContenedor{
/**
* #var Embarque
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Embarque",inversedBy="contenedores",)
*/
private $embarque;
/**
* #var Contenedor
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Contenedor",inversedBy="embarques")
*/
private $contenedor;
}
Contenedor class
class Contenedor{
/**
* #var EmbarqueContenedor[]|ArrayCollection
*
* #Serializer\SerializedName("contenedorEmbarques")
* #ORM\OneToMany(targetEntity="AppBundle\Entity\EmbarqueContenedor",mappedBy="contenedor")
*/
private $embarques;
public function addEmbarqueContenedor($embarqueContenedor)
{
if (!$this->embarques->contains($embarqueContenedor)) {
$this->embarques->add($embarqueContenedor);
$embarqueContenedor->setContenedor($this);
}
}
public function removeEmbarqueContenedor($embarqueContenedor)
{
if ($this->embarques->contains($embarqueContenedor)) {
$this->embarques->removeElement($embarqueContenedor);
}
}
}
in forms is as follow
class EmbarqueType{
$builder->add('contenedores', CollectionType::class, [
'entry_type' => EmbarqueContenedorType::class,
'allow_add' => true,
]);
}
class EmbarqueContenedorType{
$builder->add('contenedor', EntityType::class, [
'class' => Contenedor::class,
])
}
The entity contenedor is create apart and selected in EmbarqueContenedorType when adding or editing, the EmbarqueContenedorEntity is created from EmbarqueType.
The problem is that the records are persisted in the database but with out any reference. The EmbarqueContenedor table has no reference for the Embarque or Contenedor tables.
There is no error because the data is persisted but not referenced. How could this be??
Thanks in advance!
Edit
I noticed that I was not serializing the Id property of Contenedor Entity so is imposible to make the reference, now is fixed but the Embarque entity still not being referenced.
I think the problem is in the design of the tables relationships. The table/entity EmbarqueContenedor is unnecessary. When you have a many-to-many relation just say that to the Doctrine and the Doctrine takes care of the rest (Doctrine will create all necessary tables).
So the solution should be to define your relations with ManyToMany annotation.

Symfony2 entity relationships not working as expected

This is either a huge bug in Symfony2 or I'm just not getting it. I've spent literally days trying to understand what is going on here. I have two entities:
Event
Date
I want a relationship where there are many dates to one event. Sounds simple enough, so in my Event entity I have:
/**
* #ORM\OneToMany(targetEntity="Date", mappedBy="event")
*/
protected $dates;
And in my Date entity I have:
/**
* #ORM\ManyToOne(targetEntity="Event", inversedBy="dates")
*/
private $event;
I have also generated a CRUD (doctrine:generate:crud) on the Event entity so that I may add events to the database. In the form builder in my EventType I have added:
->add('date', new DateType())
This is so that I may include the date field in the form, as per the Symfony documentation.
Now comes my problem.
Whenever I run doctrine:generate:entities my entities are created on the Event and Date entity, but they seem to be the wrong way around. On my Event entity I get:
/**
* Constructor
*/
public function __construct()
{
$this->dates = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add dates
*
* #param \Raygun\EventBundle\Entity\Date $dates
* #return Event
*/
public function addDate(\Raygun\EventBundle\Entity\Date $dates)
{
$this->dates[] = $dates;
return $this;
}
/**
* Remove dates
*
* #param \Raygun\EventBundle\Entity\Date $dates
*/
public function removeDate(\Raygun\EventBundle\Entity\Date $dates)
{
$this->dates->removeElement($dates);
}
/**
* Get dates
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getDates()
{
return $this->dates;
}
and on my Date entity I get:
/**
* Set event
*
* #param \Raygun\EventBundle\Entity\Event $event
* #return Date
*/
public function setEvent(\Raygun\EventBundle\Entity\Event $event = null)
{
$this->event = $event;
return $this;
}
/**
* Get event
*
* #return \Raygun\EventBundle\Entity\Event
*/
public function getEvent()
{
return $this->event;
}
Now when I try to load the form so I can add the event/date to the database I get
Neither the property "date" nor one of the methods "getDate()", "date()", "isDate()", "hasDate()", "__get()" exist and have public access in class "Raygun\EventBundle\Entity\Event".
It's like it should be adding getters and setters to the Event entity, NOT the Date entity. I'm really tearing my hair out with this and am thinking of ditching Symfony entirely, as it seems to go completely against logic.
if you want Form component automatically mappes the fields you should change date field to dates:
->add('dates', 'collection', [
'type' => new DateType()
])
or you can add mapped => false option to your date field to map it manually.
Your Event form type should contain a collection type for the protected $dates field, so this line is incorrect:
->add('date', new DateType())
It should be:
->add('dates', 'collection', array('type' => new DateType()))
Please have a look at this Symfony cookbook entry on how to work with form collections:
http://symfony.com/doc/current/cookbook/form/form_collections.html

Collection Prototype and Doctrine Persistance ManyToOne Relation

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!

Resources