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

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);
}
}
}

Related

Symfony 4 when entity field change, change field in another entity

I have two entities for example:
class Dog
{
/**
* #var House
*
* #ORM\ManyToOne(targetEntity="House")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="house_id", referencedColumnName="id")
* })
*/
private $house;
}
class House
{
/**
* #var ArrayCollection|null
* #ORM\ManyToMany(targetEntity="Dog",cascade={"persist"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="dog_id", referencedColumnName="id", nullable=true)
* })
*/
protected $dog;
}
I need to throw an event if field house in Entity Dog was update (set or remove) then add or remove field dog in Entity House.
Can anyone show me how do this ?
Doctrine will do this for you but depending on the cascade option. But your annotations are not correct. In the Dog entity you have annotation for a ManyToOne and in the House entity for a ManyToMany relation. But you should choose between
ManyToOne - OneToMany
ManyToMany - ManyToMany
Take a look into the Doctrine's association mapping to read about all the types of associations and how to define them.
If you are using Symfony (4 or 5) you should use the commandline make tool to add
properties and methods with all the annotations, even for relations.
bin/console make:entity Dog
Type relation when asked for the Field type and you will have to answer some additional questions.
You must call $dog->setHouse($this); from the addDog method. If you used the commandline then below class House would be generated for you.
class House
{
// ...
/**
* #ORM\OneToMany(targetEntity="App\Entity\Dog", mappedBy="house")
*/
private $dogs;
public function __construct()
{
$this->dogs = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
/**
* #return Collection|Dog[]
*/
public function getDogs(): Collection
{
return $this->dogs;
}
public function addDog(Dog $dog): self
{
if (!$this->dogs->contains($dog)) {
$this->dogs[] = $dog;
$dog->setHouse($this); // <-- here you go
}
return $this;
}
public function removeDog(Dog $dog): self
{
if ($this->dogs->contains($dog)) {
$this->dogs->removeElement($dog);
// set the owning side to null (unless already changed)
if ($dog->getHouse() === $this) {
$dog->setHouse(null);
}
}
return $this;
}
}
Same thing counts for removeDog() method.

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);
}

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

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

Doctrine one to many - persisting multiple files from owning side not working

I have 2 entities Submission and Documents. 1 Submission can have Multiple documents.
Submission Entity:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*/
protected $document;
/**
* #return mixed
*/
public function getDocument()
{
return $this->document->toArray();
}
public function setDocument(Document $document)
{
$this->document[] = $document;
return $this;
}
Document Entity:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Submission", inversedBy="document")
* #ORM\JoinColumn(name="submission_id", referencedColumnName="id",onDelete="cascade", nullable=true)
*/
protected $submission;
public function getSubmission()
{
return $this->submission;
}
/**
* #param mixed $submission
*/
public function setSubmission($submission)
{
$this->submission = $submission;
}
After receiving files dropzonejs - I'm saving them into Document object, and then, i'm try to save this object into Submission, and persist.
$document = new Document();
$em = $this->getDoctrine()->getManager();
$media = $request->files->get('file');
foreach($media as $req){
$document->setFile($req);
$document->setPath($req->getPathName());
$document->setName($req->getClientOriginalName());
$em->persist($document);
}
$submission->setSubmissionStatus(true);
foreach($document as $item){
$submission->setDocument($item);
}
$submission->setUser($user);
$em = $this->getDoctrine()->getManager();
$em->persist($submission);
$em->flush();
Problem is that all the time, i'm receiving error that submission_title is not set, but that's not true, because i have set this field before. I haven't got idea, what is wrong.
I think you'll get some mileage out of following the tutorial over at http://symfony.com/doc/current/doctrine/associations.html, if you haven't already.
I can see that your getters / setters aren't optimal for associating more than one Document with your Submission.
As they write in the Symfony docs, where they want to associate one category with many products, they have the following code:
// src/AppBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
// ...
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
}
From the docs:
The code in the constructor is important. Rather than being
instantiated as a traditional array, the $products property must be of
a type that implements Doctrine's Collection interface. In this case,
an ArrayCollection object is used. This object looks and acts almost
exactly like an array, but has some added flexibility. If this makes
you uncomfortable, don't worry. Just imagine that it's an array and
you'll be in good shape.
So, you'll want to be sure the constructor for your Document entity has something like $this->submissions = new ArrayCollection();. I've changed the property to a plural name, because I think it's more semantically correct. But you can keep your $submission property name, if you like.
Next is to add a addSubmission, removeSubmission, and a getSubmissions method.
Then, your class might end up looking like this:
<?php
// src/AppBundle/Entity/Submission.php
namespace AppBundle\Entity
use Doctrine\Common\Collections\ArrayCollection;
class Submission
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*
* #var ArrayCollection()
*/
protected $documents;
...
/**
* Instantiates the Submission Entity
*
* #return void
*/
public function __construct()
{
$this->documents = new ArrayCollection();
}
/**
* Returns all documents on the Submission
*
* #return mixed
*/
public function getDocuments()
{
return $this->documents;
}
/**
* Add document to this Submission
*
* #param Document $document The object to add to the $documents collection.
*
* #return Submission
*/
public function setDocument(Document $document)
{
$this->documents[] = $document;
return $this;
}
/**
* Remove a document from this Submission
*
* #param Document $document The object to remove from the $documents collection.
*
* #return Submission
*/
public function removeDocument(Document $document)
{
$this->documents->removeElement($document);
return $this;
}
}

In Symfony, is this the best way to prevent duplicates in custom ManyToMany

I have a ManyToMany relationship between Person and FooBar, but the join table needs a couple of extra fields. So I create my own join Entity PersonFooBar, and set Person and FooBar to have OneToMany relationships to PersonFooBar.
This works okay but allows me to add duplicates which I don't want.
To prevent this I have added some code to the add function in the Person anbd FooBar Entities.
So instead of this
class Person
{
/**
* #ORM\OneToMany(targetEntity="Company\MyBundle\Entity\PersonFooBar", mappedBy="person")
*/
protected $person_foo_bars;
/**
* Add person_foo_bar
*
* #param \Company\MyBundle\Entity\PersonFooBar $personFooBar
* #return Person
*/
public function addSurgeryTray(\Company\SurgeryBundle\Entity\PersonFooBar $personFooBar)
{
$this->person_foo_bars[] = $personFooBar;
return $this;
}
}
I have this
class Person
{
/**
* #ORM\OneToMany(targetEntity="Company\MyBundle\Entity\PersonFooBar", mappedBy="person")
*/
protected $person_foo_bars;
/**
* Add person_foo_bar
*
* #param \Company\MyBundle\Entity\PersonFooBar $personFooBar
* #return Person
*/
public function addSurgeryTray(\Company\SurgeryBundle\Entity\PersonFooBar $personFooBar)
{
// Only add if the new personfooBar is for a new fooBar
$fooBarIds = array();
foreach ($this->person_foo_bars as $currentPersonFooBar) {
$fooBarIds[] = $currentPersonFooBar->getFooBar()->getId();
}
if (!in_array($personFooBar->getFooBar()->getId(), $fooBarIds)) {
$this->person_foo_bars[] = $personFooBar;
}
return $this;
}
}
Is this the correct way to handle this?

Resources