Skip to content

Commit c6ecdd0

Browse files
committed
fix: refactor AuditLogFormatterFactory
Resolve child entity formatter using collection ClassMetadata (getTypeClass) instead of accessing elements/counting the collection, preventing unintended hydration and large per-request memory growth.
1 parent 6789395 commit c6ecdd0

File tree

2 files changed

+77
-19
lines changed

2 files changed

+77
-19
lines changed

app/Audit/AuditLogFormatterFactory.php

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
use App\Audit\ConcreteFormatters\EntityDeletionAuditLogFormatter;
1818
use App\Audit\ConcreteFormatters\EntityUpdateAuditLogFormatter;
1919
use App\Audit\Interfaces\IAuditStrategy;
20-
20+
use Doctrine\ORM\PersistentCollection;
21+
use Illuminate\Support\Facades\Log;
22+
use Doctrine\ORM\Mapping\ClassMetadata;
2123
class AuditLogFormatterFactory implements IAuditLogFormatterFactory
2224
{
2325

@@ -34,14 +36,69 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
3436
$formatter = null;
3537
switch ($eventType) {
3638
case IAuditStrategy::EVENT_COLLECTION_UPDATE:
37-
$child_entity = null;
38-
if (count($subject) > 0) {
39-
$child_entity = $subject[0];
40-
}
41-
if (is_null($child_entity) && isset($subject->getSnapshot()[0]) && count($subject->getSnapshot()) > 0) {
42-
$child_entity = $subject->getSnapshot()[0];
39+
$child_entity_formatter = null;
40+
41+
if ($subject instanceof PersistentCollection) {
42+
$targetEntity = null;
43+
Log::debug
44+
(
45+
sprintf
46+
(
47+
"AuditLogFormatterFactory::make subject is a PersistentCollection isInitialized %b ?",
48+
$subject->isInitialized()
49+
)
50+
);
51+
if (method_exists($subject, 'getTypeClass')) {
52+
Log::debug(sprintf());
53+
$type = $subject->getTypeClass();
54+
// Your log shows this is ClassMetadata
55+
if ($type instanceof ClassMetadata) {
56+
// Doctrine supports either getName() or public $name
57+
$targetEntity = method_exists($type, 'getName') ? $type->getName() : ($type->name ?? null);
58+
} elseif (is_string($type)) {
59+
$targetEntity = $type;
60+
}
61+
Log::debug("AuditLogFormatterFactory::make getTypeClass targetEntity {$targetEntity}");
62+
}
63+
elseif (method_exists($subject, 'getMapping')) {
64+
$mapping = $subject->getMapping();
65+
$targetEntity = $mapping['targetEntity'] ?? null;
66+
Log::debug("AuditLogFormatterFactory::make getMapping targetEntity {$targetEntity}");
67+
} else {
68+
// last-resort: read private association metadata (still no hydration)
69+
$ref = new \ReflectionObject($subject);
70+
foreach (['association', 'mapping', 'associationMapping'] as $propName) {
71+
if ($ref->hasProperty($propName)) {
72+
$prop = $ref->getProperty($propName);
73+
$prop->setAccessible(true);
74+
$mapping = $prop->getValue($subject);
75+
$targetEntity = $mapping['targetEntity'] ?? null;
76+
if ($targetEntity) break;
77+
}
78+
}
79+
}
80+
81+
if ($targetEntity) {
82+
// IMPORTANT: build formatter WITHOUT touching collection items
83+
$child_entity_formatter = ChildEntityFormatterFactory::build($targetEntity);
84+
}
85+
Log::debug
86+
(
87+
sprintf
88+
(
89+
"AuditLogFormatterFactory::make subject is a PersistentCollection isInitialized %b ? ( final )",
90+
$subject->isInitialized()
91+
)
92+
);
93+
} elseif (is_array($subject)) {
94+
$child_entity = $subject[0] ?? null;
95+
$child_entity_formatter = $child_entity ? ChildEntityFormatterFactory::build($child_entity) : null;
96+
} elseif (is_object($subject) && method_exists($subject, 'getSnapshot')) {
97+
$snap = $subject->getSnapshot(); // only once
98+
$child_entity = $snap[0] ?? null;
99+
$child_entity_formatter = $child_entity ? ChildEntityFormatterFactory::build($child_entity) : null;
43100
}
44-
$child_entity_formatter = $child_entity != null ? ChildEntityFormatterFactory::build($child_entity) : null;
101+
45102
$formatter = new EntityCollectionUpdateAuditLogFormatter($child_entity_formatter);
46103
break;
47104
case IAuditStrategy::EVENT_ENTITY_CREATION:
@@ -65,6 +122,7 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
65122
}
66123
break;
67124
}
125+
if ($formatter === null) return null;
68126
$formatter->setContext($ctx);
69127
return $formatter;
70128
}
@@ -73,7 +131,7 @@ private function getFormatterByContext(object $subject, string $event_type, Audi
73131
{
74132
$class = get_class($subject);
75133
$entity_config = $this->config['entities'][$class] ?? null;
76-
134+
77135
if (!$entity_config) {
78136
return null;
79137
}

app/Audit/ConcreteFormatters/ChildEntityFormatters/ChildEntityFormatterFactory.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,25 @@
1616
**/
1717

1818
use Illuminate\Support\Facades\Log;
19-
use ReflectionClass;
2019

2120
/**
2221
* Class ChildEntityFormatterFactory
2322
* @package App\Audit\ConcreteFormatters\ChildEntityFormatter
2423
*/
2524
class ChildEntityFormatterFactory {
2625

27-
public static function build($entity): ?IChildEntityAuditLogFormatter {
26+
public static function build(object|string $entity): ?IChildEntityAuditLogFormatter {
2827
try {
29-
$class_name = (new ReflectionClass($entity))->getShortName();
30-
$class_name = "App\\Audit\\ConcreteFormatters\\ChildEntityFormatters\\{$class_name}AuditLogFormatter";
31-
if(class_exists($class_name)) {
32-
return new $class_name();
33-
}
34-
return null;
35-
} catch (\ReflectionException $e) {
28+
$short = is_string($entity)
29+
? substr(ltrim($entity, '\\'), strrpos(ltrim($entity, '\\'), '\\') + 1)
30+
: (new \ReflectionClass($entity))->getShortName();
31+
Log::debug("ChildEntityFormatterFactory::build short {$short}");
32+
$class = "App\\Audit\\ConcreteFormatters\\ChildEntityFormatters\\{$short}AuditLogFormatter";
33+
return class_exists($class) ? new $class() : null;
34+
35+
} catch (\Throwable $e) {
3636
Log::error($e);
3737
return null;
3838
}
3939
}
40-
}
40+
}

0 commit comments

Comments
 (0)