I have a row with sku column that must be unique. But when I try to update this row it doesn't happen because of this. How to avoid this with Doctrine ? Or I should write custom logic ?
if (!$this->uniqueSkuSpecification->isSatisfiedBy($request->getSku())) {
return ErrorResponse::fromCustomError(
'Product sku must be unique',
'sku',
Response::HTTP_UNPROCESSABLE_ENTITY
);
}
The way I am populating the data from $request (its DTO)
public function updateFields(UpdateProductCommand $request): Product
{
$reflection = new \ReflectionClass($this);
foreach ($reflection->getProperties() as $property) {
$field = ucfirst($property->getName());
$getter = "get{$field}";
$setter = "set{$field}";
if (method_exists($this, $setter) && !empty($request->$getter())) {
$this->$setter($request->$getter());
}
}
return $this;
}
EDIT:
I am saving/updating this way:
$this->entityManager->persist($product);
$this->entityManager->flush();
I have 2 entities: Game and Product. A Game has a One to Many relationship with Product.
Game is defined as
class Game
{
/**
* #ORM\OneToMany(targetEntity=Product::class, mappedBy="game")
*/
private $products;
}
In order to delete Game when $games->getProducts() is empty, I created an onFlush listener. The issue is that the count of products is the count before the changes are actually applied. This will work fine if I remove entities one by one, but will fail if I remove 2 or more products at once as I cannot confirm that collection is empty, any idea how I could achieve this?
The Listener is defined as such:
class ChainDeletionListener {
public function onFlush(OnFlushEventArgs $event): void {
$em = $event->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityDeletions() as $entity) {
// We check if the deleted entities are Product
if ($entity instanceof Product) {
/** #var Product $entity */
$game = $entity->getGame();
if (sizeof($game->getProducts()) === 1) {
// If the Product we are removing is the last one, we also remove the Game
$em->remove($game);
$uow->computeChangeSet($em->getClassMetadata(get_class($entity)), $entity);
}
}
}
}
}
Thank you for the help!
In your #OneToMany annotation, add "cascade" attribute:
class Game
{
/**
* #ORM\OneToMany(targetEntity=Product::class, mappedBy="game", cascade={"persist", "remove"})
*/
private $products;
}
The simple approach would be to actually load the game entity with its products collection and remove the products in that collection alongside.
class ChainDeletionListener {
public function onFlush(OnFlushEventArgs $event): void {
$em = $event->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityDeletions() as $entity) {
// We check if the deleted entities are Product
if ($entity instanceof Product) {
/** #var Product $entity */
$game = $entity->getGame();
$game->removeProduct($entity); // <-- NEW, might have to implement
if (sizeof($game->getProducts()) === 0) { // <-- NEW changed size: 0
// If the Product we are removing is the last one, we also remove the Game
$em->remove($game);
$uow->computeChangeSet($em->getClassMetadata(get_class($entity)), $entity);
}
}
}
}
}
This assumes a fault-tolerant removal Game::removeProduct(Product) implementation. The obvious downside is the games' collections always being loaded. I don't see a safe way around that.
Alternatively, you could go post-flush and check for games with zero products.
For clarity I continue here the discussion started here.
Inside a Doctrine Entity Listener, in the preUpdate method (where I have access to both the old and new value of any field of the entity) I'm trying to persist an entity unrelated to the focal one.
Basically I have entity A, and when I change a value in one of the fields I want to write, in the project_notification table, the fields oldValue, newValue plus others.
If I don't flush inside the preUpdate method, the new notification entity does not get stored in DB. If I flush it I enter into a infinite loop.
This is the preUpdate method:
public function preUpdate(ProjectTolerances $tolerances, PreUpdateEventArgs $event)
{
if ($event->hasChangedField('riskToleranceFlag')) {
$project = $tolerances->getProject();
$em = $event->getEntityManager();
$notification = new ProjectNotification();
$notification->setValueFrom($event->getOldValue('riskToleranceFlag'));
$notification->setValueTo($event->getNewValue('riskToleranceFlag'));
$notification->setEntity('Entity'); //TODO substitute with the real one
$notification->setField('riskToleranceFlag');
$notification->setProject($project);
$em->persist($notification);
// $em->flush(); // gives infinite loop
}
}
Googling a bit I discovered that you cannot call the flush inside the listeners, and here it's suggested to store the stuff to be persisted in an array, to flush it later in the onFlush. Nonetheless it does not work (and probably it should not work, as the instance of the listener class gets destroyed after you call the preUpdate, so whatever you store in as protected attribute at the level of the class gets lost when you later call the onFlush, or am I missing something?).
Here is the updated version of the listener:
class ProjectTolerancesListener
{
protected $toBePersisted = [];
public function preUpdate(ProjectTolerances $tolerances, PreUpdateEventArgs $event)
{
$uow = $event->getEntityManager()->getUnitOfWork();
// $hasChanged = false;
if ($event->hasChangedField('riskToleranceFlag')) {
$project = $tolerances->getProject();
$notification = new ProjectNotification();
$notification->setValueFrom($event->getOldValue('riskToleranceFlag'));
$notification->setValueTo($event->getNewValue('riskToleranceFlag'));
$notification->setEntity('Entity'); //TODO substitute with the real one
$notification->setField('riskToleranceFlag');
$notification->setProject($project);
if(!empty($this->toBePersisted))
{
array_push($toBePersisted, $notification);
}
else
{
$toBePersisted[0] = $notification;
}
}
}
public function postFlush(LifecycleEventArgs $event)
{
if(!empty($this->toBePersisted)) {
$em = $event->getEntityManager();
foreach ($this->toBePersisted as $element) {
$em->persist($element);
}
$this->toBePersisted = [];
$em->flush();
}
}
}
Maybe I can solve this by firing an event from inside the listener with all the needed info to perform my logging operations after the flush...but:
1) I don't know if I can do it
2) It seems a bit an overkill
Thank you!
I give all the credits to Richard for pointing me into the right direction, so I'm accepting his answer. Nevertheless I also publish my answer with the complete code for future visitors.
class ProjectEntitySubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
'onFlush',
);
}
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $keyEntity => $entity) {
if ($entity instanceof ProjectTolerances) {
foreach ($uow->getEntityChangeSet($entity) as $keyField => $field) {
$notification = new ProjectNotification();
// place here all the setters
$em->persist($notification);
$classMetadata = $em->getClassMetadata('AppBundle\Entity\ProjectNotification');
$uow->computeChangeSet($classMetadata, $notification);
}
}
}
}
}
Don't use preUpdate, use onFlush - this allows you to access the UnitOfWork API & you can then persist entities.
E.g. (this is how I do it in 2.3, might be changed in newer versions)
$this->getEntityManager()->persist($entity);
$metaData = $this->getEntityManager()->getClassMetadata($className);
$this->getUnitOfWork()->computeChangeSet($metaData, $entity);
As David Baucum stated, the initial question referred to Doctrine Entity Listeners, but as a solution, the op ended up using an Event Listener.
I am sure many more will stumble upon this topic, because of the infinite loop problem.
For those that adopt the accepted answer, TAKE NOTE that the onFlush event (when using an Event Listener like above) is executed with ALL the entities that might be in queue for an update, whereas an Entity Listener is used only when doing something with the entity it was "assigned" to.
I setup a custom auditing system with symfony 4.4 and API Platform, and i managed to achieve the desired result with just an Entity Listener.
NOTE: Tested and working however, the namespaces and functions have been modified, and this is purely to demonstrate how to manipulate another entity inside a Doctrine Entity Listener.
// this goes into the main entity
/**
* #ORM\EntityListeners({"App\Doctrine\MyEntityListener"})
*/
<?
// App\Doctrine\MyEntityListener.php
namespace App\Doctrine;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Security;
// whenever an Employee record is inserted/updated
// log changes to EmployeeAudit
use App\Entity\Employee;
use App\Entity\EmployeeAudit;
private $security;
private $currentUser;
private $em;
private $audit;
public function __construct(Security $security, EntityManagerInterface $em) {
$this->security = $security;
$this->currentUser = $security->getUser();
$this->em = $em;
}
// HANDLING NEW RECORDS
/**
* since prePersist is called only when inserting a new record, the only purpose of this method
* is to mark our object as a new entry
* this method might not be necessary, but for some reason, if we set something like
* $this->isNewEntry = true, the postPersist handler will not pick up on that
* might be just me doing something wrong
*
* #param Employee $obj
* #ORM\PrePersist()
*/
public function prePersist(Employee $obj){
if(!($obj instanceof Employee)){
return;
}
$isNewEntry = !$obj->getId();
$obj->markAsNewEntry($isNewEntry);// custom Employee method (just sets an internal var to true or false, which can later be retrieved)
}
/**
* #param Employee $obj
* #ORM\PostPersist()
*/
public function postPersist(Employee $obj){
// in this case, we can flush our EmployeeAudit object safely
$this->prepareAuditEntry($obj);
}
// END OF NEW RECORDS HANDLING
// HANDLING UPDATES
/**
* #see {https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html}
* #param Employee $obj
* #param PreUpdateEventArgs $args
* #ORM\PreUpdate()
*/
public function preUpdate(Employee $obj, PreUpdateEventArgs $args){
$entity = $args->getEntity();
$changeset = $args->getEntityChangeSet();
// we just prepare our EmployeeAudit obj but don't flush anything
$this->audit = $this->prepareAuditEntry($obj, $changeset, $flush = false);
}
/**
* #ORM\PostUpdate()
*/
public function postUpdate(){
// if the preUpdate handler was called, $this->audit should exist
// NOTE: the preUpdate handler DOES NOT get called, if nothing changed
if($this->audit){
$this->em->persist($this->audit);
$this->em->flush();
}
// don't forget to unset this
$this->audit = null;
}
// END OF HANDLING UPDATES
// AUDITOR
private function prepareAuditEntry(Employee $obj, $changeset = [], $flush = true){
if(!($obj instanceof Employee) || !$obj->getId()){
// at this point, we need a DB id
return;
}
$audit = new EmployeeAudit();
// this part was cut out, since it is custom
// here you would set things to your EmployeeAudit object
// either get them from $obj, compare with the changeset, etc...
// setting some custom fields
// in case it is a new insert, the changedAt datetime will be identical to the createdAt datetime
$changedAt = $obj->isNewInsert() ? $obj->getCreatedAt() : new \DateTime('#'.strtotime('now'));
$changedFields = array_keys($changeset);
$changedCount = count($changedFields);
$changedBy = $this->currentUser->getId();
$entryId = $obj->getId();
$audit->setEntryId($entryId);
$audit->setChangedFields($changedFields);
$audit->setChangedCount($changedCount);
$audit->setChangedBy($changedBy);
$audit->setChangedAt($changedAt);
if(!$flush){
return $audit;
}
else{
$this->em->persist($audit);
$this->em->flush();
}
}
The idea is to NOT persist/flush anything inside preUpdate (except prepare your data, because you have access to the changeset and stuff), and do it postUpdate in case of updates, or postPersist in case of new inserts.
Theres a little hack I came across today. Maybe it helps to future generations.
So basicly, in onFlush Listener I cant store anything (because of deadlock or something similar If I call flush in another repository) and in postFlush i dont have access to change sets.
So I registered it as Subscriber with both events (onFlush, postFlush) implemented and just have class variable private array $entityUpdateBuffer = []; where I temp store Entities scheduled to update from onFlush event.
class MyEntityEventSubscriber implements EventSubscriber
{
private array $entityUpdateBuffer = [];
public function __construct(private MyBusiness $myBusiness)
{
}
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
Events::postFlush,
];
}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$this->entityUpdateBuffer = $uow->getScheduledEntityUpdates();
}
public function postFlush(PostFlushEventArgs $args): void
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($this->entityUpdateBuffer as $entity) {
if (!$entity instanceof MyEntity) {
continue;
}
$changeSet = $uow->getEntityChangeSet($entity);
// Call whatever that uses $entity->getId() as reference
$this->myBusiness->createChangeRecordWithEntityId(
$entity->getId(),
$changeSet,
)
}
}
}
Using an Lifecycle Listener instead of an EntityListener might be better suited in this case (I find that the symfony docs provide a better overview over the different options). This is due to onFlush, a very powerful event, not being available for EntityListeners. This event is invoked after all changesets are computed and before the database actions are executed.
In this answer I explore the options using an Entity Listener.
Using preUpdate: This event provides a PreUpdateEventArgs which makes it easy to find all values that are going to be changed. However this event is triggered within UnitOfWork#commit, after the inserts have been processed. Hence there is now no possibility to add a new entity to be persisted within current transaction.
Using preFlush: This event occurs at the beginning of a flush operation. Changesets might not yet be available, but we can compare the original values with the current ones. This approach might not be suitable when there are many changes that are needed. Here is an example implementation:
public function preFlush(Order $order, PreFlushEventArgs $eventArgs)
{
// Create a log entry when the state was changed
$entityManager = $eventArgs->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
$originalEntityData = $unitOfWork->getOriginalEntityData($order);
$newState = $order->getState();
if (empty($originalEntityData)) {
// We're dealing with a new order
$oldState = "";
} else {
$stateProperty = 'state';
$oldState = $originalEntityData[$stateProperty];
// Same behavior as in \Doctrine\ORM\UnitOfWork:720: Existing
// changeset is ignored when the property was changed
$entityChangeSet = $unitOfWork->getEntityChangeSet($order);
$stateChanges = $entityChangeSet[$stateProperty] ?? [];
if ($oldState == $newState && $stateChanges) {
$oldState = $stateChanges[0] ?? "";
$newState = $stateChanges[1] ?? "";
}
}
if ($oldState != $newState) {
$statusLog = $this->createOrderStatusLog($order, $oldState, $newState);
$unitOfWork->scheduleForInsert($statusLog);
$unitOfWork->computeChangeSet($entityManager->getClassMetadata('App\Entity\OrderStatusLog'), $statusLog);
}
}
Using postFlush/postUpdate: Using these events would lead to a second database transaction, which is undesirable.
I have set up an event listener with a preUpdate function, and I am trying to use this to update an address entity related through a OneToMany relationship. The function runs, and the object is updated, but this is not then persisted tot he database.
You can see the code below, does anyone have any ideas?
public function preUpdate(LifecycleEventArgs $args){
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Company) {
$key = 0;
$areaCode = explode(' ', $this->parsePostcode($entity->getAddresses()[$key]->getPostCode() ));
$geo = $entityManager->getRepository('AlphaRMSBundle:SelGeo')->findOneByPostcode($areaCode[0]);
if ($geo instanceof SelGeo) {
$entity->getAddresses()[$key]->setEastings($geo->getEastings());
$entity->getAddresses()[$key]->setNorthings($geo->getNorthings());
$entity->getAddresses()[$key]->setLatitude($geo->getLatitude());
$entity->getAddresses()[$key]->setLongitude($geo->getLongitude());
$entity->getAddresses()[$key]->setMunicipality($geo->getTownCity());
$entity->getAddresses()[$key]->setRegion($geo->getRegion());
$entity->getAddresses()[$key]->setCountry($geo->getCountryString());
$entity->getAddresses()[$key]->setCountryUkCode($geo->getCountryUkCode());
//die($entity->getAddresses()[$key]->getMunicipality());
}
}
}
I have a Doctrine Event Listener that listens to the onFlush event. I use it to update an eTag on an entity when saved.
I need to get access to the entities scheduled for deletion, so I can access their associated object, however:
I'm using a soft-deletable filter, so the entities aren't actually in $uow->getScheduledEntityDeletions(), they're in $uow->extraUpdates marking the deleted flag changed instead.
This variable is private and I don't know of any programatic way to get notified of this change. Any ideas?
private function updateIfRequired(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
// Entities either updated or just inserted
$upsertedEntities = array_merge(
$uow->getScheduledEntityUpdates(),
$uow->getScheduledEntityInsertions()
);
foreach ($upsertedEntities as $entity) {
if ($entity instanceof ETaggableInterface || $entity instanceof ETagRelatedInterface) {
$this->updateETag($entity);
}
}
// When soft-deleted, this and getScheduledEntityUpdates are both empty!
$deletedEntities = $uow->getScheduledEntityDeletions();
foreach ($deletedEntities as $entity) {
$this->deletedEntities[spl_object_hash($entity)] = $entity;
$this->updateETag($entity);
}
}
So, the detailed answer for this question would be something like this:
Listen to the preSoftDelete event (Symfony2 DoctrineExtensions preSoftDelete event call):
tags:
- { name: doctrine.event_listener, event: preSoftDelete }
Then in your listener class, add the following method:
public function preSoftDelete(LifecycleEventArgs $args){
$entity = $args->getEntity();
$em = $args->getEntityManager();
if ($entity instanceof ETaggableInterface || $entity instanceof ETagRelatedInterface) {
$entity->updateEtag('foo'); // Was not clear what $this->updateEtag() do
$em->persist($entity);
$classMetadata = $em->getClassMetadata(get_class($entity));
$em->getUnitOfWork()->computeChangeSet($classMetadata, $entity);
}
}
This will update the entity, tell to persist it, calculate the changes.
Maybe you should listen to preSoftDelete events: https://github.com/Atlantic18/DoctrineExtensions/blob/master/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php#L74