Doctrine updating many to many association after deserialization - php

I am making something like a wizard and I need to pass an object from one request to another. I use serialization for this. The object is a doctrine entity with a many to many association. For demonstration purposes I will simplify, since this issue regards only the association.
class User
{
// scalar properties
/**
* #var User\Role
* #ORM\ManyToMany(targetEntity="User\Role", cascade={"persist", "remove"})
* #ORM\JoinTable(name="roles")
*/
protected $roles;
// getters, setters
}
Now, when I deserialize the object, it deserializes perfectly, with the associations. Problem is, when I merge it with $em->merge($object); and then flush, the object gets saved into database and all the scalar properties that changed are persisted correctly. But the association is ignored during save. Before saving, there are three roles in the database. I have only one role in my association, but when I flush and then reload the object from database, there are still the three roles that were there before. This issue occurs only with deserialization, if I work with an entity that is originaly loaded from $em, the association gets updated like it should.
One more thing - if I define cascade={"merge"} on the association, the merge operation ends with error "spl_object_hash() expects parameter 1 to be object, array given" in UnitOfWork on line 1810, where an array of roles (in this case with one element) is passed into spl_object_hash() function. Not sure if this is a bug or I'm doing something wrong.
Does anyone have and idea how to get around this issue, or what am I doing wrong? Any help appreciated!

I had the EXACT same issue today, that's why I landed here.
It seems I have solved it:
cascade={"merge"}
was definitely useful and once I configured the ManyToMany relation correctly on both sides, it started working - no "spl_objects_hash()"-issue any more. Pay Attention to inversedBy and Mapped by. Also, but I don't know if this is part of the trick, take a look at the add and remove methods (which I implemented on both sides.) Before, I didn't take care of adding/removing the other side of the relation...
class User {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Project", cascade={"merge"}, inversedBy="users")
* #JMS\Type("Relation<App\Entity\Project>")
*/
private $projects;
public function addProject(Project $project): self
{
if (!$this->projects->contains($project)) {
$this->projects[] = $project;
$project->addUser($this);
}
return $this;
}
public function removeProject(Project $project): self
{
if ($this->projects->contains($project)) {
$this->projects->removeElement($project);
$project->removeUser($this);
}
return $this;
}
}
class Project {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\User", mappedBy="projects")
* #JMS\Type("Relation<App\Entity\Project>")
*/
private $users;
}
EDIT: SO after having the same issue with another entity, I tested if the removing and adding of the related entity in add()/remove() was necessary in this case: NOPE. cascade={merge} does the trick, which started working after configuring the inverse side correctly.

Related

Issue with Doctrine OneToMany relationship

I have an issue with the doctrine relationship. I try different ways but anything won't work.
Idea is that I have a News entity and every news should have many comments. So I try next:
The News entity:
/**
* #ORM\OneToMany(targetEntity="App\ORM\Entity\NewsComment", mappedBy="news")
*/
protected \Doctrine\Common\Collections\Collection $comments;
/**
* News constructor.
*/
public function __construct() {
$this->comments = new ArrayCollection();
}
And NewsComment entity:
/**
* #ORM\ManyToOne(targetEntity="App\ORM\Entity\News", inversedBy="comments")
*/
protected \App\ORM\Entity\News $news;
Every entity has its own get and set methods as well.
But, when I receive a News entity a can get comments collection but it always empty. On the other hand, I can take any NewsComment entity and get from this News entity. It is working fine. But not to another way.
Is anything wrong with my code?
Doctrine sets owned (non-inversed) collection as lazy by default.
When retrieving an entity by database, you should see an empty PersistentCollection instead of ArrayCollection, with initialized property set to false.
When calling any method on that collection, doctrine fires the queries needed to initialize the collection and populate it.
Collection emptiness should be only checked invoking isEmpty.

Why injecting EntityManagerInterface return me a Cannot serialize PhpFilesAdapter error

