Foreign Key issue in a virtual many-to-many user relationship when removing user - doctrine

In my Symfony app, I have to handle some kind of friendship requests. It's basically a many to many relation between users, but with an accepted boolean property, and a createdAt DateTime, hence why I created an independent "friendship" entity instead of just a Many to many relations in the user entity.
Here is the issue though: I have a Sender property (one to many relation to user) and a recipient property (one to many relation to user as well). I set the orphanRemoval to true on the Friendships property in the user.
However, if I delete a user with only friendships where he was the sender, it works well and deletes the friendship entities. But if he is a recipient of a friend request, it just doesn't work and returns a Foreign Key constraint error, more precisely
SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or
update a parent row: a foreign key constraint fails
(portail.friendship, CONSTRAINT FK_7234A45FE92F8F78
FOREIGN KEY (recipient_id) REFERENCES user (id)).
I reckon it has something to do with the fact that user appears twice in my friendship entity, and obviously, it seems like only the sender is mentioned in the addFriendship() / removeFriendship() methods, but I'm not sure how to fix it, and I'd like to know if maybe I didn't tackle the issue the right way, and what I could do to change this and make it work (ie: remove all Friendships related to the User, whether he's the sender or recipient).
Below is the friendship entity, as well as the part of the User entity mentioning the Friendship entity relation.
<?php
namespace App\Entity;
use App\Repository\FriendshipRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=FriendshipRepository::class)
*/
class Friendship
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="friendships")
* #ORM\JoinColumn(nullable=false)
*/
private ?User $sender;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="friendships")
* #ORM\JoinColumn(nullable=false)
*/
private ?User $recipient;
/**
* #ORM\Column(type="boolean")
*/
private ?bool $accepted;
/**
* #ORM\Column(type="datetime_immutable")
*/
private ?\DateTimeImmutable $createdAt;
public function getId(): ?int
{
return $this->id;
}
public function getSender(): ?User
{
return $this->sender;
}
public function setSender(?User $sender): self
{
$this->sender = $sender;
return $this;
}
public function getRecipient(): ?User
{
return $this->recipient;
}
public function setRecipient(?User $recipient): self
{
$this->recipient = $recipient;
return $this;
}
public function getAccepted(): ?bool
{
return $this->accepted;
}
public function setAccepted(bool $accepted): self
{
$this->accepted = $accepted;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
}
Part of the user
/**
* #ORM\OneToMany(targetEntity=Friendship::class, mappedBy="sender", orphanRemoval=true)
*/
private $friendships;
/**
* #return Collection|Friendship[]
*/
public function getFriendships(): Collection
{
return $this->friendships;
}
public function addFriendship(Friendship $friendship): self
{
if (!$this->friendships->contains($friendship)) {
$this->friendships[] = $friendship;
$friendship->setSender($this);
}
return $this;
}
public function removeFriendship(Friendship $friendship): self
{
if ($this->friendships->removeElement($friendship)) {
// set the owning side to null (unless already changed)
if ($friendship->getSender() === $this) {
$friendship->setSender(null);
}
}
return $this;
}

