Symfony. Cómo detectar cambios en una colección de una entidad
Detectar los cambios en una entidad en el evento onFlush de Doctrine

Hoy os traigo una de esas recetas que, si bien está perfectamente documentada en la web de Doctrine, dar con ella no es tan sencillo si no estás familiarizado con el concepto de UnitOfWork
y los LifeCycleEvents
.
Básicamente, de lo que trata este artículo es de cómo poder detectar cambios en un campo OneToMany
de una entidad, de modo que podamos por ejemplo loggearlos o crear otras entidades cuando se produce un cambio.
En mi caso, lo que necesitaba es saber qué entidades son añadidas y cuáles eliminadas de una colección. Por ejemplo, llevar un histórico de qué comentarios son añadidos o eliminados de un artículo:
Así que voy a contaros a continuación la forma en que lo he resuelto. ¡Espero que os sirva!
Explicación del evento OnFlush de Doctrine
Lo primero de todo es escoger el evento adecuado para llevar a cabo esta operación.
Dado que necesitamos que Doctrine haya calculado todos los cambios hechos en las colecciones de la entidad para poder acceder a ellos, el evento al que necesitaremos engancharnos es OnFlush
, ya que, y esto es importante, el evento preUpdate
no permite actualizar las relaciones de la entidad:
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
Puede que en vuestro caso de uso sí que podáis, pero en el mío concreto la entidad de la que estoy llevando el histórico (en este artículo, la entidadPost)
tiene asociado el campo historyRecords
al que añadiré en el evento los cambios detectados, por lo que preUpdate
no me sirve.
Dicho esto, para escuchar el evento onFlush
basta con declarar nuestro servicio de la siguiente forma:
App\Doctrine\EntityListener\PostEntityListener:tags:- { name: doctrine.event_listener, event: onFlush }
Ojo. El evento onFlush
no es un lifecycle callback, es decir, no podemos asignarle a un entidad en concreto como sí podemos hacer con preUpdate
, prePersist
:
Por tanto, tendremos que filtrar dentro de nuestro método para detectar cambios solo en la entidad que queramos.
Sabiendo esto, la clase PostEntityListener
se nos puede quedar con el siguiente aspecto:
De este código hay que destacar 3 cosas, (prestad atención a la última que os ahorrará algún que otro quebradero de cabeza).
UnitOfWork
me da la unidad de trabajo que va a ser procesada en elflush
. Tiene diferentes métodos comogetScheduledEntityInsertions
,getScheduledEntityUpdates
y, el que me interesa a mí,getScheduledCollectionUpdates
, el cual me permite obtener los cambios en las colecciones.- Puesto que
onFlush
no es un lifecycle callback, necesito comprobar a mano que se va a actualizar una colección de la entidadPost
, ya que este listener será llamado siempre que se actualice una colección. - Y aquí lo importante, la clase de la variable
$entity
es la de la entidad que posee la relación (el owning side, vamos, de ahí que estemos llamando al métodogetOwner
). Así que dependiendo de vuestro modelo obtendréis la clasePost
o la claseComment
. Recordad que el owning side es el que lleva el inversedBy.
Detectando los cambios en la colección
Una vez que ya estamos seguros de que estamos trabajando con cambios en una colección de la entidad Post
, lo siguiente será detectar lo que se ha añadido y lo que se ha eliminado.
Esto es realmente sencillo, ya que la variable $collection
tiene dos métodos:
getDeleteDiff
, con las entidades eliminadasgetInsertdiff
, con las entidades añadidas.
Por tanto, podremos escribir lo siguiente:
Lo que hago es recorrer ambos arrays e ir creando objetos de la clase HistoryRecord
para registrar los comentarios eliminados o añadidos. Y con esto en teoría ya estaría, pero como siempre, queda un último escollo…
Invalid parameter number: number of bound variables does not match number of tokens
Si vais a probar directamente el listener sin añadir la línea mágica, os toparéis con este fallo, es decir, como si no se estuvieran detectando los campos de la entidad HistoryRecord
recién creada.
Esto se debe a una característica propia del evento onFlush
el cual nos exige lo siguiente (sacado de la documentación):
- If you create and persist a new entity in
onFlush
, then callingEntityManager#persist()
is not enough. You have to execute an additional call to$unitOfWork->computeChangeSet($classMetadata, $entity)
. - Changing primitive fields or associations requires you to explicitly trigger a re-computation of the changeset of the affected entity. This can be done by calling
$unitOfWork->recomputeSingleEntityChangeSet($classMetadata, $entity)
.
Es decir, que necesitamos lanzar manualmente en nuestro caso el evento computeChangeSet
para cada objeto HistoryRecord
que creemos de cara a que Doctrine pueda insertarlos correctamente.
Añadiendo dicha línea a nuestro código ya tendremos la funcionalidad que buscábamos:
Enlaces de utilidad
¿Quieres recibir más artículos como este?
Suscríbete a nuestra newsletter: