How to update an entity and its relations on multiple levels? - symfony

I am making a web app with Symfony 3 and Doctrine that permits to draw a tree structure and persist it.
A node contains subNodes in a OneToMany relation called children, the root of the tree being the only node that is not a child of any other node.
Here is the entity:
/**
* #ORM\Entity(repositoryClass="App\Repository\NodeRepository")
*/
class Node
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
public $id;
/**
* #ORM\Column(type="string", length=255)
*/
public $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Node", mappedBy="parent")
*/
public $children;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Node", inversedBy="children")
* #ORM\JoinColumn(name="id_parent", referencedColumnName="id")
*/
public $parent;
public function __construct()
{
$this->children = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* #return Collection|Node[]
*/
public function getChildren(): Collection
{
return $this->children;
}
public function addChild(Node $child): self
{
if (!$this->children->contains($child)) {
$this->children[] = $child;
$child->setParent($this);
}
return $this;
}
public function removeChild(Node $child): self
{
if ($this->children->contains($child)) {
$this->children->removeElement($child);
// set the owning side to null (unless already changed)
if ($child->getParent() === $this) {
$child->setParent(null);
}
}
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;
return $this;
}
}
I have no particular issue creating a tree. But when it comes to updating it, I am not sure what is the best way to do it.
The main example is removing a node (but the question is also applicable for moving a node) which I will take further down to illustrate my question.
I have a updateNodeAction controller that takes the whole new tree as a parameter (with the doctrine ids set all along the structure). I want to merge the incoming tree with the already persisted one so that removed nodes are removed when performing the flush.
In my dreams, I imagine something like (the parameter $node being the root node with its id set):
public function updateNodeAction(Node $node, EntityManagerInterface $entityManager) {
$entityManager->merge($node);
$entityManager->flush();
return new Response('OK');
}
where Doctrine deals with doing all the modifications of the node itself and all its descendants (additions, removals, moves) in database. For example, if the following tree with root's id is 1 is already persisted in database
and then I call my update controller with the following one (respecting the ids of course)
Doctrine would be able to "see" that node 5 is missing in the new structure and then do the $entityManager->remove($node5) by itself.
Is this not a dream and there is some kind of way to make Doctrine behave like this? Or do I have to recursively go down the tree and make a node-by-node comparison in order to make the modifications via entityManager myself?

I went to the conclusion that Doctrine was not such magical ;)
So I had to:
declare the Doctrine merge operation to propagate along the children relation (with the cascade={"merge"} annotation)
merge the root node
call the $em->remove operation on the nodes that existed on the tree before the merge, and that are not present on the incoming tree
finally, call $em->flush

Related

Symfony/Doctrine Infinite recursion while fetching from database with inverted side of a ManyToOne relationship

