Symfony Doctrine relationships - delete entity on the inverse side - symfony

my question is how to delete entity on the inverse side without going through every association and delete it manually.
<?php
/** #Entity */
class User
{
// ...
/**
* #OneToMany(targetEntity="Address", mappedBy="user")
*/
private $addresses;
// ...
public function __construct() {
$this->addresses = new ArrayCollection();
}
}
/** #Entity */
class Address
{
// ...
/**
* #ManyToOne(targetEntity="User", inversedBy="features")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
// ...
}
In this example, Address is the owning side, so I can't delete User because foreign key validation will fail. I have to delete Address and then delete User. If I have 10 relationships like these the delete process is painful.
I can create ManyToMany relationship, but this way the Address entity will have users not user and I want addresses to have only one user.
What is the best way to do this?

I hope it's helpful.
Just add cascade to inverse side entity.
/** #Entity */
class User
{
// ...
/**
* #OneToMany(targetEntity="Address", mappedBy="user", cascade={"persist", "remove"})
*/
private $addresses;
// ...
public function __construct() {
$this->addresses = new ArrayCollection();
}
/*
* #return ArrayCollection
*/
public function getAddresses (){
return $this->addresses:
}
/*
* #pram Address $address
*/
public function setAddresses (Address $address){
$this->addresses->add ($address);
}
}

Related

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

Delete an entity inside another entity Symfony

I have 2 entities : User And Love. This is a "One-To-Many, Bidirectional".
Here are my entities:
User:
namespace SE\UserBundle\Entity;
/**
* User
*
* #ORM\Table(name="user")
* #ORM\Entity(repositoryClass="SE\UserBundle\Repository\UserRepository")
*/
class User extends BaseUser
{
//...
/**
* One User has Many Loves.
* #ORM\OneToMany(targetEntity="SE\CoreBundle\Entity\Love", mappedBy="user", cascade={"persist"})
*/
private $loves;
//...
public function __construct()
{
//...
$this->loves = new ArrayCollection();
//...
}
//...
/**
* Remove love
*
* #param \SE\CoreBundle\Entity\Love $love
*/
public function removeLove(\SE\CoreBundle\Entity\Love $love)
{
$this->loves->removeElement($love);
//delete love
}
//...
}
Love :
namespace SE\CoreBundle\Entity;
/**
* Love
*
* #ORM\Table(name="love")
* #ORM\Entity(repositoryClass="SE\CoreBundle\Repository\LoveRepository")
*/
class Love
{
//...
/**
* Many Loves have One User.
* #ORM\ManyToOne(targetEntity="SE\UserBundle\Entity\User", inversedBy="loves", cascade={"persist"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
//...
/**
* Set user
*
* #param \SE\UserBundle\Entity\User $user
*
* #return Love
*/
public function setUser(\SE\UserBundle\Entity\User $user = null)
{
$this->user = $user;
return $this;
}
//...
}
My question is:
Is there a way, when i execute removeLove() on my User, the corresponding Love entity autodelete in my database without using an entity manager in my controller. I would like my controller to look like that:
class DefaultController extends Controller
{
public function indexAction()
{
//...
$em = $this->getDoctrine()->getManager();
$user->removeLove($love);
$em->persist($user);
$em->flush();
//...
}
}
And the result would be that, $love would not be in getLoves() of the $user and $love would be delete of the database.
You could try: Orphan Removal option
Make sure Entity Love is not associated with another User. So one Entity Love is always associated with only one User

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

OneToOne in Symfony 2

I try to save related entities User and Profile. I use the cascade={"persist"} option. Data saves properly in Database except user_id field in Profile table, its equal to null. When i turn relations profile_id field in User table saves properly. Where is my mistake? Here is my relations:
User entity:
/**
* #ORM\OneToOne(targetEntity="Profile", mappedBy="user", ,cascade={"persist"})
*/
Profile entity:
/**
* #ORM\OneToOne(targetEntity="User", inversedBy="profile", cascade={"persist"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
Getters & setters:
User:
` /**
* #param Profile $profile
*/
public function setProfile(Profile $profile)
{
$this->profile = $profile;
}
/**
* #return Profile
*/
public function getProfile()
{
return $this->profile;
}`
Profile:
`/**
* #param User $user
*/
public function setUser(User $user)
{
$this->user = $user;
}
/**
* #return User
*/
public function getUser()
{
return $this->user;
}`
This is essentially #skler answer but I couldn't explain myself in comments.
Try doing this in your User.php's setProfile():
public function setProfile(Profile $profile)
{
$this->profile = $profile;
$this->profile->setUser($this);
}
Maybe you add only profile in user: $user->addProfile($profile); and you didn't the viceversa: $profile->addUser($user);

Doctrine: Get auto increment ID before persist/flush

I'm having trouble in Doctrine-Fixtures. I'd like to add a user and a email in another entity, but in relation to the user. So here is my process:
// Create user
$user1 = new User();
// Create user email and add the foreign key to the user
$user1Mail = new UserEmail();
$user1Mail->setEmail('test#example.com');
$user1Mail->setUser($user1);
// Add attributes
$user1->setEmail($user1Mail);
// ...
$manager->persist($user1Mail);
$manager->persist($user1);
$manager->flush();
I add the user of the email in $user1Mail->setUser($user1); before the persist, but the problem is, the user has no primary key --> the ID (auto increment). So to create the relation between the email and the user, the user needs to have a primary key to refer to.
I know the solution to create a unique token before and set this to the ID of the user, but I think this is a uncomfortable way because I need to check if the user ID is already in use.
Is there a good way to handle this?
// EDIT:
Here is the necessary entity relation:
User:
class User implements UserInterface, \Serializable
{
// ...
/**
* #var Application\CoreBundle\Entity\UserEmail
*
* #ORM\OneToOne(
* targetEntity="UserEmail",
* cascade={"persist"}
* )
* #ORM\JoinColumn(
* name="primaryEmail",
* referencedColumnName="id",
* nullable=false,
* onDelete="restrict"
* )
*/
private $email;
// ...
}
UserEmail:
class UserEmail
{
// ...
/**
* #var Application\CoreBundle\Entity\User
* #ORM\ManyToOne(
* targetEntity="User",
* cascade={"persist", "remove"}
* )
* #ORM\JoinColumn(
* name="userID",
* referencedColumnName="id",
* nullable=false
* )
*/
private $user;
// ...
}
As you can see, if you add a user you have to add a UserEmail also. But the UserEmail requires that the userID is already set, but it is only set if you persist the user into the db. How can I realize a fix for it?
I find it strange to see that your User has a OneToOne association towards UserEmail, and UserEmail has a ManyToOne association towards User, and those are 2 separate associations.
I think you'd rather have a single bidirectional OneToMany association:
class User implements UserInterface, \Serializable
{
// ...
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="UserEmail", mappedBy="user", cascade={"persist", "remove"})
*/
private $emails;
public function __construct()
{
$this->emails = new ArrayCollection();
}
/**
* #param UserEmail $email
*/
public function addEmail(UserEmail $email)
{
$this->emails->add($email);
$email->setUser($this);
}
/**
* #param UserEmail $email
*/
public function removeEmail(UserEmail $email)
{
$this->emails->removeElement($email);
$email->setUser(null);
}
/**
* #return UserEmail[]
*/
public function getEmails()
{
return $this->emails->toArray();
}
// ...
}
class UserEmail
{
// ...
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="User", inversedBy="emails")
* #ORM\JoinColumn(name="userID", referencedColumnName="id", nullable=FALSE)
*/
private $user;
/**
* #param User $user
*/
public setUser(User $user = null)
{
$this->user = $user;
}
/**
* #return User[]
*/
public function getUser()
{
return $this->user;
}
// ...
}
I've put a cascade on User::$emails, so any changes to User get cascaded towards UserEmail. This will make managing them easier.
Using this would look something like this:
$email = new UserEmail();
$user = new User();
$user->addEmail($email);
$em->persist($user);
$em->flush();
About foreign keys
Doctrine will manage the foreign keys of your entities for you. You don't need to manually set them on your entities when using associations.
Primary email
Personally I would add a property to UserEmail to mark it as primary. You'll need a bit more logic in the entities, but managing them will become effortless.
Here's the additional code you need:
class User
{
// ...
/**
* #param UserEmail $email
*/
public function addEmail(UserEmail $email)
{
$this->emails->add($email);
$email->setUser($this);
$this->safeguardPrimaryEmail();
}
/**
* #param UserEmail $email
*/
public function removeEmail(UserEmail $email)
{
$this->emails->removeElement($email);
$email->setUser(null);
$this->safeguardPrimaryEmail();
}
/**
* #param UserEmail $email
*/
public function setPrimaryEmail(UserEmail $newPrimaryEmail)
{
if (!$this->emails->contains($newPrimaryEmail)) {
throw new \InvalidArgumentException('Unknown email given');
}
foreach ($this->emails as $email) {
if ($email === $newPrimaryEmail) {
$email->setPrimary(true);
} else {
$email->setPrimary(false);
}
}
}
/**
* #return UserEmail|null
*/
public function getPrimaryEmail()
{
foreach ($this->emails as $email) {
if ($email->isPrimary()) {
return $email;
}
}
return null;
}
/**
* Make sure there's 1 and only 1 primary email (if there are any emails)
*/
private function safeguardPrimaryEmail()
{
$primaryFound = false;
foreach ($this->emails as $email) {
if ($email->isPrimary()) {
if ($primaryFound) {
// make sure there's no more than 1 primary email
$email->setPrimary(false);
} else {
$primaryFound = true;
}
}
}
if (!$primaryFound and !$this->emails->empty()) {
// make sure there's at least 1 primary email
$this->emails->first()->setPrimary(true);
}
}
// ...
}
class UserEmail
{
// ...
/**
* #var boolean
*
* #ORM\Column(type="boolean")
*/
private $isPrimary = false;
/**
* #internal Use
* #param bool $isPrimary
*/
public function setPrimary($isPrimary)
{
$this->isPrimary = (bool)$isPrimary;
}
/**
* #return bool
*/
public function isPrimary()
{
return $this->isPrimary;
}
// ...
}
You'll probably notice safeguardPrimaryEmail(). This will make sure the primary-mark will remain consistent when adding/removing emails.
Using this is very simple:
An email that's created is not primary by default.
If it's the first email added to a user, it will automatically become primary.
Additionally added emails will remain not primary.
When the primary email is removed, the first remaining email will become primary.
You can manually set another primary email by calling User::setPrimaryEmail().
There are many variations to this concept possible, so just view this as an example and refine it to your needs.
It's because Doctrine will generate the entity id when it's inserted into the database.
You can do it with an extra flush():
$user1 = new User();
$manager->persist($user1);
$manager->flush();
// Create user email and add the foreign key to the user
$user1Mail = new UserEmail();
$user1Mail->setEmail('test#example.com');
$user1Mail->setUser($user1);
// Add attributes
$user1->setEmail($user1Mail);
// ...
$manager->persist($user1Mail);
$manager->persist($user1);
$manager->flush();
Or you can set the email mapping in your User class to cascade persist.
It means that if a new not persisted object is added to that object, then the new object will be saved as well.
I don't know the exact structure of the entites, but it would look like
/**
* #ORM\OneToOne(targetEntity="user", cascade={"persist"})
* #ORM\JoinColumn(name="user_email_id", referencedColumnName="id")
*/
private $userEmail
So if you set a new e-mail to the user it will be auto-persisted if you persist the User entity.
I would prefer the second method if it works. I hope it will help.
Doctrine reference: Transitive persistence / Cascade Operations

Resources