Skip to content

Commit eeca602

Browse files
fix: change in strategy for adapt to adr
1 parent 9d27bf5 commit eeca602

19 files changed

Lines changed: 1251 additions & 533 deletions

app/Audit/AbstractAuditLogFormatter.php

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,42 @@ final public function setContext(AuditContext $ctx): void
3434
$this->ctx = $ctx;
3535
}
3636

37-
protected function processCollection(
38-
object $col,
39-
bool $isDeletion = false
40-
): ?array
37+
protected function handleManyToManyCollection(array $change_set): ?PersistentCollectionMetadata
4138
{
42-
if (!($col instanceof PersistentCollection)) {
39+
if (!isset($change_set['collection'])) {
4340
return null;
4441
}
4542

46-
$mapping = $col->getMapping();
43+
$collection = $change_set['collection'];
44+
if (!($collection instanceof PersistentCollection)) {
45+
return null;
46+
}
4747

48-
$addedEntities = $col->getInsertDiff();
49-
$removedEntities = $col->getDeleteDiff();
48+
$preloadedDeletedIds = $change_set['deleted_ids'] ?? [];
49+
if (!is_array($preloadedDeletedIds)) {
50+
$preloadedDeletedIds = [];
51+
}
5052

51-
$addedIds = $this->extractCollectionEntityIds($addedEntities);
52-
$removedIds = $this->extractCollectionEntityIds($removedEntities);
53+
return PersistentCollectionMetadata::fromCollection($collection, $preloadedDeletedIds);
54+
}
5355

56+
protected function processCollection(PersistentCollectionMetadata $metadata): ?array
57+
{
58+
$addedIds = [];
59+
$removedIds = [];
60+
61+
if (!empty($metadata->preloadedDeletedIds)) {
62+
$removedIds = array_values(array_unique(array_map('intval', $metadata->preloadedDeletedIds)));
63+
sort($removedIds);
64+
} else {
65+
$addedIds = $this->extractCollectionEntityIds($metadata->collection->getInsertDiff());
66+
$removedIds = $this->extractCollectionEntityIds($metadata->collection->getDeleteDiff());
67+
}
5468

5569
return [
56-
'field' => $mapping->fieldName ?? 'unknown',
57-
'target_entity' => $mapping->targetEntity ?? 'unknown',
58-
'is_deletion' => $isDeletion,
70+
'field' => $metadata->fieldName,
71+
'target_entity' => class_basename($metadata->targetEntity),
72+
'is_deletion' => $this->event_type === Interfaces\IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE,
5973
'added_ids' => $addedIds,
6074
'removed_ids' => $removedIds,
6175
];

app/Audit/AuditEventListener.php

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
use App\Audit\Interfaces\IAuditStrategy;
1616
use Doctrine\ORM\Event\OnFlushEventArgs;
17-
use Doctrine\ORM\Mapping\ClassMetadata;
17+
use Doctrine\ORM\EntityManagerInterface;
1818
use Doctrine\ORM\PersistentCollection;
1919
use Illuminate\Support\Facades\App;
2020
use Illuminate\Support\Facades\Log;
@@ -27,16 +27,16 @@
2727
class 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
}

app/Audit/AuditLogFormatterFactory.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
use App\Audit\ConcreteFormatters\EntityCollectionUpdateAuditLogFormatter;
1919
use App\Audit\ConcreteFormatters\EntityCreationAuditLogFormatter;
2020
use App\Audit\ConcreteFormatters\EntityDeletionAuditLogFormatter;
21-
use App\Audit\ConcreteFormatters\EntityManyToManyCollectionUpdateAuditLogFormatter;
22-
use App\Audit\ConcreteFormatters\EntityManyToManyCollectionDeleteAuditLogFormatter;
21+
use App\Audit\ConcreteFormatters\DefaultEntityManyToManyCollectionUpdateAuditLogFormatter;
22+
use App\Audit\ConcreteFormatters\DefaultEntityManyToManyCollectionDeleteAuditLogFormatter;
2323
use App\Audit\ConcreteFormatters\EntityUpdateAuditLogFormatter;
2424
use App\Audit\Interfaces\IAuditStrategy;
2525
use Doctrine\ORM\PersistentCollection;
@@ -109,13 +109,13 @@ public function make(AuditContext $ctx, $subject, string $event_type): ?IAuditLo
109109
case IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE:
110110
$formatter = $this->getFormatterByContext($subject, $event_type, $ctx);
111111
if (is_null($formatter)) {
112-
$formatter = new EntityManyToManyCollectionUpdateAuditLogFormatter();
112+
$formatter = new DefaultEntityManyToManyCollectionUpdateAuditLogFormatter();
113113
}
114114
break;
115115
case IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE:
116116
$formatter = $this->getFormatterByContext($subject, $event_type, $ctx);
117117
if (is_null($formatter)) {
118-
$formatter = new EntityManyToManyCollectionDeleteAuditLogFormatter();
118+
$formatter = new DefaultEntityManyToManyCollectionDeleteAuditLogFormatter();
119119
}
120120
break;
121121
case IAuditStrategy::EVENT_ENTITY_CREATION:

app/Audit/AuditLogOtlpStrategy.php

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -168,25 +168,42 @@ private function buildAuditLogData($entity, $subject, array $change_set, string
168168
case IAuditStrategy::EVENT_COLLECTION_UPDATE:
169169
if ($subject instanceof PersistentCollection) {
170170
$data['audit.collection_type'] = $this->getCollectionType($subject);
171-
$data['audit.collection_count'] = count($subject);
172-
173-
$changes = $this->getCollectionChanges($subject, $change_set);
174-
$data['audit.collection_current_count'] = $changes['current_count'];
175-
$data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
176-
$data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
171+
if ($subject->isInitialized()) {
172+
$data['audit.collection_count'] = count($subject);
173+
$changes = $this->getCollectionChanges($subject, $change_set);
174+
$data['audit.collection_current_count'] = $changes['current_count'];
175+
$data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
176+
$data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
177+
} else {
178+
$data['audit.collection_count'] = 0;
179+
$data['audit.collection_current_count'] = 0;
180+
$data['audit.collection_snapshot_count'] = 0;
181+
$data['audit.collection_is_dirty'] = 'true';
182+
}
177183
}
178184
break;
179185
case IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE:
180186
case IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE:
181187
if (isset($change_set['collection']) && $change_set['collection'] instanceof PersistentCollection) {
182188
$collection = $change_set['collection'];
183189
$data['audit.collection_type'] = $this->getCollectionType($collection);
184-
$data['audit.collection_count'] = count($collection);
185-
186-
$changes = $this->getCollectionChanges($collection, $change_set);
187-
$data['audit.collection_current_count'] = $changes['current_count'];
188-
$data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
189-
$data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
190+
if (!empty($change_set['deleted_ids'])) {
191+
$data['audit.collection_count'] = count($change_set['deleted_ids']);
192+
$data['audit.collection_current_count'] = 0;
193+
$data['audit.collection_snapshot_count'] = 0;
194+
$data['audit.collection_is_dirty'] = 'true';
195+
} elseif ($collection->isInitialized()) {
196+
$data['audit.collection_count'] = count($collection);
197+
$changes = $this->getCollectionChanges($collection, $change_set);
198+
$data['audit.collection_current_count'] = $changes['current_count'];
199+
$data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
200+
$data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
201+
} else {
202+
$data['audit.collection_count'] = 0;
203+
$data['audit.collection_current_count'] = 0;
204+
$data['audit.collection_snapshot_count'] = 0;
205+
$data['audit.collection_is_dirty'] = 'true';
206+
}
190207
}
191208
break;
192209
}
@@ -206,7 +223,7 @@ private function getCollectionType(PersistentCollection $collection): string
206223
}
207224
return class_basename($targetEntity);
208225
} catch (\Exception $ex) {
209-
return 'AuditLogOtlpStrategy:: unknown targetEntity';
226+
return 'unknown';
210227
}
211228
}
212229