Context
In a simple Symfony project, I've created two entities, Product and Category, which are related by a #ManyToOne and a #OneToMany relationship with Doctrine Annotations. One category can have multiple products and one product relates to one category. I've manually inserted data in the Category table.
When I fetch data using Category entity repository and I display it with a var_dump(...), an infinite recursion happens. When I return a JSON response with these data, it is just empty. It should retrieve exactly the data I inserted manually.
Do you have any idea of how to avoid this error without removing the inverse side relationship in the Category entity?
What I've tried
Adding the Doctrine Annotation fetch="LAZY" in one side, the other side and both side of the relationship.
Inserting Category object in the database using Doctrine to see if the database connection is working. Yes it is.
Removing the inverse side of the relationship. It worked but it's not what I want.
Code snippet
Controller
dummy/src/Controller/DefaultController.php
...
$entityManager = $this->getDoctrine()->getManager();
$repository = $entityManager->getRepository(Category::class);
// ===== PROBLEM HERE =====
//var_dump($repository->findOneByName('house'));
//return $this->json($repository->findOneByName('house'));
...
Entities
dummy/src/Entity/Category.php
<?php
namespace App\Entity;
use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=CategoryRepository::class)
*/
class Category
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity=Product::class, mappedBy="category", fetch="LAZY")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* #return Collection|Product[]
*/
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
$product->setCategory($this);
}
return $this;
}
public function removeProduct(Product $product): self
{
if ($this->products->contains($product)) {
$this->products->removeElement($product);
// set the owning side to null (unless already changed)
if ($product->getCategory() === $this) {
$product->setCategory(null);
}
}
return $this;
}
}
dummy/src/Entity/Product.php
<?php
namespace App\Entity;
use App\Repository\ProductRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ProductRepository::class)
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity=Category::class, inversedBy="products", fetch="LAZY")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
}
I assume you use var_dump for debugging purposes. For debugging purposes use dump or dd which is from symfony/debug and should already be enabled on dev by default. Both dump and dd should abort the infinite recursion in time. (Lots of symfony/doctrine objects/services have circular references or just a lot of referenced objects.) dump adds the given php var(s) to either the profiler (target mark symbol in the profiler bar) or to the output. dd adds the given var(s) like dump but also ends the process (so dump and die). - On production never use dump/dd/var_dump, but properly serialize your data.
Secondly, $this->json is essentially a shortcut for packing json_encode into a JsonResponse object (or use the symfony/serializer instead). json_encode on the other hand serializes public properties of the object(s) given unless the object(s) implement JsonSerializable (see below). Since almost all entities usually have all their properties private, the result is usually an empty object(s) serialization.
There are a multitude of options to choose from, but essentially you need to solve the problem of infinite recursion. The imho standard options are:
using the symfony serializer which can handle circular references (which cause the infinite recursion/loop) and thus turning the object into a safe array. However, the results may still not be to your liking...
implementing JsonSerializable on your entity and carefully avoid recursively adding the child-objects.
building a safe array yourself from the object, to pass to $this->json ("the manual approach").
A safe array in this context is one, that contains only strings, numbers and (nested) arrays of strings and numbers, which essentially means, losing all actual objects.
There are probably other options, but I find these the most convenient ones. I usually prefer the JsonSerializable option, but it's a matter of taste. One example for this would be:
class Category implements \JsonSerializable { // <-- new implements!
// ... your entity stuff
public function jsonSerialize() {
return [
'id' => $this->id,
'name' => $this->name,
'products' => $this->products->map(function(Product $product) {
return [
'id' => $product->getId(),
'name' => $product->getName(),
// purposefully excluding category here!
];
})->toArray(),
];
}
}
After adding this your code should just work. For dev, you always should use dump as mentioned and all $this->json will just work. That's why I usually prefer this option. However, the caveat: You only can have one json serialization scheme for categories this way. For any additional ways, you would have to use other options then ... which is almost always true anyway.

Symfony 4: remove collection from entity

I have a product entity and product image entity. I want to use soft delete on product entity only and make a delete on product image entity.
The soft delete works fine. When I delete the product, the deleted_at column is set to current time.
So I would like to delete product image when the deleted_at column is updated.
I was wondering if I can do it directly in entity class? and how?
Product entity where I try to make the collection delation in setDeletedAt function.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
* #ORM\Table(name="product")
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\ProductImage", mappedBy="product", orphanRemoval=true, cascade={"persist"})
*/
private $productImages;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $deleted_at;
public function __construct()
{
$this->productImages = new ArrayCollection();
}
public function setDeletedAt(?\DateTimeInterface $deleted_at): self
{
// Here I try to remove images when deleted_at column is updated
$productImage = $this->getProductImages();
$this->removeProductImage($productImage);
$this->deleted_at = $deleted_at;
return $this;
}
/**
* #return Collection|ProductImage[]
*/
public function getProductImages(): Collection
{
return $this->productImages;
}
public function addProductImage(ProductImage $productImage): self
{
if (!$this->productImages->contains($productImage)) {
$this->productImages[] = $productImage;
$productImage->setProduct($this);
}
return $this;
}
public function removeProductImage(ProductImage $productImage): self
{
if ($this->productImages->contains($productImage)) {
$this->productImages->removeElement($productImage);
// set the owning side to null (unless already changed)
if ($productImage->getProduct() === $this) {
$productImage->setProduct(null);
}
}
return $this;
}
}
But when I make the soft delete, setDeletedAt() is called and the following error is returned:
Argument 1 passed to App\Entity\Product::removeProductImage() must be an instance of App\Entity\ProductImage, instance of Doctrine\ORM\PersistentCollection given, called in ...
Thanks for your help!
---- UPDATE ----
Solution provided by John works fine:
foreach ($this->getProductImages() as $pi) {
$this->removeProductImage($pi);
}
Thanks!
pretty self-explaining error:
at this point:
$productImage = $this->getProductImages();
$this->removeProductImage($productImage);
you are passing a collection instead a single ProductImage object.
to delete them all, just do:
foreach ($this->getProductImages() as $pi) {
$this->removeProductImage($pi);
}

