1414
1515use App \Audit \Interfaces \IAuditStrategy ;
1616use Doctrine \ORM \Event \OnFlushEventArgs ;
17- use Doctrine \ORM \Mapping \ ClassMetadata ;
17+ use Doctrine \ORM \EntityManagerInterface ;
1818use Doctrine \ORM \PersistentCollection ;
1919use Illuminate \Support \Facades \App ;
2020use Illuminate \Support \Facades \Log ;
2727class AuditEventListener
2828{
2929 private const ROUTE_METHOD_SEPARATOR = '| ' ;
30-
30+ private $ em ;
3131 public function onFlush (OnFlushEventArgs $ eventArgs ): void
3232 {
3333 if (app ()->environment ('testing ' )) {
3434 return ;
3535 }
36- $ em = $ eventArgs ->getObjectManager ();
37- $ uow = $ em ->getUnitOfWork ();
36+ $ this -> em = $ eventArgs ->getObjectManager ();
37+ $ uow = $ this -> em ->getUnitOfWork ();
3838 // Strategy selection based on environment configuration
39- $ strategy = $ this ->getAuditStrategy ($ em );
39+ $ strategy = $ this ->getAuditStrategy ($ this -> em );
4040 if (!$ strategy ) {
4141 return ; // No audit strategy enabled
4242 }
@@ -54,12 +54,21 @@ public function onFlush(OnFlushEventArgs $eventArgs): void
5454
5555 foreach ($ uow ->getScheduledEntityDeletions () as $ entity ) {
5656 $ strategy ->audit ($ entity , [], IAuditStrategy::EVENT_ENTITY_DELETION , $ ctx );
57- }
57+ }
5858 foreach ($ uow ->getScheduledCollectionDeletions () as $ col ) {
59- $ this ->auditCollection ($ col , $ strategy , $ ctx , $ uow , true );
59+ [$ subject , $ payload , $ eventType ] = $ this ->auditCollection ($ col , $ uow , IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE );
60+
61+ if (!is_null ($ subject )) {
62+ $ strategy ->audit ($ subject , $ payload , $ eventType , $ ctx );
63+ }
6064 }
65+
6166 foreach ($ uow ->getScheduledCollectionUpdates () as $ col ) {
62- $ this ->auditCollection ($ col , $ strategy , $ ctx , $ uow , false );
67+ [$ subject , $ payload , $ eventType ] = $ this ->auditCollection ($ col , $ uow , IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE );
68+
69+ if (!is_null ($ subject )) {
70+ $ strategy ->audit ($ subject , $ payload , $ eventType , $ ctx );
71+ }
6372 }
6473
6574 } catch (\Exception $ e ) {
@@ -135,40 +144,96 @@ private function buildAuditContext(): AuditContext
135144
136145 /**
137146 * Audit collection changes
138- * Only determines if it's ManyToMany and emits appropriate event
147+ * Returns triple: [$subject, $payload, $eventType]
148+ * Subject will be null if collection should not be audited
149+ *
150+ * @param object $subject The collection
151+ * @param mixed $uow The UnitOfWork
152+ * @param string $eventType The event type constant (EVENT_COLLECTION_MANYTOMANY_DELETE or EVENT_COLLECTION_MANYTOMANY_UPDATE)
153+ * @return array [$subject, $payload, $eventType]
139154 */
140- private function auditCollection ($ subject , IAuditStrategy $ strategy , AuditContext $ ctx , $ uow , bool $ isDeletion = false ): void
155+ private function auditCollection ($ subject , $ uow , string $ eventType ): array
141156 {
142157 if (!$ subject instanceof PersistentCollection) {
143- return ;
158+ return [ null , null , null ] ;
144159 }
145160
146161 $ mapping = $ subject ->getMapping ();
162+
147163 if (!$ mapping ->isManyToMany ()) {
148- $ strategy ->audit ($ subject , [], IAuditStrategy::EVENT_COLLECTION_UPDATE , $ ctx );
149- return ;
164+ return [$ subject , [], IAuditStrategy::EVENT_COLLECTION_UPDATE ];
150165 }
151166
152- $ isOwningSide = $ mapping ->isOwningSide ();
153- if (!$ isOwningSide ) {
154- Log::debug ("AuditEventListerner::Skipping audit for non-owning side of many-to-many collection " );
155- return ;
167+ if (!$ mapping ->isOwningSide ()) {
168+ Log::debug ("AuditEventListener::Skipping audit for non-owning side of many-to-many collection " );
169+ return [null , null , null ];
156170 }
157171
158172 $ owner = $ subject ->getOwner ();
159173 if ($ owner === null ) {
160- return ;
174+ return [null , null , null ];
175+ }
176+
177+ $ payload = ['collection ' => $ subject ];
178+
179+ if ($ eventType === IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE
180+ && !$ subject ->isInitialized () ) {
181+ if ($ this ->em instanceof EntityManagerInterface) {
182+ $ payload ['deleted_ids ' ] = $ this ->fetchManyToManyIds ($ subject , $ this ->em );
183+ }
161184 }
162185
163- $ payload = [
164- 'collection ' => $ subject ,
165- 'uow ' => $ uow ,
166- 'is_deletion ' => $ isDeletion ,
167- ];
168- $ eventType = $ isDeletion
169- ? IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE
170- : IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE ;
186+ return [$ owner , $ payload , $ eventType ];
187+ }
188+
171189
172- $ strategy ->audit ($ owner , $ payload , $ eventType , $ ctx );
190+ private function fetchManyToManyIds (PersistentCollection $ collection , EntityManagerInterface $ em ): array
191+ {
192+ try {
193+ $ mapping = $ collection ->getMapping ();
194+ $ joinTable = $ mapping ->joinTable ;
195+ $ tableName = is_array ($ joinTable ) ? ($ joinTable ['name ' ] ?? null ) : ($ joinTable ->name ?? null );
196+ $ joinColumns = is_array ($ joinTable ) ? ($ joinTable ['joinColumns ' ] ?? []) : ($ joinTable ->joinColumns ?? []);
197+ $ inverseJoinColumns = is_array ($ joinTable ) ? ($ joinTable ['inverseJoinColumns ' ] ?? []) : ($ joinTable ->inverseJoinColumns ?? []);
198+
199+ $ joinColumn = $ joinColumns [0 ] ?? null ;
200+ $ inverseJoinColumn = $ inverseJoinColumns [0 ] ?? null ;
201+ $ sourceColumn = is_array ($ joinColumn ) ? ($ joinColumn ['name ' ] ?? null ) : ($ joinColumn ->name ?? null );
202+ $ targetColumn = is_array ($ inverseJoinColumn ) ? ($ inverseJoinColumn ['name ' ] ?? null ) : ($ inverseJoinColumn ->name ?? null );
203+
204+ if (!$ sourceColumn || !$ targetColumn || !$ tableName ) {
205+ return [];
206+ }
207+
208+ $ owner = $ collection ->getOwner ();
209+ if ($ owner === null ) {
210+ return [];
211+ }
212+
213+ $ ownerId = method_exists ($ owner , 'getId ' ) ? $ owner ->getId () : null ;
214+ if ($ ownerId === null ) {
215+ $ ownerMeta = $ em ->getClassMetadata (get_class ($ owner ));
216+ $ ownerIds = $ ownerMeta ->getIdentifierValues ($ owner );
217+ $ ownerId = empty ($ ownerIds ) ? null : reset ($ ownerIds );
218+ }
219+
220+ if ($ ownerId === null ) {
221+ return [];
222+ }
223+
224+ $ ids = $ em ->getConnection ()->fetchFirstColumn (
225+ "SELECT {$ targetColumn } FROM {$ tableName } WHERE {$ sourceColumn } = ? " ,
226+ [$ ownerId ]
227+ );
228+
229+ return array_values (array_map ('intval ' , $ ids ));
230+
231+ } catch (\Exception $ e ) {
232+ Log::error ("AuditEventListener::fetchManyToManyIds error: " . $ e ->getMessage (), [
233+ 'exception ' => get_class ($ e ),
234+ 'trace ' => $ e ->getTraceAsString ()
235+ ]);
236+ return [];
237+ }
173238 }
174239}
0 commit comments