Skip to content

Commit 728ae67

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 728ae67

File tree

2 files changed

+76
-19
lines changed

2 files changed

+76
-19
lines changed

app/Audit/AuditLogFormatterFactory.php

Lines changed: 66 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,68 @@ 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+
$type = $subject->getTypeClass();
53+
// Your log shows this is ClassMetadata
54+
if ($type instanceof ClassMetadata) {
55+
// Doctrine supports either getName() or public $name
56+
$targetEntity = method_exists($type, 'getName') ? $type->getName() : ($type->name ?? null);
57+
} elseif (is_string($type)) {
58+
$targetEntity = $type;
59+
}
60+
Log::debug("AuditLogFormatterFactory::make getTypeClass targetEntity {$targetEntity}");
61+
}
62+
elseif (method_exists($subject, 'getMapping')) {
63+
$mapping = $subject->getMapping();
64+
$targetEntity = $mapping['targetEntity'] ?? null;
65+
Log::debug("AuditLogFormatterFactory::make getMapping targetEntity {$targetEntity}");
66+
} else {
67+
// last-resort: read private association metadata (still no hydration)
68+
$ref = new \ReflectionObject($subject);
69+
foreach (['association', 'mapping', 'associationMapping'] as $propName) {
70+
if ($ref->hasProperty($propName)) {
71+
$prop = $ref->getProperty($propName);
72+
$prop->setAccessible(true);
73+
$mapping = $prop->getValue($subject);
74+
$targetEntity = $mapping['targetEntity'] ?? null;
75+
if ($targetEntity) break;
76+
}
77+
}
78+
}
79+
80+
if ($targetEntity) {
81+
// IMPORTANT: build formatter WITHOUT touching collection items
82+
$child_entity_formatter = ChildEntityFormatterFactory::build($targetEntity);
83+
}
84+
Log::debug
85+
(
86+
sprintf
87+
(
88+
"AuditLogFormatterFactory::make subject is a PersistentCollection isInitialized %b ? ( final )",
89+
$subject->isInitialized()
90+
)
91+
);
92+
} elseif (is_array($subject)) {
93+
$child_entity = $subject[0] ?? null;
94+
$child_entity_formatter = $child_entity ? ChildEntityFormatterFactory::build($child_entity) : null;
95+
} elseif (is_object($subject) && method_exists($subject, 'getSnapshot')) {
96+
$snap = $subject->getSnapshot(); // only once
97+
$child_entity = $snap[0] ?? null;
98+
$child_entity_formatter = $child_entity ? ChildEntityFormatterFactory::build($child_entity) : null;
4399
}
44-
$child_entity_formatter = $child_entity != null ? ChildEntityFormatterFactory::build($child_entity) : null;
100+
45101
$formatter = new EntityCollectionUpdateAuditLogFormatter($child_entity_formatter);
46102
break;
47103
case IAuditStrategy::EVENT_ENTITY_CREATION:
@@ -65,6 +121,7 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
65121
}
66122
break;
67123
}
124+
if ($formatter === null) return null;
68125
$formatter->setContext($ctx);
69126
return $formatter;
70127
}
@@ -73,7 +130,7 @@ private function getFormatterByContext(object $subject, string $event_type, Audi
73130
{
74131
$class = get_class($subject);
75132
$entity_config = $this->config['entities'][$class] ?? null;
76-
133+
77134
if (!$entity_config) {
78135
return null;
79136
}

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)