Arleigh was indeed correct. What I did wrong was set the same name, ie Friendships, while updating my friendship entity to get both relations in the user, whether he was a sender or recipient. I thought this would allow me to more easily get all friendships related to a user in my controllers and twig template, regardless of them being a recipient or a sender but in the end, all it did was make it all go wrong.
I fixed it by having two separate properties in my user, the friendshipRequestsSent(mapped by the sender field of the Friendship entity) and friendshipRequestsReceived (mapped by the recpient field of the Friendship entity) as well as the methods and typing of the properties associated. This is what it looks like in my User entity now
/**
* #ORM\OneToMany(targetEntity=Friendship::class, mappedBy="sender", orphanRemoval=true)
*/
private $friendshipRequestsSent;
/**
* #ORM\OneToMany(targetEntity=Friendship::class, mappedBy="recipient", orphanRemoval=true)
*/
private $friendshipRequestsReceived;
public function __construct()
{
$this->userSkills = new ArrayCollection();
$this->experiences = new ArrayCollection();
$this->missions = new ArrayCollection();
$this->friendshipRequestsSent = new ArrayCollection();
$this->friendshipRequestsReceived = new ArrayCollection();
}
/**
* #return Collection|Friendship[]
*/
public function getFriendshipRequestsSent(): Collection
{
return $this->friendshipRequestsSent;
}
public function addFriendshipRequestSent(Friendship $friendship): self
{
if (!$this->friendshipRequestsSent->contains($friendship)) {
$this->friendshipRequestsSent[] = $friendship;
$friendship->setSender($this);
}
return $this;
}
public function removeFriendshipRequestSent(Friendship $friendship): self
{
if ($this->friendshipRequestsSent->removeElement($friendship)) {
// set the owning side to null (unless already changed)
if ($friendship->getSender() === $this) {
$friendship->setSender(null);
}
}
return $this;
}
/**
* #return Collection|Friendship[]
*/
public function getFriendshipRequestsReceived(): Collection
{
return $this->friendshipRequestsReceived;
}
public function addFriendshipRequestReceived(Friendship $friendship): self
{
if (!$this->friendshipRequestsReceived->contains($friendship)) {
$this->friendshipRequestsReceived[] = $friendship;
$friendship->setSender($this);
}
return $this;
}
public function removeFriendshipRequestReceived(Friendship $friendship): self
{
if ($this->friendshipRequestsReceived->removeElement($friendship)) {
// set the owning side to null (unless already changed)
if ($friendship->getSender() === $this) {
$friendship->setSender(null);
}
}
return $this;
}
And this is how my Friendship entity sender and recipient properties are declared
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="friendshipRequestsSent")
* #ORM\JoinColumn(nullable=false)
*/
private ?User $sender;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="friendshipRequestsReceived")
* #ORM\JoinColumn(nullable=false)
*/
private ?User $recipient;

Related

how to avoid creating a slug that already exists as a Route "Symfony 4"

I have the entity Page identified by slug. Also I have the action to view a page in the Page controler :
class PageController extends AbstractController
{
/**
* #Route("/{slug}", name="fronend_page")
*/
public function show(Page $page)
{
return $this->render("font_end/page/show.html.twig", [
"page" => $page,
]);
}
}
I am looking for good practice to validate the slug ( check if exist in routes) before save it in the database without use prefixes
Example :
route exist : #route ("/blog")
check if blog exist before create slug : /{slug} = /blog
thanks
You can use the UniqueEntity annotation for checking slug for unique.
For example, in your entity add UniqueEntity annotation with the slug field.
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #UniqueEntity("slug")
*/
class Page
{
/**
* #ORM\Column(type="string", length=255, unique=true)
* #Assert\NotBlank
*/
private $slug;
public function __construct(string $slug)
{
$this->slug = $slug;
}
}
Then you can create some service or do validation in your controller.
/**
* #Route("/{slug}")
*/
public function show($slug, ValidatorInterface $validator)
{
$page = new Page($slug);
$errors = $validator->validate($author);
if (count($errors) > 0) {
// handle errors
}
// save entity
}
Update:
For checking already existing routes probably you can do something like this
public function isRouteExist(string $slug): bool
{
/** #var Symfony\Component\Routing\RouterInterface $router */
$routes = array_filter(array_map(function (\Symfony\Component\Routing\Route $route) {
if (!$route->compile()->getVariables()) {
return $route->getPath();
}
return null;
}, $router->getRouteCollection()->all()));
return in_array(sprintf('/%s', $slug), $routes, true);
}
I created a validation Symfony :
class ContainsCheckSlugValidator extends ConstraintValidator
{
private $router;
public function __construct(UrlGeneratorInterface $router)
{
$this->router = $router;
}
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ContainsCheckSlug) {
throw new UnexpectedTypeException($constraint, ContainsCheckSlug::class);
}
// custom constraints should ignore null and empty values to allow
// other constraints (NotBlank, NotNull, etc.) take care of that
if (null === $value || "" === $value) {
return;
}
$routes = $this->router->getRouteCollection()->all();
$routes = array_map(function($route){
return $route->getPath();
},$routes);
if (in_array(sprintf("/{_locale}/%s/", $value), $routes, true)) {
$this->context->buildViolation($constraint->message)
->setParameter("{{ string }}", $value)
->addViolation();
}
}
}
Thanks #Ihor_Kostrov :)

How to validate unique entities in an entity collection in symfony2