Type error with ArrayCollection / OneToMany relationship in Symfony 3.4

For the past couple of days I have been trying to create a bidirectionnal ManyToOne-OneToMany relationship in Symfony 3.4
I have two entities. One is Contribution and the other is Source. A Contribution can have several sources. So the relationship should be
Contribution – ManyToOne – Source – OneToMany – Contribution
But I keep getting the following error during $em→flush(); in my controller:
Type error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in /var/www/html/Edebate/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 605
I do not have any set method related to the Array Collection in my Entity Contribution as I could see in other posts here:
Type error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given
Symfony-Catchable Fatal Error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given
And the annotations are ok as mentionned here:
Doctrine OneToMany relationship error
Any help would be appreciate ! :)
Here is my Entity Contribution
use Doctrine\Common\Collections\ArrayCollection;
//annotations
abstract class Contribution
{
/**
* #ORM\OneToMany(targetEntity="Shaker\DebateBundle\Entity\Source", mappedBy="parent")
*/
protected $sources;
//Other attributes and methods
public function __construct() {
$this->sources = new ArrayCollection();
}
/**
* Add source
*
* #param \Shaker\DebateBundle\Entity\Source $source
*
* #return Contribution
*/
public function addSource(\Shaker\DebateBundle\Entity\Source $source)
{
$this->sources[] = $source;
return $this;
}
/**
* Remove source
*
* #param \Shaker\DebateBundle\Entity\Source $source
*/
public function removeSource(\Shaker\DebateBundle\Entity\Source $source)
{
$this->sources->removeElement($source);
}
/**
* Get sources
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getSources()
{
return $this->sources;
}
}
And this is in my Entity Source:
/**
* #ORM\ManyToOne(targetEntity="Shaker\DebateBundle\Entity\Contribution", inversedBy="sources")
*/
protected $parent;
/**
* Set parent
*
* #param \Shaker\DebateBundle\Entity\Contribution $parent
*
* #return Contribution
*/
public function setParent(\Shaker\DebateBundle\Entity\Contribution $parent = null)
{
$this->parent = $parent;
$parent->addSource($this);
return $this;
}
/**
* Get parent
*
* #return \Shaker\JRQBundle\Entity\Contribution
*/
public function getParent()
{
return $this->parent;
}
And in my Controller, the problem arises with flush:
$formsourcebook->handleRequest($request);
$contributionid=$formsourcebook->get('ContributionId')->getData();
if ($formsourcebook->isValid()) {
$topicargtarget=$this->getContribution($contributionid);
$sourcebook->setUser($user);
$sourcebook->setContribution($topicargtarget);
$em->persist($sourcebook);
$em->flush();
}
I don't know your question very well. However, did you try with this syntax in the Source entity?
private $parent;
// ...
public function __construct() {
$this->parent = new ArrayCollection();
// or new \Doctrine\Common\Collections\ArrayCollection();
}
I think you're forgetting the constructor in the class.
I think you "switched" some logic when working with collections. Here's how I think your "add" method should look like:
public function addSource(\Shaker\DebateBundle\Entity\Source $source)
{
$this->sources[] = $source;
$source->setParent($this);
return $this;
}
And in the other entity:
public function setParent(\Shaker\DebateBundle\Entity\Contribution $parent = null)
{
$this->parent = $parent;
return $this;
}
There are missing variables in your controller snippet, together with the form fields definitions, so you shouldn't work that much after submitting the form. Try to directly map as many fields as you can (even via autoguessing), and even if it looks ugly, but works, but then you can beautify later. Just my two cents with several months of delay.

How to remove a collection element without EntityManager#remove(...) in Doctrine 2?

I have a classical construction like EntityA OneToMany EntityB. Implemented as a bidirectional relationship:
the EntityB has a property $entityA of type EntityA and
the EntityA has a property $entityBs, that contains an ArrayCollection of EntityB elements.
Now I want to remove some EntityB elements. It would work like this:
$entityManager->remove($myEntityB);
$entityManager->flush();
But I'd like to be able just to "say" $myEntityA->removeEntityB($entityB) and not need to care about anything else. An advantage would be, that I can implement a method EntityA#replaceEntityBs(ArrayCollection $entityBs), that simply removes all EntityA#$entityBs and replace them be the given elements.
Is it possible / How to remove elements of a collection directly from the inverse side of a relationship (of course without to pass the EntityManager into the entity)?
The solution is to remove the reference to EntityA from the EntityB (first). In this case Doctrine will try to persist an EntityB without. But if we combine this with orphanRemoval=true, we'll get the aimed result:
class EntityA
{
...
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="EntityB", mappedBy="entityA", cascade={"persist"}, orphanRemoval=true)
*/
protected $entityBs;
...
public function removeEntityB(EntityB $entityB)
{
$this->entityBs->removeElement($entityB);
$entityB->setEntityA(null);
return $this;
}
...
}
class EntityB
{
...
/**
* #var EntityA
*
* #ORM\ManyToOne(targetEntity="EntityA", inversedBy="entityBs")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="entity_a_id", referencedColumnName="id")
* })
*/
protected $entityA;
...
/**
* #param EntityA $entityA
* #return EntityB
*/
public function setEntityA(EntityA $entityA = null)
{
$this->entityA = $entityA;
return $this;
}
...
}
Off topic: Replacing a collection
Since I noted in the question, that an advantage would be, that one can implement a method like EntityA#replaceEntityBs(ArrayCollection $entityBs), I want to share here a possible implementation.
The first naïve attempt was just to remove all EntityBs and then add (and persist) the new elements.
public function setEntityBs($entityBs)
{
$this->removeEntityBs();
$this->entityBs = new ArrayCollection([]);
/** #var EntityB $entityB */
foreach ($entityBs as $entityB) {
$this->addEntityB($entityB);
}
return $this;
}
public function removeEntityBs()
{
foreach ($this->getEntityBs() as $entityB) {
$this->removeEntityB($entityB);
}
return $this;
}
But if the input collection of the setEntityBs(...) contained existing EntityBs (that shold be updated), it led to deleting of them and only the new elements got persisted.
Here is a solution, that works as wished:
public function setEntityBs($entityBs)
{
$this->removeEntityBsNotInList($entityBs);
$this->entityBs = new ArrayCollection([]);
/** #var EntityB $entityB */
foreach ($entityBs as $entityB) {
$this->addEntityB($entityB);
}
return $this;
}
private function removeEntityBsNotInList($entityBs)
{
foreach ($this->getEntityBs() as $entityB) {
if ($entityBs->indexOf($entityB) === false) {
$this->removeEntityB($entityB);
}
}
}