I have a problem that I've never encountered before even if I've done this on an other project. I tried to inject an EntityManagerInterface into my User entity (in order to fetch something in a configuration database). So I've used the #PostLoad technic.
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #ORM\HasLifecycleCallbacks()
*/
class User implements UserInterface
{
/** #var EntityManagerInterface $em */
private $em;
/**
* #ORM\PostLoad
* #ORM\PostPersist
*/
public function fetchEntityManager(LifecycleEventArgs $args)
{
$this->setEntityManager($args->getEntityManager());
}
public function setEntityManager($em) {
$this->em=$em;
}
}
I've already done this in an other project but not in the user entity and it worked just fine. But now I get this error:
Cannot serialize Symfony\Component\Cache\Adapter\PhpFilesAdapter
I think that creating a sort of utility class that could prevent me from injecting the entityManagerInterface in my entity will help me but I don't know what type of class should I use, a Service maybe ?
not entirely unrelated to your question: injecting an entity manager into an entity indicates a bad design and is not intended in symfony/doctrine's orm.
what is causing your problem is, that the user is serialized by symfony's session management and when it tries to serialize a sufficiently complex object/service, it'll probably fail to do so. It is recommended to implement the Serializable interface to only serialize stuff that is actually needed (see same link). Which will solve this and other possible problems you might get. It also reduces the size of the session, which might get larger the bigger your application grows and attaches to the user object. Since the user object is loaded on every request anyway, serializing anything more than the mentioned properties (same link again) is simply wasteful.
I use Symfony 5.3.9 and for me worked, after thousand of hours of try, to call "composer update"

symfony #oneToMany relation returns only last object in collection

I have a situation and not really sure what I'm doing wrong. in Sumfony 3.3 I've created a relation between entity Page and Language, where Page is related to multiple Languages, and when I search for a Page and get Page object but property Languages returns collection with only last Language object. No matter how many objects are there in collection it always returns last.
Page Entity:
/**
* #ORM\OneToMany(targetEntity="Language", mappedBy="page", cascade={"ALL"}, indexBy="page_id")
*/
private $languages;
Language entity:
/**
* #ORM\ManyToOne(targetEntity="Page", inversedBy="languages")
*/
private $page;
public function addLanguage(\AppBundle\Entity\Langaugee $language)
{
$this->languages[] = $language;
return $this;
}
public function removeLanguage(\AppBundle\Entity\Language $language)
{
$this->$languages->removeElement($language);
}
public function getLanguages()
{
return $this->languages;
}
Page object is fetching in PageService:
public function getPageByName($name)
{
return $this->pageRepository->findBy(array("name"=>$name));
}
Since property $languages by default set on lazy, JMS serializer when serializes Page object it's fetching languages collection
Did anyone had this problem?
After thorough debugging, I figure it out that indexBy is misused here. Defined indexBy = page_id provided always the same value so every record that is mapped to entity in SimpleObjectHydrator overrun the existing record, leaving only last added Language object in collection
I know Its an old post, but solution below worked for me.
I had to clear EntityManager cache in my repository class method by calling this method just before fetching data.
$this->_em->clear();
here _em is the default EntityManager available in the repository class.

Symfony 3 / Doctrine - Get changes to associations in entity change set

So I already know that I can get changes to a specific entity in the preUpdate lifecycle event:
/**
* Captures pre-update events.
* #param PreUpdateEventArgs $args
*/
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof ParentEntity) {
$changes = $args->getEntityChangeSet();
}
}
However, is there a way to also get changes for any associated Entities? For example, say ParentEntity has a relationship setup like so:
/**
* #ORM\OneToMany(targetEntity="ChildEntity", mappedBy="parentEntity", cascade={"persist", "remove"})
*/
private $childEntities;
And ChildEntity also has:
/**
* #ORM\OneToMany(targetEntity="GrandChildEntity", mappedBy="childEntity", cascade={"persist", "remove"})
*/
private $grandChildEntities;
Is there a way to get all relevant changes during the preUpdate of ParentEntity?
All of the associated entities from a OneToMany or ManyToMany relationships appear as a Doctrine\ORM\PersistentCollection.
Take a look at the PersistentCollection's API, it have some interesting public methods even if they are marked as INTERNAL: https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/PersistentCollection.php#L308
For example you can check if your collection is dirty which means that its state needs to be synchronized with the database. Then you can retrieve the entities that have been removed from the collection or inserted into it.
if ($entity->getChildEntities()->isDirty()) {
$removed = $entity->getChildEntities()->getDeleteDiff();
$inserted = $entity->getChildEntities()->getInsertDiff();
}
Also you can get a snapshot of the collection at the moment it was fetched from the database: $entity->getChildEntities()->getSnapshot();, this is used to create the diffs above.
May be this is not optimal, but it can do the job. You can add a version field on ParentEntiy with a timestamp, then on each related entity setter function (Child or GranChild) you need to add a line updating that parent timestamp entity. In this way each time you call a setter you will produce a change on the parent entity that you can capture at the listener.
I have used this solution to update ElasticSearch documents that need to be updated when a change happens on a child entity and it works fine.

Doctrine2 - #OneToOne unidirectional entity returns cascade persistence error

