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);
}
Related
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.
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);
}
}
}
I am stuck at this case, I reproduced it in an example from symfony documentation, here it how it looks:
FIXTURES
/**
* #ORM\Entity
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category", fetch="EAGER")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function products(): Collection
{
return $this->products;
}
public function id()
{
return $this->id;
}
}
and related Product class
/**
* #ORM\Entity
*/
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
public function __construct($category)
{
$this->category = $category;
}
public function id()
{
return $this->id;
}
public function category()
{
return $this->category;
}
}
TEST
Now I have this snippet of test code where I want to fetch Category and be able to get its Products:
$cat = new Category();
$prod = new Product($cat);
$this->entityManager->persist($prod);
$this->entityManager->persist($cat);
$this->entityManager->flush();
$crepo = $this->getEntityManager()->getRepository(Category::class);
$c = $crepo->findAll()[0];
var_dump(get_class($c->products()), $c->products()->count())
What I am getting is products of class PersistentCollection which is expected, but the count is 0 while there should be 1 product.
I can see that in the database I have proper category and product records with proper foreign key set.
WORKAROUND
I am debugging PersistentCollection for products and can see that its flag is set to initialized = true. With this I am able to force this to work by calling
$c->products()->setInitialized(false);
$c->products()->initialize();
But afaik this is not how it should work, should it ?
I managed to found an answer. It basically works but not when run in the same process. If I split the script in two - first one persists, second retrieves the data then the products collection will contain products related to category.
This is because when it is done in single process doctrine does not know that the category in question has products, and since it retrieves the same object it just saved and that was created few lines above, the entity manager won't populate the collection using database but will use the one from the category object. And the category object does not have products in products collection, since there is no call like $this->products()->add($category) neither in Product constructor or anywhere else. Only forcing to reinitialize the collection works since then it really retrieves products from database
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;
}
}
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?