I have a bit of problems with PreUpdate HasLifecycleCallbacks.
I have an entity, let say "A" with have a OneToOne relation with the entity "B".
So I have:
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks
*/
class A
{
/**
* #ORM\OneToOne(targetEntity="B", inversedBy="attrA", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="fieldB", referencedColumnName="id")
*/
private $attrB;
public function __construct()
{
$this->attrB = new B();
}
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updateSomthing(){
//$gestor = fopen("/pruebitas.txt", "r");
$this->attrB->setDate($this->getDate());
}
}
And class B is:
class B
{
/**
* #ORM\OneToOne(targetEntity="A", mappedBy="attrB")
*/
private $attrA;
}
When I create a new entity A, everything works fine, the problem is when I update the entity A, the PreUpdate function is fire, (because it creates the file in the commented line), but the entity B does not persist in the database, even if the field in B should be updated.
Any idea to cascade the persist on the PreUpdate??
Thanks!!
Use preFlush instead
From the Doctrine documentation of the preUpdate event:
Changes to associations of the updated entity are never allowed in
this event, since Doctrine cannot guarantee to correctly handle
referential integrity at this point of the flush operation.
That makes sense, so you need to do your changes to associated entities before all the changesets are genereated by the Unit of Work. That's what the preFlush event is for.
preFlush is called at EntityManager#flush() before anything else.
EntityManager#flush() can be called safely inside its listeners.
Simply replace your #ORM\PreUpdate annotation with #ORM\PreFlush and it should work.
The preFlush event is available since Doctrine 2.2.
Doctrine Documentation: "Events - preFlush"
Doctrine bug tracker: "preFlush event and lifecycle callback"
You need to manually call ->recomputeSingleEntityChangeSet() on the unit of work if you make a change to an entity in a preUpdate listener.
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updateSomething($eventArgs)
{
$this->attrB->setDate($this->getDate());
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$meta = $em->getClassMetadata(get_class($entity));
$uow->recomputeSingleEntityChangeSet($meta, $entity);
}
Related
The documentation says: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/events.html#postupdate-postremove-postpersist
The three post events are called inside EntityManager#flush(). Changes in here are not relevant to the persistence in the database, but you can use these events to alter non-persistable items, like non-mapped fields, logging or even associated classes that are not directly mapped by Doctrine.
So let's imagine I have an Image entity:
<?php
/**
* #ORM\Entity
*/
class Image
{
/**
* #var int|null
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* #var string
* #ORM\Column(type="string")
*/
private string $path;
/**
* #var string
* #ORM\Column(type="string")
*/
private string $status = 'RECEIVED';
}
Once I flush my Image entity I want to upload the corresponding file on a FTP server, so I do it in the postPersist event.
But if the upload on FTP fail I want to change the status of my Image to FTP_ERROR.
<?php
public function postPersist(Image $image, LifecycleEventArgs $event)
{
$em = $event->getEntityManager();
try {
$this->someService->uploadToFtp($image);
} catch (Exception $e) {
$image->setStatus('FTP_ERROR');
$em->persist($image);
$em->flush();
}
}
And it works, but as documentation says it's not a good way to do it.
I have seen this post: Flushing in postPersist possible or not? which says:
#iiirxs:
It is ok to call flush() on a PostPersist lifecycle callback in order to change a mapped property of your entity.
So how to do so? #iiirxs and the documentation say 2 different things.
The best way to download something on such event as postPersist is using https://symfony.com/doc/current/components/messenger.html
so, you can create asynchronous task for file uploading and Symfony Messenger will be able to handle errors and retry it automatically on fail, also you will be able to update it, status and etc in separate process and it will not depend on specific doctrine cases like case in your question.
I'm trying to record every change in quantity of a given item. For that purpose, I listen for a change of an Item entity and wish to create a new Transaction instance with details about the action. So I'm creating an entity inside a listener.
I've set up everything according to the documentation and created the listener based on this example.
The code (I believe) is relevant for my problem is following.
ItemListener
// ...
private $log;
/** #ORM\PreUpdate */
public function preUpdateHandler (Item $item, PreUpdateEventArgs $args)
{
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($item)['quantity'];
$quantityChange = $changeSet[1] - $changeSet[0];
$transaction = new Transaction();
$transaction->setItem($item);
$transaction->setQuantityChange($quantityChange);
$this->log = $transaction;
}
/** #ORM\PostUpdate */
public function postUpdateHandler(Item $item, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$em->persist($this->log);
$em->flush();
}
This works perfectly. However, the problem is when I add another field to the transaction entity. The user field inside Transaction entity has ManyToOne relation. Now when I try to set the user inside the preUpdateHandler, it leads to and undefined index error inside the UnitOfWork function of the Entity Manager.
Notice: Undefined index: 000000003495bf92000000001108e474
The listener is now like this. I retreive the user based on the token that was sent with the request. Therefore, I inject the request stack and my custom user provider in the listener's constructor. I do not think this is the source of the problem. However, if necessary, I'll edit the post and add all the remaining code (rest of the listener, services.yaml and user provider).
ItemListener
// ...
private $log;
/** #ORM\PreUpdate */
public function preUpdateHandler (Item $item, PreUpdateEventArgs $args)
{
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($item)['quantity'];
$quantityChange = $changeSet[1] - $changeSet[0];
$transaction = new Transaction();
$transaction->setItem($item);
$transaction->setQuantityChange($quantityChange);
$request = $this->requestStack->getCurrentRequest();
$company = $this->userProvider->getUserByRequest($request);
$this->log = $transaction;
}
/** #ORM\PostUpdate */
public function postUpdateHandler(Item $item, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$em->persist($this->log);
$em->flush();
}
I do not understand why retreiving the flush with retrieval of another entity leads to that error. When searching for an answer I found that that many recommend not to use flush() inside the postUpdate cycle but rather in postFlush. However, this method is not defined for Entity listeners according to the documentation and if possible, I'd like to stick to such a listener and not an event listener.
Thank you for any help. I also include the transaction entity code just in case.
Transaction Entity
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\DoctrineUtils\MagicAccessors;
use App\Entity\T\TIdentifier;
/**
* #ORM\Entity
* #ORM\Table(name="transaction")
*/
class Transaction
{
use TIdentifier;
use MagicAccessors;
/**
* #ORM\ManyToOne(targetEntity="Item")
* #ORM\JoinColumn(name="item_id", referencedColumnName="id", nullable=false)
*/
public $item;
/**
* #ORM\Column(type="decimal", length=14, precision=4, nullable=false)
*/
public $quantityChange;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $createdTime;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
public function __construct()
{
$this->createdTime = new \DateTime();
}
/**
* #param mixed $quantityChange
*/
public function setQuantityChange(int $quantityChange): void
{
$this->quantityChange = $quantityChange;
}
/**
* #param mixed $createdTime
*/
public function setCreatedTime($createdTime): void
{
$this->createdTime = $createdTime;
}
/** #ORM\PrePersist **/
public function onCreate() : void
{
$this->setCreatedTime(new \DateTime('now'));
}
public function setUser(?User $user): self
{
$this->user= $user;
return $this;
}
}
I found out that the problem was that another instance of the entity manager was instantiated in the getUserByRequest() function, where I log that the user's token was used. Apart others, I created inside it a new manager, persisted the entry and flushed the result. However, the new entity manager does not know about the unit of work inside the other entity manager inside the listener. Hence the undefined index error.
I tried to omit the persist and the flush part inside the user getter function, but that was not enough. In the end I solved the problem by passing the given instance entity manager from inside the listener to the getter function. So basically, I ended up calling this from the preUpdateHandler function inside the listener.
$em = $args->getEntityManager();
$company = $this->userProvider->getUserByRequest($request, $em);
Hope this helps if you find yourself in a similar pickle.
I'm tying to create one to many relations
A have class
class Interview {
/**
* #OneToMany(targetEntity="Question", mappedBy="question")
*/
private $questions;
public function __construct() {
$this->questions = new ArrayCollection();
}
public function __toString() {
return $this->id;
}
/**
* #return Collection|Question[]
*/
public function getQuestions() {
return $this->questions;
}
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
......
}
another
class Question {
/**
* #ManyToOne(targetEntity="Interview", inversedBy="interview")
* #JoinColumn(name="interview_id", referencedColumnName="id")
*/
private $interview;
public function getInterview() {
return $this->interview;
}
public function setInterview(Interview $interview) {
$this->interview = $interview;
return $this;
}
/**
* #ORM\Column(type="integer")
* #ORM\Id
*/
private $interview_id;
......
}
and Controller for all this
if ($form->isSubmitted() && $form->isValid()) {
$interview = new Interview();
$question = new Question();
$em->persist($interview);
$question->setInterview($interview);
$question->setTitle($request->get('title'));
$em->persist($question);
$em->flush();
return $this->redirectToRoute('homepage');
}
i'm receiving an error:
Entity of type AppBundle\Entity\Question is missing an assigned ID for
field 'interview_id'. The identifier generation strategy for this
entity requires the ID field to be populated before
EntityManager#persist() is called. If you want automatically generated
identifiers instead you need to adjust the metadata mapping
accordingly.
Don't understand what the problem and how to fix it.
To enforce loading objects from the database again instead of serving them from the identity map. You can call $em->clear(); after you did $em->persist($interview);, i.e.
$interview = new Interview();
$em->persist($interview);
$em->clear();
It seems like your project config have an error in doctrine mapped part.
If you want automatically generated identifiers instead you need to
adjust the metadata mapping accordingly.
Try to see full doctrine config and do some manipulation with
auto_mapping: false
to true as example or something else...
Also go this , maybe it will be useful.
I am sure, its too late to answer but maybe someone else will get this error :-D
You get this error when your linked entity (here, the Interview entity) is null.
Of course, you have already instantiate a new instance of Interview.But, as this entity contains only one field (id), before this entity is persited, its id is equal to NULL. As there is no other field, so doctrine think that this entity is NULL. You can solve it by calling flush() before linking this entity to another entity
In a Symfony2 project I'm using the Loggable Doctrine Extension.
I saw that there is a LoggableListener.
Is there indeed an event that gets fired when a (loggable) field in a loggable entity changes? If it is so, is there a way to get the list of fields that triggered it?
I'm imagining the case of an entity with, let's say 10 fields of which 3 loggable. For each of the 3 I want to perform some actions if they change value, so 3 actions will be performed if the 3 of them change.
Any idea?
Thank you!
EDIT
After reading the comment below and reading the docs on doctrine's events I understood have 3 options:
1) using lifecycle callbacks directly at the entity level even with arguments if I'm using doctrine >2.4
2) I can listen and subscribe to Lifecycle Events, but in this case the docs say that "Lifecycle events are triggered for all entities. It is the responsibility of the listeners and subscribers to check if the entity is of a type it wants to handle."
3) doing what you suggest, which is using an Entity listener, where you can define at the entity level which is the listener that is going to be "attached" to the class.
Even if the first solution seems easier, I read that "You could also use this listener to implement validation of all the fields that have changed. This is more efficient than using a lifecycle callback when there are expensive validations to call". What's considered an "expensive validation?".
In my case what I have to perform is something like "if field X of entity Y changed than add a notification on the notification table saying "user Z changed the value of X(Y) from A to B"
Which would be the most suitable approach, considering that I have around 1000 fields like those?
EDIT2
To solve my problem I'm trying to inject the service_container service inside the listener, so that I can have access to the dispatcher to dispatch a new event which can perform the persist of new entity I need. But how can I do that?
I tried the usual way, I add the following to the service.yml
app_bundle.project_tolereances_listener:
class: AppBundle\EventListener\ProjectTolerancesListener
arguments: [#service_container]
and of course I added the following to the listener:
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
but I get the following:
Catchable Fatal Error: Argument 1 passed to AppBundle\ProjectEntityListener\ProjectTolerancesListener::__construct() must be an instance of AppBundle\ProjectEntityListener\ContainerInterface, none given, called in D:\provarepos\user\vendor\doctrine\orm\lib\Doctrine\ORM\Mapping\DefaultEntityListenerResolver.php on line 73 and defined
Any idea?
The Loggable listener only saves the changesvalue for the watched properties of your entities over time.
It does not fire an event, it listens to the onFlush and postPersist doctrine events.
I think you are looking for Doctrine listeners on preUpdate and prePersist events where you can manipulate the changeset before a flush.
see: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html
If you are using Doctrine 2.4+ you can add them easily to your entity:
Simple entity class:
namespace Your\Namespace\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\EntityListeners({"Your\Namespace\Listener\DogListener"})
*/
class Dog
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="integer")
*/
private $age;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* #return int
*/
public function getAge()
{
return $this->age;
}
/**
* #param int $age
*/
public function setAge($age)
{
$this->age = $age;
}
}
Then in Your\Namespace\Listener you create the ListenerClass DogListener:
namespace Your\Namespace\Listener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Your\Namespace\Entity\Dog;
class DogListener
{
public function preUpdate(Dog $dog, PreUpdateEventArgs $event)
{
if ($event->hasChangedField('name')) {
$updatedName = $event->getNewValue('name'). ' the dog';
$dog->setName($updatedName);
}
if ($event->hasChangedField('age')) {
$updatedAge = $event->getNewValue('age') % 2;
$dog->setAge($updatedAge);
}
}
public function prePersist(Dog $dog, LifecycleEventArgs $event)
{
//
}
}
Clear the cache and the listener should be called when flushing.
Update
You are right about recomputeSingleEntityChangeSet which was not needed in this case. I updated the code of the listener.
The problem with the first choice (in-entity methods) is that you can't inject other services in the method.
If you only need the EntityManager then yes, it is the easiest way code-wise.
With an external Listener class, you can do so.
If those 1000 fields are in several separate entities, the second type of Listener would be the most suited. You could create a NotifyOnXUpdateListener that would contain all your watch/notification logic.
Update 2
To inject services in an EntityListener declare the Listener as a service tagged with doctrine.orm.entity_listener and inject what you need.
<service id="app.entity_listener.your_service" class="Your\Namespace\Listener\SomeEntityListener">
<argument type="service" id="logger" />
<argument type="service" id="event_dispatcher" />
<tag name="doctrine.orm.entity_listener" />
</service>
and the listener will look like:
class SomeEntityListener
{
private $logger;
private $dispatcher;
public function __construct(LoggerInterface $logger, EventDispatcherInterface $dispatcher)
{
$this->logger = $logger;
$this->dispatcher = $dispatcher;
}
public function preUpdate(Block $block, PreUpdateEventArgs $event)
{
//
}
}
According to: How to use Doctrine Entity Listener with Symfony 2.4? it requires DoctrineBundle 1.3+
I have Entity provider (it is just entity repository which searches and gives me a user while authentication) like this entity provider
(MyBundle:Employee implements UserInterface so that`s ok)
class EmployeeRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$user = $this->getEntityManager()
->createQuery("SELECT e FROM MyBundle:Employee e ...")
->setParameters(...)->getOneOrNullResult();
if ($user) {
return $user;
}
throw new UsernameNotFoundException();
}
public function refreshUser(UserInterface $user)
{
...
return $this->find($user->getId());
}
}
and I have another entity like
class Task {
...
/**
* #ManyToOne(targetEntity="Employee")
* #JoinColumn()
*/
protected $creator;
... + setters/getters
}
so somewhere in controller i have:
$task = new Task();
$task->setCreator($this->getUser()) // or $this->get('security.context')->getToken()->getUser();
$em->persist($task);
$em->flush();
and I have exception "A new entity was found through the relationship '...\Entity\Task#creator' that was not configured to cascade persist operations for entity: ...\Entity\Employee#0000000066a194ca0000000038e61044. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist"
but how can it be: unpersisted entity was given by entityRepository ???
(if persist getUser() em tries to insert new Employee) How can I set creator of task?
As I understand, you want to be able to join the Employee entity on Task entity in order to know which user has created the Task.
If so, you should probably take a look at the StofDoctrineExtensionBundle that allows you to easily use DoctrineExtension in Symfony2.
DoctrineExtension provides a blameable behavior:
Blameable behavior will automate the update of username or user reference fields on your Entities or Documents. It works through annotations and can update fields on creation, update or even on specific property value change.
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Article
{
// ...
/**
* #var string $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\Column(type="string")
*/
private $createdBy;
/**
* #var string $updatedBy
*
* #Gedmo\Blameable(on="update")
* #ORM\Column(type="string")
*/
private $updatedBy;
// ...
public function getCreated()
{
return $this->created;
}
public function getUpdated()
{
return $this->updated;
}
}
Hope this helps
I found the solution.
in this code I used new instance of entity manager like $this->get('doctrine.orm.entity_manager')
$task = new Task();
$task->setCreator($this->getUser())
$em->persist($task);
$em->flush();
, so new entity manager knows nothing about entities such as User
and the solution was to use default entity manager
$em = $this->get('doctrine.orm.default_entity_manager');
...
$em->persist(task);
$em->flush();
works fine.
I can't see al the code, but if you want to insert new task and new user in the same time (using collection type in forms) you need to define it in your entity (cascade annotation):
class Task {
...
/**
* #ManyToOne(targetEntity="Employee", cascade={"persist"})
* #JoinColumn()
*/
protected $creator;
... + setters/getters
}
For more info about cascade operations see Doctrine documentation: http://docs.doctrine-project.org/en/2.0.x/reference/working-with-associations.html#transitive-persistence-cascade-operations