I have the following class:
/**
* #ORM\Entity(repositoryClass="Repository")
* #ORM\Table(name="my_data")
* #ORM\HasLifecycleCallbacks
*/
class Data extends ModelEntity
{
/**
* #var integer $id
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var Customer $userId
*
* #ORM\OneToOne(targetEntity="\Shopware\Models\Customer\Customer")
* #ORM\JoinColumn(name="userId", referencedColumnName="id")
*/
private $userId;
//in the class are more methods + getters and setters #not important
}
The Customer Entity is not a class of mine, therefore I want this relationship to be unidirectional and don't care how the Customer looks like (minus the id field).
Now from Doctrine documentation I got that I don't need any other cascade={"persist"} thingy like in the error I get:
exception 'Doctrine\ORM\ORMInvalidArgumentException' with message
'A new entity was found through the relationship
'Data#userId' that was not configured to cascade persist operations
for entity:
Shopware\Models\Customer\Customer#0000000035d66eab000000001b1ed901.
To solve this issue: Either explicitly call EntityManager#persist()
on this unknown entity or configure cascade persist
this association in the mapping for example
#ManyToOne(..,cascade={"persist"}).
If you cannot find out which entity causes the problem implement
'Shopware\Models\Customer\Customer#__toString()' to get a clue.'
what am I doing wrong? And how can I fix this error?
PS: I don't want to modify or save any information in this Customer, just to have a reference to the entity.
LE: Due this info Doctrine documentation: cascade-persist I will output also some code from where the error is triggered. As it says in the documentation:
New entities in a collection not marked as cascade: persist will produce an Exception and rollback the flush() operation.
the error is trown by the following flush()
$this->entityModel->persist($this->getDataInstance());
$this->entityModel->flush();
and the getDataInstance method is Data entity that has a Customer, the Customer Object is found from its repository through method findBy('Customer', id). The Customer entity it is never new instatiated or modified, therefore no need to persist it.
The getDataInstance:
protected function getDataInstance()
{
if (is_null($this->data)) {
$this->initData();
}
return $this->data;
}
and the initData:
private function initData()
{
if (is_null($this->data)) {
$this->data = new Data();
/**
* #var \Shopware\Models\Customer\Customer $customer
*/
$customer = $this->entityModel->getRepository('Shopware\Models\Customer\Customer')->findOneBy(['id' => $this->session->sUserId]);
$this->data->setUserId($customer);
}
}
UPDATE: Still didn't find an answer, but I found some leads after speaking with someone on the doctrine #IRC channel.
Here is the conversation:
p1: did your Customer entity maybe get detached? for instance de/serialization, using more than one entity manager or calling $em->clear() can lead to detaching
p1: do you expect to create new customer? if no then either you did, or it "found" an already existing entity which would suggest one of the above
me: $em->clear() is not called by me for sure
me: the de/serialization I'm not sure ... the shop has a HTTP-cache that can do that
p1: where do you get the entity from? yes, cache might do it if it caches customers without knowing about orm
me: and I don't expect to create a new customer, I just called from the repository one
me: $customer = $this->swagModelManager->getRepository('Shopware\Models\Customer\Customer')->findOneBy(['id' => $this->session->sUserId]);
me: which retruns either null or the object
p1: is $this, respectively $this->data cached between requests possibly?
me: the method findOneBy can be find in the EntityRepository.php (line 192)
me: hmm I don't know, the thing is on my local host and other installation that never happens, but on 1 client server it does :D and I assumed that has to do with the caches and his settings on the server
me: so if that's the problem how can I fix it ?
me: to don't call the cascade persist
p1: the detached entity can be reattached by doing "$entity = $em->refresh($entity)" or "$entity = $em->merge($entity)" (they differ in handling of possible changes between the entity state and the db - refresh reloads the data from db - "resets", merge updates the db) - but that is not a fix if you do not know why it happens - you probably can't tell when it is safe to do it
me: I know, it is bad that I cannot reproduce that - thanks for the tips!
p1: the main thing - if EM says it found "new" entity and it already exists in the DB, it is not "managed" by that EM - either it has been detached somehow or it is still attached but to a different EM instance
me: then an $em->refresh($entity) before the persist should sufice, right? because I don't want to change anything related to this Entity
p1: you would have to do that to the referenced customer, so "$entity->customer = $em->refresh($entity->customer)" - but beware of possible side effects of reassigning that (any events, side effects in getter or similar stuff if you have it)
Due to the fact that I cannot reporduce the error right now, I cannot say if the end of the conversation is the right answer or not. When I will know for sure I will revise the question/answer.

Categories