bentools / doctrine-safe-events
Fires postPersist / postUpdate / postRemove events AFTER the transaction has completed.
Requires
- php: >=8.2
- doctrine/orm: ~2.20|~3.6
Requires (Dev)
- pestphp/pest: ^2.36|^3.8|^4.4
- phpstan/phpstan: ^1.8
- squizlabs/php_codesniffer: ^3.7
- symfony/cache: ~5.4|~6.2
- symfony/var-dumper: @stable
README
Doctrine safe post* events
Doctrine's postPersist, postUpdate and postRemove events are fired when the corresponding SQL queries (INSERT / UPDATE / DELETE) have been performed against the database server.
What happens under the hood is that Doctrine creates a wrapping transaction, runs SQL queries, then commits the transaction.
However, these events are fired immediately, e.g. not once the transaction is complete, which means:
- If the wrapping transaction fails, events have already been fired anyway (meaning you trusted generated primary key values, although they're going to be rolled back)
- If the wrapping transaction takes some time (typically during row locks), you get the inserted / updated / deleted information before it's actually done (meaning if you run some async process once those events are triggered, you end up in processing not data which is not up-to-date)
Doctrine ORM documentation states:
Warning
At the time postPersist is called, there may still be collection and/or "extra" updates pending. The database may not yet be completely in sync with the entity states in memory, not even for the new entities.
Similarly, also at the time postUpdate and postRemove are called, in-memory collections may still be in a "dirty" state or still contain removed entities.
Background
The idea of this repository indeed came up with the following issue:
- An entity is persisted, then
$em->flush()is called - A
postPersistevent listener gets the entity's id, then asks a worker to do some async processing through Symfony Messenger - The worker queries database against the entity's id, and gets an
EntityNotFoundexception (theCOMMITdid not happen yet) - The flush operation on the main thread completes, and the
postFlushevent is fired (but it does not contain the inserted / updated / deleted entities)
Our solution
If you run into the same kind of issues, you can replace your listeners' listened events in favor of:
Bentools\DoctrineSafeEvents\SafeEvents::POST_PERSIST(and implementsafePostPersistas a replacement ofpostPersist)Bentools\DoctrineSafeEvents\SafeEvents::POST_UPDATE(and implementsafePostUpdateas a replacement ofpostUpdate)Bentools\DoctrineSafeEvents\SafeEvents::POST_REMOVE(and implementsafePostRemoveas a replacement ofpostRemove)
Basically, this library will collect entities which are scheduled for insertion / update / deletion, except it will delay event firing until the postFlush occurs.
Installation
composer require bentools/doctrine-safe-events
Usage in Symfony
Although this library has no dependency on Symfony, you can easily use it in your Symfony project:
Bentools\DoctrineSafeEvents\SafeEventsDispatcher: tags: - { name: 'doctrine.event_subscriber' } - { name: 'kernel.reset', method: 'reset' }
Example usage
declare(strict_types=1); namespace App; use Bentools\DoctrineSafeEvents\SafeEvents; use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\Persistence\Event\LifecycleEventArgs; #[AsDoctrineListener(SafeEvents::POST_PERSIST)] final class SomeListener { public function safePostPersist(LifecycleEventArgs $eventArgs): void { // ... } }
Tests
composer test
License
MIT.