app/Audit/ConcreteFormatters/EntityManyToManyCollectionDeleteAuditLogFormatter.php renamed to app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use App\Audit\AbstractAuditLogFormatter;
66
use App\Audit\ConcreteFormatters\ChildEntityFormatters\IChildEntityAuditLogFormatter;
77
use App\Audit\Interfaces\IAuditStrategy;
8-
use Doctrine\ORM\PersistentCollection;
98
use Illuminate\Support\Facades\Log;
109

1110
/**
@@ -24,7 +23,7 @@
2423
/**
2524
* Formatter for Many-to-Many collection deletions
2625
*/
27-
class EntityManyToManyCollectionDeleteAuditLogFormatter extends AbstractAuditLogFormatter
26+
class DefaultEntityManyToManyCollectionDeleteAuditLogFormatter extends AbstractAuditLogFormatter
2827
{
2928
/**
3029
* @var IChildEntityAuditLogFormatter|null
@@ -43,40 +42,43 @@ public function __construct(mixed $child_entity_formatter = null)
4342
public function format($subject, array $change_set): ?string
4443
{
4544
try {
46-
47-
48-
$collection = is_array($change_set) && isset($change_set['collection'])
49-
? $change_set['collection']
50-
: null;
51-
52-
if ($collection === null) {
45+
$metadata = $this->handleManyToManyCollection($change_set);
46+
if ($metadata === null) {
5347
return null;
5448
}
5549

5650
$changes = [];
57-
$deleteDiff = $collection->getDeleteDiff();
5851

59-
if ($this->child_entity_formatter != null) {
52+
if ($this->child_entity_formatter != null && empty($metadata->preloadedDeletedIds)) {
53+
$deleteDiff = $metadata->collection->getDeleteDiff();
6054
foreach ($deleteDiff as $child_changed_entity) {
61-
$changes[] = $this->child_entity_formatter
55+
$formatted = $this->child_entity_formatter
6256
->format($child_changed_entity, IChildEntityAuditLogFormatter::CHILD_ENTITY_DELETION);
57+
if ($formatted !== null) {
58+
$changes[] = $formatted;
59+
}
6360
}
6461

6562
if (!empty($changes)) {
6663
return implode("|", $changes);
6764
}
6865
} else {
69-
$deleted_count = count($deleteDiff);
66+
$collectionData = $this->processCollection($metadata);
67+
if ($collectionData === null) {
68+
return null;
69+
}
7070

71-
if ($deleted_count > 0) {
72-
$details = $this->buildManyToManyDetailedMessage($collection, [], $deleteDiff);
73-
return self::formatManyToManyDetailedMessage($details, 0, $deleted_count, 'deleted');
71+
$deletedCount = count($collectionData['removed_ids']);
72+
if ($deletedCount === 0) {
73+
return sprintf("Many-to-Many collection '%s' deleted: Removed IDs: []", $collectionData['field']);
7474
}
75+
76+
return self::formatManyToManyDetailedMessage($collectionData, 0, $deletedCount, 'deleted');
7577
}
7678

7779
return null;
7880
} catch (\Throwable $e) {
79-
Log::error(get_class($this) . " error: " . $e->getMessage());
81+
Log::error(get_class($this) . " error: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
8082
return null;
8183
}
8284
}

0 commit comments

Comments
 (0)