Symfony OneToMany - ManyToOne entity association not working

I'm training myself on Symfony and struggling with a problem with bidirectional association (very basic) because by dumping my entity in a twig template I verify that data is correct but the association is always null.
My problem is like this one but the solution is not shared.
I read the documentation here and it seems I follow the right steps.
My db contain a Parent table and a Children table related by children.parent_id as foreign key, both table are popolated and I use DOCTRINE:GENERATE:ENTITIES and DOCTRINE:GENERATE:CRUD.
In Parents class I have:
function __construct() {
$this->lastUpd = new \DateTime();
$this->children = new ArrayCollection();
}
/*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Children", mappedBy="parent_id", cascade={"persist"})
*/
private $children;
public function setChildren(ArrayCollection $children) {
return $this->children = $children;
}
public function getChildren() {
return $this->children;
}
In Children class I have:
/**
* #var \AppBundle\Entity\Parents
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Parents", inversedBy="children")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="parent_id", referencedColumnName="parent_id")
* })
*/
private $parent_id;
/**
* Set parent_id
* #param \AppBundle\Entity\Parents $parent_id
* #return Parents
*/
public function setParentID(\AppBundle\Entity\Parents $parent_id= null) {
$this->parent_id = $parent_id;
return $this;
}
/**
* Get parent_id
* #return \AppBundle\Entity\Parents
*/
public function getParentID() {
return $this->parent_id;
}
As additional info looking at Simfony profiler (of parents list page) -> Doctrine -> Entities Mapping I found (with no errors) AppBundle\Entity\Parents and AppBundle\Entity\Type (a working unidirectional OneToMany association).
I am sorry to post a so basic error and I bet the solution is simple but I can't see it.
note: Im assuming that youre not creating an ArrayCollection of children and adding them en'mass.
you dont have any addChild method (which you need to call).
this is easy with an ArrayCollection.
public function addChild(Children $child) {
$this->children->add($child);
}
you could also do with a removeChild as well.
public function removeChild(Children $child) {
$this->children->removeElement($child);
}
then when in your controller.
$child = new Children();
$parent->addChild($child);
then when you persist the parent object, the children will follow due to the cascade persist. I would also add cascade={"remove"} as well, so when you delete the parent, the children will go to.

Resources