I have an entity with a OneToMany relation to another entity, when I persist the parent entity I want to ensure the children contain no duplicates.
Here's the classes I have been using, the discounts collection should not contain two products with the same name for a given client.
I have a Client entity with a collection of discounts:
/**
* #ORM\Entity
*/
class Client {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=128, nullable="true")
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="Discount", mappedBy="client", cascade={"persist"}, orphanRemoval="true")
*/
protected $discounts;
}
/**
* #ORM\Entity
* #UniqueEntity(fields={"product", "client"}, message="You can't create two discounts for the same product")
*/
class Discount {
/**
* #ORM\Id
* #ORM\Column(type="string", length=128, nullable="true")
*/
protected $product;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Client", inversedBy="discounts")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
*/
protected $client;
/**
* #ORM\Column(type="decimal", scale=2)
*/
protected $percent;
}
I tried using UniqueEntity for the Discount class as you can see, the problem is that it seems the validator only checks what's loaded on the database (which is empty), so when the entities are persisted I get a "SQLSTATE[23000]: Integrity constraint violation".
I have checked the Collection constraint buy it seems to handle only collections of fields, not entities.
There's also the All validator, which lets you define constraints to be applied for each entity, but not to the collection as a whole.
I need to know if there are entity collection constraints as a whole before persisting to the database, other than writing a custom validator or writing a Callback validator each time.
I've created a custom constraint/validator for this.
It validates a form collection using the "All" assertion, and takes an optional parameter : the property path of the property to check the entity equality.
(it's for Symfony 2.1, to adapt it to Symfony 2.0 check the end of the answer) :
For more information on creating custom validation constraints, check The Cookbook
The constraint :
#src/Acme/DemoBundle/Validator/constraint/UniqueInCollection.php
<?php
namespace Acme\DemoBundle\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class UniqueInCollection extends Constraint
{
public $message = 'The error message (with %parameters%)';
// The property path used to check wether objects are equal
// If none is specified, it will check that objects are equal
public $propertyPath = null;
}
And the validator :
#src/Acme/DemoBundle/Validator/constraint/UniqueInCollectionValidator.php
<?php
namespace Acme\DemoBundle\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Form\Util\PropertyPath;
class UniqueInCollectionValidator extends ConstraintValidator
{
// We keep an array with the previously checked values of the collection
private $collectionValues = array();
// validate is new in Symfony 2.1, in Symfony 2.0 use "isValid" (see below)
public function validate($value, Constraint $constraint)
{
// Apply the property path if specified
if($constraint->propertyPath){
$propertyPath = new PropertyPath($constraint->propertyPath);
$value = $propertyPath->getValue($value);
}
// Check that the value is not in the array
if(in_array($value, $this->collectionValues))
$this->context->addViolation($constraint->message, array());
// Add the value in the array for next items validation
$this->collectionValues[] = $value;
}
}
In your case, you would use it like this :
use Acme\DemoBundle\Validator\Constraints as AcmeAssert;
// ...
/**
* #ORM\OneToMany(targetEntity="Discount", mappedBy="client", cascade={"persist"}, orphanRemoval="true")
* #Assert\All(constraints={
* #AcmeAssert\UniqueInCollection(propertyPath ="product")
* })
*/
For Symfony 2.0, change the validate function by :
public function isValid($value, Constraint $constraint)
{
$valid = true;
if($constraint->propertyPath){
$propertyPath = new PropertyPath($constraint->propertyPath);
$value = $propertyPath->getValue($value);
}
if(in_array($value, $this->collectionValues)){
$valid = false;
$this->setMessage($constraint->message, array('%string%' => $value));
}
$this->collectionValues[] = $value;
return $valid
}
Here is a version working with multiple fields just like UniqueEntity does. Validation fails if multiple objects have same values.
Usage:
/**
* ....
* #App\UniqueInCollection(fields={"name", "email"})
*/
private $contacts;
//Validation fails if multiple contacts have same name AND email
The constraint class ...
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class UniqueInCollection extends Constraint
{
public $message = 'Entry is duplicated.';
public $fields;
public function validatedBy()
{
return UniqueInCollectionValidator::class;
}
}
The validator itself ....
<?php
namespace App\Validator\Constraints;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class UniqueInCollectionValidator extends ConstraintValidator
{
/**
* #var \Symfony\Component\PropertyAccess\PropertyAccessor
*/
private $propertyAccessor;
public function __construct()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
/**
* #param mixed $collection
* #param Constraint $constraint
* #throws \Exception
*/
public function validate($collection, Constraint $constraint)
{
if (!$constraint instanceof UniqueInCollection) {
throw new UnexpectedTypeException($constraint, UniqueInCollection::class);
}
if (null === $collection) {
return;
}
if (!\is_array($collection) && !$collection instanceof \IteratorAggregate) {
throw new UnexpectedValueException($collection, 'array|IteratorAggregate');
}
if ($constraint->fields === null) {
throw new \Exception('Option propertyPath can not be null');
}
if(is_array($constraint->fields)) $fields = $constraint->fields;
else $fields = [$constraint->fields];
$propertyValues = [];
foreach ($collection as $key => $element) {
$propertyValue = [];
foreach ($fields as $field) {
$propertyValue[] = $this->propertyAccessor->getValue($element, $field);
}
if (in_array($propertyValue, $propertyValues, true)) {
$this->context->buildViolation($constraint->message)
->atPath(sprintf('[%s]', $key))
->addViolation();
}
$propertyValues[] = $propertyValue;
}
}
}
For Symfony 4.3(only tested version) you can use my custom validator.
Prefered way of usage is as annotaion on validated collection:
use App\Validator\Constraints as App;
...
/**
* #ORM\OneToMany
*
* #App\UniqueProperty(
* propertyPath="entityProperty"
* )
*/
private $entities;
Difference between Julien and my solution is, that my Constraint is defined on validated Collection instead on element of Collection itself.
Constraint:
#src/Validator/Constraints/UniqueProperty.php
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class UniqueProperty extends Constraint
{
public $message = 'This collection should contain only elements with uniqe value.';
public $propertyPath;
public function validatedBy()
{
return UniquePropertyValidator::class;
}
}
Validator:
#src/Validator/Constraints/UniquePropertyValidator.php
<?php
namespace App\Validator\Constraints;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class UniquePropertyValidator extends ConstraintValidator
{
/**
* #var \Symfony\Component\PropertyAccess\PropertyAccessor
*/
private $propertyAccessor;
public function __construct()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
/**
* #param mixed $value
* #param Constraint $constraint
* #throws \Exception
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof UniqueProperty) {
throw new UnexpectedTypeException($constraint, UniqueProperty::class);
}
if (null === $value) {
return;
}
if (!\is_array($value) && !$value instanceof \IteratorAggregate) {
throw new UnexpectedValueException($value, 'array|IteratorAggregate');
}
if ($constraint->propertyPath === null) {
throw new \Exception('Option propertyPath can not be null');
}
$propertyValues = [];
foreach ($value as $key => $element) {
$propertyValue = $this->propertyAccessor->getValue($element, $constraint->propertyPath);
if (in_array($propertyValue, $propertyValues, true)) {
$this->context->buildViolation($constraint->message)
->atPath(sprintf('[%s]', $key))
->addViolation();
}
$propertyValues[] = $propertyValue;
}
}
}
I can't manage to make the previous answer works on symfony 2.6. Because of the following code on l. 852 of RecursiveContextualValidator, it only goes once on the validate method when 2 items are equals.
if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
continue;
}
So, here is what I've done to deals with the original issue :
On the Entity :
* #AcmeAssert\UniqueInCollection(propertyPath ="product")
Instead of
* #Assert\All(constraints={
* #AcmeAssert\UniqueInCollection(propertyPath ="product")
* })
On the validator :
public function validate($collection, Constraint $constraint){
$propertyAccessor = PropertyAccess::getPropertyAccessor();
$previousValues = array();
foreach($collection as $collectionItem){
$value = $propertyAccessor->getValue($collectionItem, $constraint->propertyPath);
$previousSimilarValuesNumber = count(array_keys($previousValues,$value));
if($previousSimilarValuesNumber == 1){
$this->context->addViolation($constraint->message, array('%email%' => $value));
}
$previousValues[] = $value;
}
}
Instead of :
public function isValid($value, Constraint $constraint)
{
$valid = true;
if($constraint->propertyPath){
$propertyAccessor = PropertyAccess::getPropertyAccessor();
$value = $propertyAccessor->getValue($value, $constraint->propertyPath);
}
if(in_array($value, $this->collectionValues)){
$valid = false;
$this->setMessage($constraint->message, array('%string%' => $value));
}
$this->collectionValues[] = $value;
return $valid
}
Can be used Unique built-in validator for Symfony >= 6.1
The fields option was introduced in Symfony 6.1.

ODM: References not being created on both documents

Say I have two simple Documents like this, where a person can have many papers, but a paper can only belong to one person.
namespace Dashboard\Document;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/** #ODM\Document(db="testing", collection="person")
* #ODM\InheritanceType("COLLECTION_PER_CLASS")
*/
class Person
{
/**
* #ODM\Id
*/
protected $id;
/** #ODM\Field(type="string") */
protected $slug;
/** #ODM\Field(type="string") */
protected $name;
/** #ODM\ReferenceMany(targetDocument="Paper", cascade={"all"}) */
protected $papers;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
public function toArray() {
return get_object_vars($this);
}
}
namespace Dashboard\Document;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/** #ODM\Document(db="testing", collection="paper")
* #ODM\InheritanceType("COLLECTION_PER_CLASS")
*/
class Paper
{
/**
* #ODM\Id
*/
protected $id;
/** #ODM\Field(type="string") */
protected $name;
/** #ODM\ReferenceOne(targetDocument="Person", cascade={"all"}) */
protected $person;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
public function toArray() {
return get_object_vars($this);
}
}
I thought I read somewhere when you create a reference on one end, Doctrine ODM will auto create the references on both sides for you. So if I execute the statement below, I will see a reference to Person from a Paper document, AS WELL AS references to Paper(s) in a Person document.
//For demo sake; $person already contains a Person document
try {
$paper = $dm->getRepository('\Dashboard\Document\Paper')
->find($paperId);
} catch (\Doctrine\ODM\MongoDB\MongoDBException $e) {
$this->setStatusFailure($e->getMessage());
$this->sendResponse();
}
$paper->person = $person;
$dm->persist($paper);
$dm->flush();
When I do that, and check the mongodb, the reference from paper-->person is there. But I see no reference person-->paper shown in the db. I thought the cascade annotations helped with this, but obviously I'm missing something.
How can I ensure the reference is contained on both ends, so I can run queries to see all the papers that belong a single person? Does this have to be done manually, or can I have doctrine handle this for me?
UPDATE
The first paragraph on this page made me think it was possible.
http://docs.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/reference/bidirectional-references.html
Turns out I should have read that whole page. If I use mappedBy & inversedBy, and always persist the document that has inversedBy in it's Annotation, then I get that bi-directional relationship
/** #ODM\ReferenceOne(targetDocument="Person", cascade={"all"}, inversedBy="papers") */
protected $person;
//Will give me a relationship I can query on both sides
$person->papers->add($paper);

ODM: Cannot achieve bi-directional relationship

I have two documents. I am trying to find all papers that are associated to a specific person. The documents are saving in their collections, and a reference is being created from Person to Paper, but not the other way around.
/** #ODM\Document */
class Paper
{
/**
* #ODM\Id
*/
protected $id;
/** #ODM\ReferenceOne(targetDocument="Person", cascade={"all"}, mappedBy="papers") */
protected $person;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
public function toArray() {
return get_object_vars($this);
}
}
/** #ODM\Document */
class Person
{
/**
* #ODM\Id
*/
protected $id;
/** #ODM\ReferenceMany(targetDocument="Paper", cascade={"all"}, inversedBy="person") */
protected $papers;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
public function toArray() {
return get_object_vars($this);
}
}
CREATE A NEW BI-DIRECTIONAL REFERENCE
$person = $dm->getRespository('Person')->find($person_id);
$paper = new Paper();
$person->papers->add($paper);
$dm->persist($person);
$dm->flush();
Later in the code, this query returns 0 results; shouldn't it be returning papers written by specified person?
$papers = $dm->createQueryBuilder('Paper')
->field('person.$id')->equals(new \MongoId($person_id_as_string))
->getQuery()->execute();
If Paper::person is annotated with "mappedBy" it means that Paper is not the "owning side" and doctrine will not persist any changes to Paper::person.
To make your query work, make Paper the owning side so Paper stores the reference to Person.
/** #ODM\Document */
class Person
{
/** #ODM\ReferenceMany(targetDocument="Paper", mappedBy="person") */
protected $papers;
}
/** #ODM\Document */
class Paper
{
/** #ODM\ReferenceOne(targetDocument="Person", inversedBy="papers") */
protected $person;
}
Creating a paper and persisting a reference to person:
$person = $dm->getRespository('Person')->find($person_id);
$paper = new Paper();
$paper->person = $person;
$dm->persist($paper);
$dm->flush();
Querying Papers by $person:
$papers = $dm->createQueryBuilder('Paper')
->field('person')->references($person)
->getQuery()->execute();

removeElement() and clear() doesn't work in doctrine 2 with array collection property

I'm trying to get some simple CRUD done with doctrine 2 but when it's time to update a record with one property set as an array collection I don't seem to get removeElement() to work as it's supposed to. I even tried doing it in this ridiculously ugly way:
foreach($entity->getCountries() as $c) {
$entity->getCountries()->removeElement($c);
$this->em->persist($entity);
$this->em->flush();
}
and it didn't work... Anyone knows how to handle this? I've asked for a solution to this in many different forms and haven't got a good response so far... seems there's lack of good examples of Doctrine 2 CRUD handling. I'll post more code at request.
Edit
//in user entity
/**
*
* #param \Doctring\Common\Collections\Collection $property
* #OneToMany(targetEntity="Countries",mappedBy="user", cascade={"persist", "remove"})
*/
private $countries;
//in countries entity
/**
*
* #var User
* #ManyToOne(targetEntity="User", inversedBy="id")
* #JoinColumns({
* #JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $user;
I do something similar in a project with Events which have participants not unlike your User/Country relationship. I will just lay out the process and you can see if there's anything you are doing differently.
On the Participant entity
/**
* #ManyToOne(targetEntity="Event", inversedBy="participants", fetch="LAZY")
* #JoinColumn(name="event_id", referencedColumnName="id", nullable="TRUE")
* #var Event
*/
protected $event;
On the Event entity:
/**
* #OneToMany(targetEntity="Participant", mappedBy="event")
* #var \Doctrine\Common\Collections\ArrayCollection
*/
protected $participants;
Also in Event#__constructor I initialize like this:
$this->participants = new \Doctrine\Common\Collections\ArrayCollection();
Here is how I update an event:
public function update(Event $event, Event $changes)
{
// Remove participants
$removed = array();
foreach($event->participants as $participant)
{
if(!$changes->isAttending($participant->person))
{
$removed[] = $participant;
}
}
foreach($removed as $participant)
{
$event->removeParticipant($participant);
$this->em->remove($participant);
}
// Add new participants
foreach($changes->participants as $participant)
{
if(!$event->isAttending($participant->person))
{
$event->addParticipant($participant);
$this->em->perist($participant);
}
}
$event->copyFrom($changes);
$event->setUpdated();
$this->em->flush();
}
The methods on the Event entity are:
public function removeParticipant(Participant $participant)
{
$this->participants->removeElement($participant);
$participant->unsetEvent();
}
public function addParticipant(Participant $participant)
{
$participant->setEvent($this);
$this->participants[] = $participant;
}
The methods on the Participant entity are:
public function setEvent(Event $event)
{
$this->event = $event;
}
public function unsetEvent()
{
$this->event = null;
}
UPDATE: isAttending method
/**
* Checks if the given person is a
* participant of the event
*
* #param Person $person
* #return boolean
*/
public function isAttending(Person $person)
{
foreach($this->participants as $participant)
{
if($participant->person->id == $person->id)
return true;
}
return false;
}
New answer
In your countries entity, should you not have:
#ManyToOne(targetEntity="User", inversedBy="countries")
instead of inversedBy="id"?
Initial answer
You need to set the countries field in your entity as remove cascade. For example, on a bidirectional one to many relationship:
class Entity
{
/**
*
* #OneToMany(targetEntity="Country", mappedBy="entity", cascade={"remove"})
*/
private $countries;
}
This way, when saving your entity, doctrine will also save changes in collections attached to your entity (such as countries). Otherwise you have to explicitly remove the countries you want to remove before flushing, e.g.
$this->em()->remove($aCountry);
This is also valid for persist, merge and detach operations. More information here.

Resources