1414
1515use App \Audit \Interfaces \IAuditStrategy ;
1616use Doctrine \ORM \Event \OnFlushEventArgs ;
17+ use Doctrine \ORM \EntityManagerInterface ;
18+ use Doctrine \ORM \PersistentCollection ;
1719use Illuminate \Support \Facades \App ;
1820use Illuminate \Support \Facades \Log ;
1921use Illuminate \Support \Facades \Route ;
2527class AuditEventListener
2628{
2729 private const ROUTE_METHOD_SEPARATOR = '| ' ;
28-
30+ private $ em ;
2931 public function onFlush (OnFlushEventArgs $ eventArgs ): void
3032 {
3133 if (app ()->environment ('testing ' )) {
3234 return ;
3335 }
34- $ em = $ eventArgs ->getObjectManager ();
35- $ uow = $ em ->getUnitOfWork ();
36+ $ this -> em = $ eventArgs ->getObjectManager ();
37+ $ uow = $ this -> em ->getUnitOfWork ();
3638 // Strategy selection based on environment configuration
37- $ strategy = $ this ->getAuditStrategy ($ em );
39+ $ strategy = $ this ->getAuditStrategy ($ this -> em );
3840 if (!$ strategy ) {
3941 return ; // No audit strategy enabled
4042 }
@@ -52,11 +54,23 @@ public function onFlush(OnFlushEventArgs $eventArgs): void
5254
5355 foreach ($ uow ->getScheduledEntityDeletions () as $ entity ) {
5456 $ strategy ->audit ($ entity , [], IAuditStrategy::EVENT_ENTITY_DELETION , $ ctx );
57+ }
58+ foreach ($ uow ->getScheduledCollectionDeletions () as $ col ) {
59+ [$ subject , $ payload , $ eventType ] = $ this ->auditCollection ($ col , IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE );
60+
61+ if (!is_null ($ subject )) {
62+ $ strategy ->audit ($ subject , $ payload , $ eventType , $ ctx );
63+ }
5564 }
5665
5766 foreach ($ uow ->getScheduledCollectionUpdates () as $ col ) {
58- $ strategy ->audit ($ col , [], IAuditStrategy::EVENT_COLLECTION_UPDATE , $ ctx );
67+ [$ subject , $ payload , $ eventType ] = $ this ->auditCollection ($ col , IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE );
68+
69+ if (!is_null ($ subject )) {
70+ $ strategy ->audit ($ subject , $ payload , $ eventType , $ ctx );
71+ }
5972 }
73+
6074 } catch (\Exception $ e ) {
6175 Log::error ('Audit event listener failed ' , [
6276 'error ' => $ e ->getMessage (),
@@ -98,7 +112,7 @@ private function buildAuditContext(): AuditContext
98112 $ member = $ memberRepo ->findOneBy (["user_external_id " => $ userExternalId ]);
99113 }
100114
101- // $ui = app()->bound('ui.context') ? app('ui.context') : [];
115+ $ ui = [];
102116
103117 $ req = request ();
104118 $ rawRoute = null ;
@@ -127,4 +141,101 @@ private function buildAuditContext(): AuditContext
127141 rawRoute: $ rawRoute
128142 );
129143 }
144+
145+ /**
146+ * Audit collection changes
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 string $eventType The event type constant (EVENT_COLLECTION_MANYTOMANY_DELETE or EVENT_COLLECTION_MANYTOMANY_UPDATE)
152+ * @return array [$subject, $payload, $eventType]
153+ */
154+ private function auditCollection ($ subject , string $ eventType ): array
155+ {
156+ if (!$ subject instanceof PersistentCollection) {
157+ return [null , null , null ];
158+ }
159+
160+ $ mapping = $ subject ->getMapping ();
161+
162+ if (!$ mapping ->isManyToMany ()) {
163+ return [$ subject , [], IAuditStrategy::EVENT_COLLECTION_UPDATE ];
164+ }
165+
166+ if (!$ mapping ->isOwningSide ()) {
167+ Log::debug ("AuditEventListener::Skipping audit for non-owning side of many-to-many collection " );
168+ return [null , null , null ];
169+ }
170+
171+ $ owner = $ subject ->getOwner ();
172+ if ($ owner === null ) {
173+ return [null , null , null ];
174+ }
175+
176+ $ payload = ['collection ' => $ subject ];
177+
178+ if ($ eventType === IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE
179+ && (
180+ !$ subject ->isInitialized ()
181+ || ($ subject ->isInitialized () && count ($ subject ->getDeleteDiff ()) === 0 )
182+ )) {
183+ if ($ this ->em instanceof EntityManagerInterface) {
184+ $ payload ['deleted_ids ' ] = $ this ->fetchManyToManyIds ($ subject , $ this ->em );
185+ }
186+ }
187+
188+ return [$ owner , $ payload , $ eventType ];
189+ }
190+
191+
192+ private function fetchManyToManyIds (PersistentCollection $ collection , EntityManagerInterface $ em ): array
193+ {
194+ try {
195+ $ mapping = $ collection ->getMapping ();
196+ $ joinTable = $ mapping ->joinTable ;
197+ $ tableName = is_array ($ joinTable ) ? ($ joinTable ['name ' ] ?? null ) : ($ joinTable ->name ?? null );
198+ $ joinColumns = is_array ($ joinTable ) ? ($ joinTable ['joinColumns ' ] ?? []) : ($ joinTable ->joinColumns ?? []);
199+ $ inverseJoinColumns = is_array ($ joinTable ) ? ($ joinTable ['inverseJoinColumns ' ] ?? []) : ($ joinTable ->inverseJoinColumns ?? []);
200+
201+ $ joinColumn = $ joinColumns [0 ] ?? null ;
202+ $ inverseJoinColumn = $ inverseJoinColumns [0 ] ?? null ;
203+ $ sourceColumn = is_array ($ joinColumn ) ? ($ joinColumn ['name ' ] ?? null ) : ($ joinColumn ->name ?? null );
204+ $ targetColumn = is_array ($ inverseJoinColumn ) ? ($ inverseJoinColumn ['name ' ] ?? null ) : ($ inverseJoinColumn ->name ?? null );
205+
206+ if (!$ sourceColumn || !$ targetColumn || !$ tableName ) {
207+ return [];
208+ }
209+
210+ $ owner = $ collection ->getOwner ();
211+ if ($ owner === null ) {
212+ return [];
213+ }
214+
215+ $ ownerId = method_exists ($ owner , 'getId ' ) ? $ owner ->getId () : null ;
216+ if ($ ownerId === null ) {
217+ $ ownerMeta = $ em ->getClassMetadata (get_class ($ owner ));
218+ $ ownerIds = $ ownerMeta ->getIdentifierValues ($ owner );
219+ $ ownerId = empty ($ ownerIds ) ? null : reset ($ ownerIds );
220+ }
221+
222+ if ($ ownerId === null ) {
223+ return [];
224+ }
225+
226+ $ ids = $ em ->getConnection ()->fetchFirstColumn (
227+ "SELECT {$ targetColumn } FROM {$ tableName } WHERE {$ sourceColumn } = ? " ,
228+ [$ ownerId ]
229+ );
230+
231+ return array_values (array_map ('intval ' , $ ids ));
232+
233+ } catch (\Exception $ e ) {
234+ Log::error ("AuditEventListener::fetchManyToManyIds error: " . $ e ->getMessage (), [
235+ 'exception ' => get_class ($ e ),
236+ 'trace ' => $ e ->getTraceAsString ()
237+ ]);
238+ return [];
239+ }
240+ }
130241}
0 commit comments