src/EventSubscriber/AuditSubscriber.php line 334

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use App\Entity\LogsAction;
  4. use App\Entity\User;
  5. use App\Repository\LogsActionRepository;
  6. use DateTime;
  7. use DateTimeInterface;
  8. use Doctrine\Common\Collections\Collection;
  9. use Doctrine\ORM\Event\LifecycleEventArgs;
  10. use Doctrine\Persistence\ManagerRegistry;
  11. use Doctrine\Persistence\ObjectManager;
  12. use Error;
  13. use Exception;
  14. use ReflectionMethod;
  15. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  16. use Symfony\Component\HttpFoundation\File\UploadedFile;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  19. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  20. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  21. use Symfony\Component\HttpKernel\KernelEvents;
  22. use Symfony\Component\Security\Core\Security;
  23. class AuditSubscriber implements EventSubscriberInterface
  24. {
  25.     private $requestData = [];
  26.     private $entityChanges = [];
  27.     private $shouldSkipAudit false;
  28.     private $errorData null;
  29.     private $preDeleteEntityData null;
  30.     // Routes to ignore from audit logging
  31.     private const IGNORED_ROUTES = [
  32.         // Clean route patterns
  33.         '/api/discussion/list',
  34.         '/api/login_check',
  35.         // Original Symfony route patterns  
  36.         '_api_/discussion/list',
  37.         'api/login_check',
  38.         '_api_/discussion/list.{_format}_get',
  39.         'api/login_check_post',
  40.     ];
  41.     public function __construct(
  42.         private readonly \Doctrine\ORM\EntityManagerInterface $entityManager,
  43.         private readonly ManagerRegistry $doctrine,
  44.         private Security $security,
  45.         private readonly LogsActionRepository $logsActionRepository
  46.     ) {
  47.     }
  48.     public static function getSubscribedEvents() :array
  49.     {
  50.         return [
  51.             // HTTP Kernel Events
  52.             KernelEvents::CONTROLLER => 'onKernelController',
  53.             KernelEvents::RESPONSE => 'onKernelResponse',
  54.             KernelEvents::EXCEPTION => 'onKernelException',
  55.         ];
  56.     }
  57.     // Spammed routes like /api/login_check or /api/discussion/list
  58.     private function shouldIgnoreRoute(string $originalRoute nullstring $cleanRoute null): bool
  59.     {
  60.         if (!$originalRoute && !$cleanRoute) {
  61.             return false;
  62.         }
  63.         if ($originalRoute && in_array($originalRouteself::IGNORED_ROUTEStrue)) {
  64.             return true;
  65.         }
  66.         if ($cleanRoute && in_array($cleanRouteself::IGNORED_ROUTEStrue)) {
  67.             return true;
  68.         }
  69.         foreach (self::IGNORED_ROUTES as $ignoredRoute) {
  70.             if ($originalRoute && str_starts_with($originalRoute$ignoredRoute)) {
  71.                 return true;
  72.             }
  73.             if ($originalRoute && $this->matchesRoutePattern($originalRoute$ignoredRoute)) {
  74.                 return true;
  75.             }
  76.         }
  77.         return false;
  78.     }
  79.     private function matchesRoutePattern(string $routestring $pattern): bool
  80.     {
  81.         // Remove format and method suffixes for comparison
  82.         $cleanedRoute preg_replace('/\.\{_format\}(_\w+)?$/'''$route);
  83.         $cleanedRoute preg_replace('/_(?:get|post|put|patch|delete)$/'''$cleanedRoute);
  84.         
  85.         $cleanedPattern preg_replace('/\.\{_format\}(_\w+)?$/'''$pattern);
  86.         $cleanedPattern preg_replace('/_(?:get|post|put|patch|delete)$/'''$cleanedPattern);
  87.         
  88.         return $cleanedRoute === $cleanedPattern;
  89.     }
  90.     private function extractEntityData($entity): array
  91.     {
  92.         try {
  93.             $data = [];
  94.             $reflection = new \ReflectionClass($entity);
  95.             foreach ($reflection->getProperties() as $property) {
  96.                 try {
  97.                     $value $property->getValue($entity);
  98.                 } catch (Error $e) {
  99.                     // Handle uninitialized typed properties (like ID in prePersist)
  100.                     if (str_contains($e->getMessage(), 'must not be accessed before initialization')) {
  101.                         continue;
  102.                     }
  103.                     continue;
  104.                 }
  105.                 if ($value instanceof DateTimeInterface) {
  106.                     $data[$property->getName()] = $value->format('Y-m-d H:i:s');
  107.                 } elseif (is_object($value) && method_exists($value'getId')) {
  108.                     // For relations just store the ID
  109.                     try {
  110.                         $data[$property->getName() . '_id'] = $value->getId();
  111.                     } catch (Error $e) {
  112.                         if (str_contains($e->getMessage(), 'must not be accessed before initialization')) {
  113.                             $data[$property->getName() . '_id'] = 'uninitialized';
  114.                         } else {
  115.                             $data[$property->getName() . '_id'] = 'error';
  116.                         }
  117.                     }
  118.                 } elseif (is_array($value) || $value instanceof Collection) {
  119.                     // For collections, store count or IDs
  120.                     try {
  121.                         if ($value instanceof Collection) {
  122.                             // Check if the collection is initialized to avoid lazy loading issues
  123.                             if ($value instanceof \Doctrine\ORM\PersistentCollection && !$value->isInitialized()) {
  124.                                 $data[$property->getName() . '_count'] = 'uninitialized_collection';
  125.                             } else {
  126.                                 $data[$property->getName() . '_count'] = $value->count();
  127.                             }
  128.                         } else {
  129.                             $data[$property->getName() . '_count'] = count($value);
  130.                         }
  131.                     } catch (Exception $e) {
  132.                         // If counting fails, just note that there's a collection
  133.                         $data[$property->getName() . '_count'] = 'collection_error';
  134.                     }
  135.                 } elseif (is_scalar($value) || is_null($value)) {
  136.                     $data[$property->getName()] = $value;
  137.                 } else {
  138.                     // For other objects, try to get a string representation
  139.                     try {
  140.                         $data[$property->getName()] = (string)$value;
  141.                     } catch (Exception $e) {
  142.                         $data[$property->getName()] = 'object_conversion_error';
  143.                     }
  144.                 }
  145.             }
  146.             return $data;
  147.         } catch (Exception $e) {
  148.             // Return basic entity info if extraction fails completely
  149.             return [
  150.                 'entity_class' => get_class($entity),
  151.                 'extraction_error' => $e->getMessage()
  152.             ];
  153.         }
  154.     }
  155.     // This function is useless right now but in case we need to specify which HTTP actions to log later
  156.     private function shouldLogHttpAction(string $methodint $statusCode): bool
  157.     {
  158.         // Log all successful requests for certain methods
  159.         if ($statusCode >= 200 && $statusCode 300) {
  160.             return in_array($method, ['GET''DELETE''POST''PUT''PATCH']);
  161.         }
  162.         // Log all error responses
  163.         return $statusCode >= 400;
  164.     }
  165.     private function getHttpActionType(string $methodstring $route null): string
  166.     {
  167.         if ($route && $this->isDeleteRoute($route)) {
  168.             return 'delete';
  169.         }
  170.         
  171.         return match ($method) {
  172.             'GET' => 'read',
  173.             'POST' => 'create',
  174.             'PUT''PATCH' => 'update'
  175.             'DELETE' => 'delete',
  176.             default => 'request'
  177.         };
  178.     }
  179.     // Some Delete routes are using POST method so u need to handle them separately
  180.     private function isDeleteRoute(string $route): bool
  181.     {
  182.         $deletePatterns = [
  183.             '/delete',
  184.             'delete_',
  185.             '_delete',
  186.             '/remove',
  187.             'remove_',
  188.             '_remove',
  189.             '/destroy',
  190.             'destroy_',
  191.             '_destroy'
  192.         ];
  193.         
  194.         $lowerRoute strtolower($route);
  195.         
  196.         foreach ($deletePatterns as $pattern) {
  197.             if (str_contains($lowerRoute$pattern)) {
  198.                 return true;
  199.             }
  200.         }
  201.         
  202.         return false;
  203.     }
  204.     private function captureEntityBeforeDelete(Request $requeststring $route): void
  205.     {
  206.         try {
  207.             $entityId $this->extractEntityIdFromRequest($request);
  208.             if (!$entityId) {
  209.                 return;
  210.             }
  211.             $entityClass $this->getEntityFromRoute($route);
  212.             if (!$entityClass) {
  213.                 return;
  214.             }
  215.             $repository $this->entityManager->getRepository($entityClass);
  216.             $entity $repository->find($entityId);
  217.             
  218.             if ($entity) {
  219.                 $this->preDeleteEntityData $this->extractEntityData($entity);
  220.             }
  221.         } catch (Exception $e) {
  222.         }
  223.     }
  224.     private function extractEntityIdFromRequest(Request $request): ?int
  225.     {
  226.         $idParams = ['id''entityId''siteId''userId''customerId'];
  227.         
  228.         foreach ($idParams as $param) {
  229.             $value $request->get($param);
  230.             if ($value && is_numeric($value)) {
  231.                 return (int) $value;
  232.             }
  233.         }
  234.         $pathInfo $request->getPathInfo();
  235.         if (preg_match('/\/(\d+)(?:\/|$)/'$pathInfo$matches)) {
  236.             $id = (int) $matches[1];
  237.             return $id;
  238.         }
  239.         return null;
  240.     }
  241.     private function getEntityFromRoute(string $route): ?string
  242.     {
  243.         $routeToEntityMap = [
  244.             'sites' => 'App\Entity\Site',
  245.             'users' => 'App\Entity\User'
  246.             'customers' => 'App\Entity\Customer',
  247.             'evaluations' => 'App\Entity\Evaluation',
  248.             'documents' => 'App\Entity\Document',
  249.             'categories' => 'App\Entity\Category',
  250.             'sections' => 'App\Entity\Section',
  251.             'compliances' => 'App\Entity\Compliance',
  252.             //
  253.             'site' => 'App\Entity\Site',
  254.             'user' => 'App\Entity\User',
  255.             'customer' => 'App\Entity\Customer'
  256.             'evaluation' => 'App\Entity\Evaluation',
  257.             'document' => 'App\Entity\Document',
  258.             'category' => 'App\Entity\Category',
  259.             'section' => 'App\Entity\Section',
  260.             'compliance' => 'App\Entity\Compliance',
  261.         ];
  262.         $lowerRoute strtolower($route);
  263.         
  264.         foreach ($routeToEntityMap as $routePattern => $entityClass) {
  265.             if (str_contains($lowerRoute$routePattern)) {
  266.                 return $entityClass;
  267.             }
  268.         }
  269.         return null;
  270.     }
  271.     private function getAuditEntityManager(): ObjectManager
  272.     {
  273.         try {
  274.             $em $this->doctrine->resetManager();
  275.             return $this->doctrine->getManager();
  276.         } catch (Exception $e) {
  277.             return $this->entityManager;
  278.         }
  279.     }
  280.     private function getCleanRoute($request): string
  281.     {
  282.         // Try to get the actual path first
  283.         $pathInfo $request->getPathInfo();
  284.         if ($pathInfo && $pathInfo !== '/') {
  285.             return $pathInfo;
  286.         }
  287.         
  288.         // Fallback to route name, but clean it up
  289.         $route $request->attributes->get('_route');
  290.         if (!$route) {
  291.             return 'unknown_route';
  292.         }
  293.         
  294.         // Clean up common Symfony route patterns
  295.         $cleanRoute $route;
  296.         
  297.         // Remove format placeholders
  298.         $cleanRoute preg_replace('/\.\{_format\}/'''$cleanRoute);
  299.         
  300.         // Remove method suffixes (like _post, _get, etc.)
  301.         $cleanRoute preg_replace('/_(?:get|post|put|patch|delete)$/'''$cleanRoute);
  302.         
  303.         // Convert underscores to forward slashes for API routes
  304.         if (str_starts_with($cleanRoute'_api_')) {
  305.             $cleanRoute str_replace('_api_''/api/'$cleanRoute);
  306.             $cleanRoute str_replace('_''/'$cleanRoute);
  307.         }
  308.         
  309.         return $cleanRoute;
  310.     }
  311.     public function onKernelController(ControllerEvent $event)
  312.     {
  313.         $request $event->getRequest();
  314.         $originalRoute $request->attributes->get('_route');
  315.         $cleanRoute $this->getCleanRoute($request);
  316.         if ($this->shouldIgnoreRoute($originalRoute$cleanRoute)) {
  317.             $this->shouldSkipAudit true;
  318.             return;
  319.         }
  320.         $this->shouldSkipAudit false;
  321.         $requestData = [];
  322.         $requestData['query'] = $request->query->all();
  323.         $requestData['request'] = $request->request->all();
  324.         $requestData['attributes'] = $request->attributes->all();
  325.         if ($request->getContentType() === 'json' || str_contains($request->headers->get('Content-Type'''), 'application/json')) {
  326.             $content $request->getContent();
  327.             if ($content) {
  328.                 $decodedContent json_decode($contenttrue);
  329.                 if (json_last_error() === JSON_ERROR_NONE) {
  330.                     $requestData['json_body'] = $decodedContent;
  331.                 } else {
  332.                     $requestData['raw_body'] = $content;
  333.                 }
  334.             }
  335.         }
  336.         if ($request->files->all()) {
  337.             $requestData['files'] = array_map(function($file) {
  338.                 return $file instanceof UploadedFile
  339.                     ? ['name' => $file->getClientOriginalName(), 'size' => $file->getSize(), 'type' => $file->getMimeType()]
  340.                     : (string)$file;
  341.             }, $request->files->all());
  342.         }
  343.         $this->requestData = [
  344.             'route' => $cleanRoute,
  345.             'method' => $request->getMethod(),
  346.             'ip_address' => $this->logsActionRepository->getRealIpAddress($request),
  347.             'user_agent' => $request->headers->get('User-Agent'),
  348.             'request_data' => json_encode($requestData),
  349.         ];
  350.         if ($this->isDeleteRoute($cleanRoute)) {
  351.             $this->captureEntityBeforeDelete($request$cleanRoute);
  352.         }
  353.     }
  354.      public function preUpdate(LifecycleEventArgs $args)
  355.      {
  356.          if ($this->shouldSkipAudit) {
  357.              return;
  358.          }
  359.          try {
  360.              $entity $args->getObject();
  361.              $entityClass get_class($entity);
  362.              $entityId 'unknown';
  363.              if (method_exists($entity'getId')) {
  364.                  try {
  365.                      $entityId $entity->getId() ?? 'null_id';
  366.                  } catch (\Error $e) {
  367.                      if (str_contains($e->getMessage(), 'must not be accessed before initialization')) {
  368.                          $entityId 'uninitialized_id';
  369.                      } else {
  370.                          $entityId 'error_getting_id';
  371.                      }
  372.                  }
  373.              }
  374.              $dataBefore = [];
  375.              try {
  376.                  $uow $this->entityManager->getUnitOfWork();
  377.                  $changeSet $uow->getEntityChangeSet($entity);
  378.                  foreach ($changeSet as $field => $values) {
  379.                      // $values[0] is the old value (before), $values[1] is the new value (after)
  380.                      // This will create 2 pairs: field => old_value, field => new_value
  381.                      $oldValue $values[0];
  382.                      
  383.                      if ($oldValue instanceof DateTimeInterface) {
  384.                          $dataBefore[$field] = $oldValue->format('Y-m-d H:i:s');
  385.                      } elseif (is_object($oldValue) && method_exists($oldValue'getId')) {
  386.                          try {
  387.                              $dataBefore[$field '_id'] = $oldValue->getId();
  388.                          } catch (\Error $e) {
  389.                              $dataBefore[$field '_id'] = 'uninitialized';
  390.                          }
  391.                      } elseif (is_scalar($oldValue) || is_null($oldValue)) {
  392.                          $dataBefore[$field] = $oldValue;
  393.                      } else {
  394.                          try {
  395.                              $dataBefore[$field] = (string)$oldValue;
  396.                          } catch (Exception $e) {
  397.                              $dataBefore[$field] = 'object_conversion_error';
  398.                          }
  399.                      }
  400.                  }
  401.                  if (empty($dataBefore)) {
  402.                      $originalData $uow->getOriginalEntityData($entity);
  403.                      foreach ($originalData as $field => $value) {
  404.                          if ($value instanceof DateTimeInterface) {
  405.                              $dataBefore[$field] = $value->format('Y-m-d H:i:s');
  406.                          } elseif ($value instanceof \Doctrine\ORM\PersistentCollection) {
  407.                              if (!$value->isInitialized()) {
  408.                                  $dataBefore[$field] = 'uninitialized_collection';
  409.                              } else {
  410.                                  $dataBefore[$field] = 'collection_count_' $value->count();
  411.                              }
  412.                          } elseif ($value instanceof \Doctrine\Common\Collections\Collection) {
  413.                              $dataBefore[$field] = 'collection_count_' $value->count();
  414.                          } elseif (is_array($value)) {
  415.                              $dataBefore[$field] = 'array_count_' count($value);
  416.                          } elseif (is_object($value) && method_exists($value'getId')) {
  417.                              try {
  418.                                  $dataBefore[$field '_id'] = $value->getId();
  419.                              } catch (Error $e) {
  420.                                  $dataBefore[$field '_id'] = 'uninitialized';
  421.                              }
  422.                          } elseif (is_object($value)) {
  423.                              if (method_exists($value'__toString')) {
  424.                                  try {
  425.                                      $dataBefore[$field] = (string)$value;
  426.                                  } catch (\Exception $e) {
  427.                                      $dataBefore[$field] = get_class($value) . '_toString_error';
  428.                                  }
  429.                              } else {
  430.                                  $dataBefore[$field] = get_class($value);
  431.                              }
  432.                          } elseif (is_scalar($value) || is_null($value)) {
  433.                              $dataBefore[$field] = $value;
  434.                          } else {
  435.                              $dataBefore[$field] = 'unknown_type_' gettype($value);
  436.                          }
  437.                      }
  438.                  }
  439.                  
  440.              } catch (Exception $e) {
  441.                  // If all fails, at least note the error
  442.                  $dataBefore['error'] = 'Could not retrieve change set: ' $e->getMessage();
  443.              }
  444.              $this->entityChanges[] = [
  445.                  'entity' => $entityClass,
  446.                  'entity_id' => $entityId,
  447.                  'data_before' => $dataBefore,
  448.                  'data_after' => $this->extractEntityData($entity),
  449.                  'action' => 'update',
  450.              ];
  451.          } catch (Exception $e) {
  452.              // Silently handle any errors to prevent disrupting the update process
  453.          }
  454.      }
  455.     public function prePersist(LifecycleEventArgs $args)
  456.     {
  457.         if ($this->shouldSkipAudit) {
  458.             return;
  459.         }
  460.         try {
  461.             $entity $args->getObject();
  462.             $entityClass get_class($entity);
  463.             $entityId 'new';
  464.             if (method_exists($entity'getId')) {
  465.                 try {
  466.                     $id $entity->getId();
  467.                     $entityId $id ?? 'new';
  468.                 } catch (Error $e) {
  469.                     // Handle uninitialized typed property
  470.                     if (str_contains($e->getMessage(), 'must not be accessed before initialization')) {
  471.                         $entityId 'new';
  472.                     } else {
  473.                         $entityId 'new_error';
  474.                     }
  475.                 }
  476.             }
  477.             $this->entityChanges[] = [
  478.                 'entity' => $entityClass,
  479.                 'entity_id' => $entityId,
  480.                 'data_before' => [],
  481.                 'data_after' => $this->extractEntityData($entity),
  482.                 'action' => 'create',
  483.             ];
  484.         } catch (Exception $e) {
  485.         }
  486.     }
  487.     // Capture entity deletions
  488.     public function preRemove(LifecycleEventArgs $args)
  489.     {
  490.         if ($this->shouldSkipAudit) {
  491.             return;
  492.         }
  493.         try {
  494.             $entity $args->getObject();
  495.             $entityClass get_class($entity);
  496.             // Safely get entity ID
  497.             $entityId 'unknown';
  498.             if (method_exists($entity'getId')) {
  499.                 try {
  500.                     $entityId $entity->getId() ?? 'null_id';
  501.                 } catch (Error $e) {
  502.                     if (str_contains($e->getMessage(), 'must not be accessed before initialization')) {
  503.                         $entityId 'uninitialized_id';
  504.                     } else {
  505.                         $entityId 'error_getting_id';
  506.                     }
  507.                 }
  508.             }
  509.             $entityData $this->extractEntityData($entity);
  510.             $this->entityChanges[] = [
  511.                 'entity' => $entityClass,
  512.                 'entity_id' => $entityId,
  513.                 'data_before' => $entityData,
  514.                 'data_after' => [], // Empty after deletion
  515.                 'action' => 'delete',
  516.                 'doctrine_event' => 'preRemove',
  517.             ];
  518.         } catch (Exception $e) {
  519.         }
  520.     }
  521.     public function onKernelException(ExceptionEvent $event): void
  522.     {
  523.         if ($this->shouldSkipAudit) {
  524.             return;
  525.         }
  526.         $exception $event->getThrowable();
  527.         $this->errorData = [
  528.             'error_message' => $exception->getMessage(),
  529.             'error_code' => $exception->getCode(),
  530.             'error_file' => $exception->getFile(),
  531.             'error_line' => $exception->getLine(),
  532.             'error_trace' => $exception->getTraceAsString()
  533.         ];
  534.     }
  535.     public function onKernelResponse(ResponseEvent $event): void
  536.     {
  537.         if ($this->shouldSkipAudit) {
  538.             return;
  539.         }
  540.         $response $event->getResponse() ?? null;
  541.         $user $this->security->getUser() ?? 0;
  542.         $statusCode $response->getStatusCode();
  543.         $method $this->requestData['method'] ?? 'UNKNOWN';
  544.         $responseData = [];
  545.         // Clean Response Data
  546.         if ($response) {
  547.             $responseData['status_code'] = $statusCode;
  548.             $responseData['headers'] = [];
  549.             $importantHeaders = ['content-type''location''cache-control''content-length'];
  550.             foreach ($importantHeaders as $header) {
  551.                 if ($response->headers->has($header)) {
  552.                     $responseData['headers'][$header] = $response->headers->get($header);
  553.                 }
  554.             }
  555.             if ($statusCode >= 200 && $statusCode 300) {
  556.                 $content $response->getContent();
  557.                 if (!empty($content)) {
  558.                     // Limit response content size
  559.                     if (strlen($content) > 5000) {
  560.                         $responseData['content'] = substr($content05000) . '... [truncated]';
  561.                         $responseData['content_truncated'] = true;
  562.                         $responseData['original_content_length'] = strlen($content);
  563.                     } else {
  564.                         $decodedContent json_decode($contenttrue);
  565.                         if (json_last_error() === JSON_ERROR_NONE) {
  566.                             $responseData['content'] = $decodedContent;
  567.                         } else {
  568.                             $responseData['content'] = $content;
  569.                         }
  570.                     }
  571.                 }
  572.             }
  573.         }
  574.         if ($statusCode >= 400 && empty($this->entityChanges)) {
  575.             $errorData $this->errorData ?? ['status_code' => $statusCode];
  576.             $errorData array_merge($errorData$responseData);
  577.             
  578.             $this->entityChanges[] = [
  579.                 'entity' => 'HTTP_ERROR',
  580.                 'entity_id' => 'error',
  581.                 'data_before' => [],
  582.                 'data_after' => $errorData,
  583.                 'action' => 'error',
  584.                 'response_data' => $responseData,
  585.             ];
  586.         }
  587.         if (empty($this->entityChanges) && $this->shouldLogHttpAction($method$statusCode)) {
  588.             $route $this->requestData['route'] ?? null;
  589.             $action $this->getHttpActionType($method$route);
  590.             $dataBefore = [];
  591.             $dataAfter = [];
  592.             if ($action === 'delete' && $this->preDeleteEntityData !== null) {
  593.                 $dataBefore $this->preDeleteEntityData;
  594.             }
  595.             $this->entityChanges[] = [
  596.                 'entity' => 'HTTP_REQUEST',
  597.                 'entity_id' => $route ?? 'unknown_route',
  598.                 'data_before' => $dataBefore,
  599.                 'data_after' => $dataAfter// Keep empty for HTTP requests - data goes in request_data
  600.                 'action' => $action,
  601.                 'response_data' => $responseData,
  602.                 'http_request_data' => [ // This will be stored in request_data column
  603.                     'method' => $method,
  604.                     'route' => $route,
  605.                     'original_route' => $this->requestData['original_route'] ?? null,
  606.                     'status_code' => $statusCode,
  607.                     'user_agent' => $this->requestData['user_agent'] ?? null,
  608.                     'ip_address' => $this->requestData['ip_address'] ?? null,
  609.                     'detected_as_delete' => $action === 'delete' && $method !== 'DELETE',
  610.                     'has_pre_delete_data' => $this->preDeleteEntityData !== null,
  611.                 ],
  612.             ];
  613.         }
  614.         if (empty($this->entityChanges)) {
  615.             return;
  616.         }
  617.         foreach ($this->entityChanges as &$change) {
  618.             if (!isset($change['response_data'])) {
  619.                 $change['response_data'] = $responseData;
  620.             }
  621.         }
  622.         unset($change);
  623.         // Use a separate EntityManager for audit operations to avoid transaction conflicts
  624.         $auditEM $this->getAuditEntityManager();
  625.         if (!$auditEM) {
  626.             return;
  627.         }
  628.         foreach ($this->entityChanges as $change) {
  629.             try {
  630.                 $log = new LogsAction();
  631.                 if ($user) {
  632.                     $auditUser null;
  633.                     $userId null;
  634.                     if ($user instanceof User) {
  635.                         $userId $user->getId();
  636.                         $auditUser $auditEM->find(User::class, $userId);
  637.                     } elseif (method_exists($user'getId')) {
  638.                         try {
  639.                             $reflection = new ReflectionMethod($user'getId');
  640.                             $userId $reflection->invoke($user);
  641.                             if ($userId) {
  642.                                 $auditUser $auditEM->find(User::class, $userId);
  643.                             }
  644.                         } catch (Exception $e) {
  645.                             // Ignore reflection errors
  646.                         }
  647.                     } 
  648.                     if (!$auditUser && method_exists($user'getUserIdentifier')) {
  649.                         $userRepo $auditEM->getRepository(User::class);
  650.                         $auditUser $userRepo->findOneBy(['username' => $user->getUserIdentifier()]);
  651.                     }
  652.                     if ($auditUser) {
  653.                         $log->setUser($auditUser);
  654.                     } else {
  655.                         continue;
  656.                     }
  657.                 } else {
  658.                     $defaultUser $auditEM->find(User::class, 0);
  659.                     if (!$defaultUser) {
  660.                         $userRepo $auditEM->getRepository(User::class);
  661.                         $defaultUser $userRepo->findOneBy(['username' => 'system']);
  662.                     }
  663.                     if ($defaultUser) {
  664.                         $log->setUser($defaultUser);
  665.                     } else {
  666.                         continue;
  667.                     }
  668.                 }
  669.                 $log->setAction($change['action']);
  670.                 $log->setRoute($this->requestData['route'] ?? null);
  671.                 $log->setMethod($this->requestData['method'] ?? null);
  672.                 $log->setIpAddress($this->requestData['ip_address'] ?? null);
  673.                 $log->setUserAgent($this->requestData['user_agent'] ?? null);
  674.                 if ($change['entity'] === 'HTTP_REQUEST' && isset($change['http_request_data'])) {
  675.                     $log->setRequestData(json_encode($change['http_request_data']));
  676.                 } else {
  677.                     $log->setRequestData($this->requestData['request_data'] ?? null);
  678.                 }
  679.                 $log->setDataBefore(json_encode($change['data_before']));
  680.                 $log->setDataAfter(json_encode($change['data_after']));
  681.                 $log->setStatusCode($statusCode);
  682.                 if (isset($change['response_data']) && !empty($change['response_data'])) {
  683.                     $log->setResponseData(json_encode($change['response_data']));
  684.                 } elseif ($this->errorData && $statusCode >= 400) {
  685.                     $errorResponseData array_merge($responseData, (array)$this->errorData);
  686.                     $log->setResponseData(json_encode($errorResponseData));
  687.                 } elseif (!empty($responseData)) {
  688.                     $log->setResponseData(json_encode($responseData));
  689.                 }
  690.                 $log->setCreatedAt(new DateTime());
  691.                 $log->setUpdatedAt(new DateTime());
  692.                 $auditEM->persist($log);
  693.                 
  694.             } catch (Exception $e) {
  695.                 continue;
  696.             }
  697.         }
  698.         try {
  699.             $auditEM->flush();
  700.         } catch (Exception $e) {
  701.         } finally {
  702.             $this->entityChanges = [];
  703.             $this->errorData null;
  704.             $this->requestData = [];
  705.             $this->preDeleteEntityData null;
  706.         }
  707.     }
  708. }