diff --git a/app/Audit/AbstractAuditLogFormatter.php b/app/Audit/AbstractAuditLogFormatter.php new file mode 100644 index 00000000..74a175ef --- /dev/null +++ b/app/Audit/AbstractAuditLogFormatter.php @@ -0,0 +1,45 @@ +ctx = $ctx; + } + + protected function getUserInfo(): string + { + if (!$this->ctx) { + return 'Unknown (unknown)'; + } + + $user_name = 'Unknown'; + if ($this->ctx->userFirstName || $this->ctx->userLastName) { + $user_name = trim(sprintf("%s %s", $this->ctx->userFirstName ?? '', $this->ctx->userLastName ?? '')) ?: 'Unknown'; + } elseif ($this->ctx->userEmail) { + $user_name = $this->ctx->userEmail; + } + + $user_id = $this->ctx->userId ?? 'unknown'; + return sprintf("%s (%s)", $user_name, $user_id); + } + + abstract public function format($subject, array $change_set): ?string; +} \ No newline at end of file diff --git a/app/Audit/AuditContext.php b/app/Audit/AuditContext.php new file mode 100644 index 00000000..ee9fad9b --- /dev/null +++ b/app/Audit/AuditContext.php @@ -0,0 +1,31 @@ +environment('testing')) { + return; + } + $em = $eventArgs->getObjectManager(); + $uow = $em->getUnitOfWork(); + // Strategy selection based on environment configuration + $strategy = $this->getAuditStrategy($em); + if (!$strategy) { + return; // No audit strategy enabled + } + + $ctx = $this->buildAuditContext(); + + try { + foreach ($uow->getScheduledEntityInsertions() as $entity) { + $strategy->audit($entity, [], IAuditStrategy::EVENT_ENTITY_CREATION, $ctx); + } + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + $strategy->audit($entity, $uow->getEntityChangeSet($entity), IAuditStrategy::EVENT_ENTITY_UPDATE, $ctx); + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + $strategy->audit($entity, [], IAuditStrategy::EVENT_ENTITY_DELETION, $ctx); + } + + foreach ($uow->getScheduledCollectionUpdates() as $col) { + $strategy->audit($col, [], IAuditStrategy::EVENT_COLLECTION_UPDATE, $ctx); + } + } catch (\Exception $e) { + Log::error('Audit event listener failed', [ + 'error' => $e->getMessage(), + 'strategy_class' => get_class($strategy), + 'trace' => $e->getTraceAsString(), + ]); + } + } + + /** + * Get the appropriate audit strategy based on environment configuration + */ + private function getAuditStrategy($em): ?IAuditStrategy + { + // Check if OTLP audit is enabled + if (config('opentelemetry.enabled', false)) { + try { + Log::debug("AuditEventListener::getAuditStrategy strategy AuditLogOtlpStrategy"); + return App::make(AuditLogOtlpStrategy::class); + } catch (\Exception $e) { + Log::warning('Failed to create OTLP audit strategy, falling back to database', [ + 'error' => $e->getMessage() + ]); + } + } + + // Use database strategy (either as default or fallback) + Log::debug("AuditEventListener::getAuditStrategy strategy AuditLogStrategy"); + return new AuditLogStrategy($em); + } + + private function buildAuditContext(): AuditContext + { + $resourceCtx = app(\models\oauth2\IResourceServerContext::class); + $userExternalId = $resourceCtx->getCurrentUserId(); + $member = null; + if ($userExternalId) { + $memberRepo = app(\models\main\IMemberRepository::class); + $member = $memberRepo->findOneBy(["user_external_id" => $userExternalId]); + } + + //$ui = app()->bound('ui.context') ? app('ui.context') : []; + + $req = request(); + $rawRoute = null; + // does not resolve the route when app is running in console mode + if ($req instanceof Request && !app()->runningInConsole()) { + try { + $route = Route::getRoutes()->match($req); + $method = $route->methods[0] ?? 'UNKNOWN'; + $rawRoute = $method . self::ROUTE_METHOD_SEPARATOR . $route->uri; + } catch (\Exception $e) { + Log::warning($e); + } + } + + return new AuditContext( + userId: $member?->getId(), + userEmail: $member?->getEmail(), + userFirstName: $member?->getFirstName(), + userLastName: $member?->getLastName(), + uiApp: $ui['app'] ?? null, + uiFlow: $ui['flow'] ?? null, + route: $req?->path(), + httpMethod: $req?->method(), + clientIp: $req?->ip(), + userAgent: $req?->userAgent(), + rawRoute: $rawRoute + ); + } +} \ No newline at end of file diff --git a/app/Audit/AuditLogFormatterFactory.php b/app/Audit/AuditLogFormatterFactory.php new file mode 100644 index 00000000..f94467bf --- /dev/null +++ b/app/Audit/AuditLogFormatterFactory.php @@ -0,0 +1,170 @@ +config = config('audit_log', []); + } + + public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatter + { + $formatter = null; + switch ($eventType) { + case IAuditStrategy::EVENT_COLLECTION_UPDATE: + $child_entity_formatter = null; + + if ($subject instanceof PersistentCollection) { + $targetEntity = null; + Log::debug + ( + sprintf + ( + "AuditLogFormatterFactory::make subject is a PersistentCollection isInitialized %b ?", + $subject->isInitialized() + ) + ); + if (method_exists($subject, 'getTypeClass')) { + $type = $subject->getTypeClass(); + // Your log shows this is ClassMetadata + if ($type instanceof ClassMetadata) { + // Doctrine supports either getName() or public $name + $targetEntity = method_exists($type, 'getName') ? $type->getName() : ($type->name ?? null); + } elseif (is_string($type)) { + $targetEntity = $type; + } + Log::debug("AuditLogFormatterFactory::make getTypeClass targetEntity {$targetEntity}"); + } + elseif (method_exists($subject, 'getMapping')) { + $mapping = $subject->getMapping(); + $targetEntity = $mapping['targetEntity'] ?? null; + Log::debug("AuditLogFormatterFactory::make getMapping targetEntity {$targetEntity}"); + } else { + // last-resort: read private association metadata (still no hydration) + $ref = new \ReflectionObject($subject); + foreach (['association', 'mapping', 'associationMapping'] as $propName) { + if ($ref->hasProperty($propName)) { + $prop = $ref->getProperty($propName); + $prop->setAccessible(true); + $mapping = $prop->getValue($subject); + $targetEntity = $mapping['targetEntity'] ?? null; + if ($targetEntity) break; + } + } + } + + if ($targetEntity) { + // IMPORTANT: build formatter WITHOUT touching collection items + $child_entity_formatter = ChildEntityFormatterFactory::build($targetEntity); + } + Log::debug + ( + sprintf + ( + "AuditLogFormatterFactory::make subject is a PersistentCollection isInitialized %b ? ( final )", + $subject->isInitialized() + ) + ); + } elseif (is_array($subject)) { + $child_entity = $subject[0] ?? null; + $child_entity_formatter = $child_entity ? ChildEntityFormatterFactory::build($child_entity) : null; + } elseif (is_object($subject) && method_exists($subject, 'getSnapshot')) { + $snap = $subject->getSnapshot(); // only once + $child_entity = $snap[0] ?? null; + $child_entity_formatter = $child_entity ? ChildEntityFormatterFactory::build($child_entity) : null; + } + + $formatter = new EntityCollectionUpdateAuditLogFormatter($child_entity_formatter); + break; + case IAuditStrategy::EVENT_ENTITY_CREATION: + $formatter = $this->getFormatterByContext($subject, $eventType, $ctx); + if(is_null($formatter)) { + $formatter = new EntityCreationAuditLogFormatter(); + } + break; + case IAuditStrategy::EVENT_ENTITY_DELETION: + $formatter = $this->getFormatterByContext($subject, $eventType, $ctx); + if(is_null($formatter)) { + $child_entity_formatter = ChildEntityFormatterFactory::build($subject); + $formatter = new EntityDeletionAuditLogFormatter($child_entity_formatter); + } + break; + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $formatter = $this->getFormatterByContext($subject, $eventType, $ctx); + if(is_null($formatter)) { + $child_entity_formatter = ChildEntityFormatterFactory::build($subject); + $formatter = new EntityUpdateAuditLogFormatter($child_entity_formatter); + } + break; + } + if ($formatter === null) return null; + $formatter->setContext($ctx); + return $formatter; + } + + private function getFormatterByContext(object $subject, string $event_type, AuditContext $ctx): ?IAuditLogFormatter + { + $class = get_class($subject); + $entity_config = $this->config['entities'][$class] ?? null; + + if (!$entity_config) { + return null; + } + + if (isset($entity_config['strategies'])) { + foreach ($entity_config['strategies'] as $strategy) { + if (!$this->matchesStrategy($strategy, $ctx)) { + continue; + } + + $formatter_class = $strategy['formatter'] ?? null; + return $formatter_class ? new $formatter_class($event_type) : null; + } + } + + if (isset($entity_config['strategy'])) { + $strategy_class = $entity_config['strategy']; + return new $strategy_class($event_type); + } + + return null; + } + + private function matchesStrategy(array $strategy, AuditContext $ctx): bool + { + if (isset($strategy['route']) && !$this->routeMatches($strategy['route'], $ctx->rawRoute)) { + return false; + } + + return true; + } + + private function routeMatches(string $route, string $actual_route): bool + { + return strcmp($actual_route, $route) === 0; + } +} diff --git a/app/Audit/AuditLogOtlpStrategy.php b/app/Audit/AuditLogOtlpStrategy.php new file mode 100644 index 00000000..1e158973 --- /dev/null +++ b/app/Audit/AuditLogOtlpStrategy.php @@ -0,0 +1,197 @@ +formatterFactory = $formatterFactory; + $this->enabled = config('opentelemetry.enabled', false); + $this->elasticIndex = config('opentelemetry.logs.elasticsearch_index', 'logs-audit'); + Log::debug("AuditLogOtlpStrategy::__construct", [ + 'enabled' => $this->enabled, + 'elasticIndex' => $this->elasticIndex, + ]); + } + + /** + * @param $subject + * @param array $change_set + * @param string $event_type + * @param AuditContext $ctx + * @return void + */ + public function audit($subject, array $change_set, string $event_type, AuditContext $ctx): void + { + if (!$this->enabled) { + return; + } + Log::debug("AuditLogOtlpStrategy::audit", ['subject' => $subject, 'change_set' => $change_set, 'event_type' => $event_type]); + try { + $entity = $this->resolveAuditableEntity($subject); + if (is_null($entity)) { + Log::warning("AuditLogOtlpStrategy::audit subject not found"); + return; + } + Log::debug("AuditLogOtlpStrategy::audit current user", ["user_id" => $ctx->userId, "user_email" => $ctx->userEmail]); + $formatter = $this->formatterFactory->make($ctx, $subject, $event_type); + if (is_null($formatter)) { + Log::warning("AuditLogOtlpStrategy::audit formatter not found"); + return; + } + $description = $formatter->format($subject, $change_set); + if (is_null($description)) { + Log::warning("AuditLogOtlpStrategy::audit description is empty"); + return; + } + $auditData = $this->buildAuditLogData($entity, $subject, $change_set, $event_type, $ctx); + if (!empty($description)) { + $auditData['audit.description'] = $description; + } + Log::debug("AuditLogOtlpStrategy::audit sending entry to OTEL", ["user_id" => $ctx->userId, "user_email" => $ctx->userEmail, 'payload' => $auditData]); + EmitAuditLogJob::dispatch($this->getLogMessage($event_type), $auditData); + Log::debug("AuditLogOtlpStrategy::audit entry sent to OTEL", ["user_id" => $ctx->userId, "user_email" => $ctx->userEmail]); + + } catch (\Exception $ex) { + Log::error('OTEL audit logging error: ' . $ex->getMessage(), [ + 'exception' => $ex, + 'subject_class' => get_class($subject), + 'event_type' => $event_type, + ]); + } + } + + private function resolveAuditableEntity($subject) + { + + // any collection → log the owner + if ($subject instanceof PersistentCollection) { + return $subject->getOwner(); + } + + // any object → log itself + if (is_object($subject)) { + return $subject; + } + + // nothing we can do + return null; + } + + private function buildAuditLogData($entity, $subject, array $change_set, string $event_type, AuditContext $ctx): array + { + $data = [ + 'audit.action' => $this->mapEventTypeToAction($event_type), + 'audit.entity' => class_basename($entity), + 'audit.entity_id' => (string) (method_exists($entity, 'getId') ? $entity->getId() : 'unknown'), + 'audit.entity_class' => get_class($entity), + 'audit.timestamp' => now()->toISOString(), + 'audit.event_type' => $event_type, + 'elasticsearch.index' => $this->elasticIndex, + ]; + // user data + $data['auth.user.id'] = $ctx->userId ?? 'unknown'; + $data['auth.user.email'] = $ctx->userEmail ?? 'unknown'; + $data['auth.user.first_name'] = $ctx->userFirstName ?? 'unknown'; + $data['auth.user.last_name'] = $ctx->userLastName ?? 'unknown'; + + // UI / request + $data['ui.app'] = $ctx->uiApp ?? 'unknown'; + $data['ui.flow'] = $ctx->uiFlow ?? 'unknown'; + $data['http.route'] = $ctx->route ?? null; + $data['http.method'] = $ctx->httpMethod ?? null; + $data['client.ip'] = $ctx->clientIp ?? null; + $data['user_agent'] = $ctx->userAgent ?? null; + + switch ($event_type) { + case IAuditStrategy::EVENT_COLLECTION_UPDATE: + if ($subject instanceof PersistentCollection) { + $data['audit.collection_type'] = $this->getCollectionType($subject); + $data['audit.collection_count'] = count($subject); + + $changes = $this->getCollectionChanges($subject, $change_set); + $data['audit.collection_current_count'] = $changes['current_count']; + $data['audit.collection_snapshot_count'] = $changes['snapshot_count']; + $data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false'; + } + break; + } + + return $data; + } + + private function getCollectionType(PersistentCollection $collection): string + { + try { + if (!method_exists($collection, 'getMapping')) { + return 'unknown'; + } + + $mapping = $collection->getMapping(); + + if (!isset($mapping['targetEntity']) || empty($mapping['targetEntity'])) { + return 'unknown'; + } + + return class_basename($mapping['targetEntity']); + } catch (\Exception $ex) { + return 'unknown'; + } + } + + private function getCollectionChanges(PersistentCollection $collection, array $change_set): array + { + return [ + 'current_count' => count($collection), + 'snapshot_count' => count($collection->getSnapshot()), + 'is_dirty' => $collection->isDirty(), + ]; + } + + private function mapEventTypeToAction(string $event_type): string + { + return match ($event_type) { + IAuditStrategy::EVENT_ENTITY_CREATION => IAuditStrategy::ACTION_CREATE, + IAuditStrategy::EVENT_ENTITY_UPDATE => IAuditStrategy::ACTION_UPDATE, + IAuditStrategy::EVENT_ENTITY_DELETION => IAuditStrategy::ACTION_DELETE, + IAuditStrategy::EVENT_COLLECTION_UPDATE => IAuditStrategy::ACTION_COLLECTION_UPDATE, + default => IAuditStrategy::ACTION_UNKNOWN + }; + } + + private function getLogMessage(string $event_type): string + { + return match ($event_type) { + IAuditStrategy::EVENT_ENTITY_CREATION => IAuditStrategy::LOG_MESSAGE_CREATED, + IAuditStrategy::EVENT_ENTITY_UPDATE => IAuditStrategy::LOG_MESSAGE_UPDATED, + IAuditStrategy::EVENT_ENTITY_DELETION => IAuditStrategy::LOG_MESSAGE_DELETED, + IAuditStrategy::EVENT_COLLECTION_UPDATE => IAuditStrategy::LOG_MESSAGE_COLLECTION_UPDATED, + default => IAuditStrategy::LOG_MESSAGE_CHANGED + }; + } + + +} \ No newline at end of file diff --git a/app/Audit/AuditLogStrategy.php b/app/Audit/AuditLogStrategy.php new file mode 100644 index 00000000..dceafc1e --- /dev/null +++ b/app/Audit/AuditLogStrategy.php @@ -0,0 +1,111 @@ +em = $em; + } + + private function resolveAuditableEntity($subject): ?IEntity + { + + return null; + } + + /** + * @param $subject + * @param $change_set + * @param $event_type + * @return void + */ + public function audit($subject, $change_set, $event_type, AuditContext $ctx): void + { + try { + $entity = $this->resolveAuditableEntity($subject); + + if (!($entity instanceof IEntity)) + return; + + $logger = AuditLoggerFactory::build($entity); + if (is_null($logger)) + return; + + $formatter = null; + + switch ($event_type) { + case self::EVENT_COLLECTION_UPDATE: + $child_entity = null; + if (count($subject) > 0) { + $child_entity = $subject[0]; + } + if (is_null($child_entity) && count($subject->getSnapshot()) > 0) { + $child_entity = $subject->getSnapshot()[0]; + } + $child_entity_formatter = $child_entity != null ? ChildEntityFormatterFactory::build($child_entity) : null; + $formatter = new EntityCollectionUpdateAuditLogFormatter($child_entity_formatter); + break; + case self::EVENT_ENTITY_CREATION: + $formatter = new EntityCreationAuditLogFormatter(); + break; + case self::EVENT_ENTITY_DELETION: + $child_entity_formatter = ChildEntityFormatterFactory::build($subject); + $formatter = new EntityDeletionAuditLogFormatter($child_entity_formatter); + break; + case self::EVENT_ENTITY_UPDATE: + $child_entity_formatter = ChildEntityFormatterFactory::build($subject); + $formatter = new EntityUpdateAuditLogFormatter($child_entity_formatter); + break; + } + + if (is_null($formatter)) + return; + + $description = $formatter->format($subject, $change_set); + + if (!empty($description)) { + $logger->createAuditLogEntry($this->em, $entity, $description); + } + } catch (\Exception $ex) { + Log::warning($ex); + } + } +} \ No newline at end of file diff --git a/app/Audit/AuditLoggerFactory.php b/app/Audit/AuditLoggerFactory.php new file mode 100644 index 00000000..7d8e3634 --- /dev/null +++ b/app/Audit/AuditLoggerFactory.php @@ -0,0 +1,40 @@ +child_entity_formatter = $child_entity_formatter; + } + + /** + * @inheritDoc + */ + public function format($subject, array $change_set): ?string + { + try { + if ($this->child_entity_formatter != null) { + $changes = []; + + $insertDiff = $subject->getInsertDiff(); + + foreach ($insertDiff as $child_changed_entity) { + $changes[] = $this->child_entity_formatter + ->format($child_changed_entity, IChildEntityAuditLogFormatter::CHILD_ENTITY_CREATION); + } + + $deleteDiff = $subject->getDeleteDiff(); + + foreach ($deleteDiff as $child_changed_entity) { + $changes[] = $this->child_entity_formatter + ->format($child_changed_entity, IChildEntityAuditLogFormatter::CHILD_ENTITY_DELETION); + } + return implode("|", $changes); + } + return null; + } catch (ReflectionException $e) { + Log::error($e); + return null; + } + } +} \ No newline at end of file diff --git a/app/Audit/ConcreteFormatters/EntityCreationAuditLogFormatter.php b/app/Audit/ConcreteFormatters/EntityCreationAuditLogFormatter.php new file mode 100644 index 00000000..ee0f8b5b --- /dev/null +++ b/app/Audit/ConcreteFormatters/EntityCreationAuditLogFormatter.php @@ -0,0 +1,47 @@ +getCreationIgnoredEntities(); + if (in_array($class_name, $ignored_entities)) + return null; + return "{$class_name} created"; + } +} \ No newline at end of file diff --git a/app/Audit/ConcreteFormatters/EntityDeletionAuditLogFormatter.php b/app/Audit/ConcreteFormatters/EntityDeletionAuditLogFormatter.php new file mode 100644 index 00000000..f50c5d6a --- /dev/null +++ b/app/Audit/ConcreteFormatters/EntityDeletionAuditLogFormatter.php @@ -0,0 +1,64 @@ +child_entity_formatter = $child_entity_formatter; + } + + protected function getCreationIgnoredEntities(): array + { + return [ + 'PresentationAction', + 'PresentationExtraQuestionAnswer' + ]; + } + + /** + * @inheritDoc + */ + public function format($subject, $change_set): ?string + { + $class_name = class_basename(is_string($subject) ? $subject : get_class($subject)); + $ignored_entities = $this->getCreationIgnoredEntities(); + if (in_array($class_name, $ignored_entities)) + return null; + + if ($this->child_entity_formatter != null) { + return $this->child_entity_formatter + ->format($subject, IChildEntityAuditLogFormatter::CHILD_ENTITY_DELETION); + } + + return "{$class_name} deleted"; + } +} \ No newline at end of file diff --git a/app/Audit/ConcreteFormatters/EntityUpdateAuditLogFormatter.php b/app/Audit/ConcreteFormatters/EntityUpdateAuditLogFormatter.php new file mode 100644 index 00000000..9110b719 --- /dev/null +++ b/app/Audit/ConcreteFormatters/EntityUpdateAuditLogFormatter.php @@ -0,0 +1,167 @@ +child_entity_formatter = $child_entity_formatter; + } + + protected function getIgnoredFields() + { + return [ + 'last_created', + 'last_updated', + 'last_edited', + 'created_by', + 'updated_by' + ]; + } + + /** + * @param string $parent_class + * @param string $prop_name + * @param IEntity|null $old_value + * @param IEntity|null $new_value + * @param $class + * @param callable $formatter + * @return string|null + */ + private static function formatEntity(string $parent_class, string $prop_name, ?IEntity $old_value, ?IEntity $new_value, callable $formatter): ?string + { + $msg = "Property \"{$prop_name}\" of entity \"{$parent_class}\" has changed from "; + + if (is_null($old_value)) { + $msg .= " TBD "; + } else { + $msg .= " \"{$formatter($old_value)})\" "; + } + $msg .= " to "; + if (is_null($new_value)) { + $msg .= " TBD "; + } else { + $msg .= " \"{$formatter($new_value)})\" "; + } + return $msg; + } + + /** + * @inheritDoc + */ + public function format($subject, $change_set): ?string + { + $res = []; + $class_name = class_basename(is_string($subject) ? $subject : get_class($subject)); + $ignored_fields = $this->getIgnoredFields(); + + foreach (array_keys($change_set) as $prop_name) { + if (in_array($prop_name, $ignored_fields)) + continue; + + $change_values = $change_set[$prop_name]; + + $old_value = $change_values[0]; + $new_value = $change_values[1]; + + if ($this->child_entity_formatter != null) { + $res[] = $this->child_entity_formatter + ->format( + $subject, + IChildEntityAuditLogFormatter::CHILD_ENTITY_UPDATE, + "Property \"{$prop_name}\" has changed from \"{$old_value}\" to \"{$new_value}\"" + ); + continue; + } + + if ($old_value instanceof BaseEntity || $new_value instanceof BaseEntity) { + $res[] = "Property \"{$prop_name}\" of entity \"{$class_name}\" has changed"; + if ($old_value instanceof SummitAbstractLocation || $new_value instanceof SummitAbstractLocation) { + $res[] = self::formatEntity($class_name, $prop_name, $old_value, $new_value, function ($value) { + return " \"{$value->getName()} ({$value->getId()})\" "; + }); + } else if ($old_value instanceof PresentationCategory || $new_value instanceof PresentationCategory) { + $res[] = self::formatEntity($class_name, $prop_name, $old_value, $new_value, function ($value) { + return " \"{$value->getTitle()} ({$value->getId()})\" "; + }); + } else if ($old_value instanceof SelectionPlan || $new_value instanceof SelectionPlan) { + $res[] = self::formatEntity($class_name, $prop_name, $old_value, $new_value, function ($value) { + return " \"{$value->getName()} ({$value->getId()})\" "; + }); + } else if ($old_value instanceof SummitEventType || $new_value instanceof SummitEventType) { + $res[] = self::formatEntity($class_name, $prop_name, $old_value, $new_value, function ($value) { + return " \"{$value->getType()} ({$value->getId()})\" "; + }); + } else if ($old_value instanceof Member || $new_value instanceof Member) { + $res[] = self::formatEntity($class_name, $prop_name, $old_value, $new_value, function ($value) { + return " \"{$value->getFullName()} ({$value->getEmail()})\" "; + }); + } else if ($old_value instanceof PresentationSpeaker || $new_value instanceof PresentationSpeaker) { + $res[] = self::formatEntity($class_name, $prop_name, $old_value, $new_value, function ($value) { + return " \"{$value->getFullName()} ({$value->getEmail()})\" "; + }); + } else if ($old_value instanceof File || $new_value instanceof File) { + $res[] = self::formatEntity($class_name, $prop_name, $old_value, $new_value, function ($value) { + return " \"{$value->getFilename()} ({$value->getId()})\" "; + }); + } + continue; + } + + if ($old_value instanceof DateTime || $new_value instanceof DateTime) { + $old_value = $old_value != null ? $old_value->format('Y-m-d H:i:s') : ""; + $new_value = $new_value != null ? $new_value->format('Y-m-d H:i:s') : ""; + } else if (is_bool($old_value) || is_bool($new_value)) { + $old_value = $old_value ? 'true' : 'false'; + $new_value = $new_value ? 'true' : 'false'; + } else { + $old_value = print_r($old_value, true); + $new_value = print_r($new_value, true); + } + + if ($old_value != $new_value) { + $res[] = "Property \"{$prop_name}\" of entity \"{$class_name}\" has changed from \"{$old_value}\" to \"{$new_value}\""; + } + } + + if (count($res) == 0) + return null; + + return join("|", $res); + } +} \ No newline at end of file diff --git a/app/Audit/IAuditLogFormatter.php b/app/Audit/IAuditLogFormatter.php new file mode 100644 index 00000000..0a942950 --- /dev/null +++ b/app/Audit/IAuditLogFormatter.php @@ -0,0 +1,35 @@ +logMessage = $logMessage; + $this->auditData = $auditData; + } + + public function handle(): void + { + try { + Logger::info($this->logMessage, $this->auditData); + } catch (\Exception $e) { + Log::error("EmitAuditLogJob::handle failed", [ + 'message' => $this->logMessage, + 'error' => $e->getMessage() + ]); + } + } +} \ No newline at end of file diff --git a/app/ModelSerializers/SerializerRegistry.php b/app/ModelSerializers/SerializerRegistry.php index 12250286..2ac2c1ed 100644 --- a/app/ModelSerializers/SerializerRegistry.php +++ b/app/ModelSerializers/SerializerRegistry.php @@ -1,4 +1,5 @@ -getShortName(); - if(!isset($this->registry[$class])) - throw new \InvalidArgumentException('Serializer not found for '.$class); + public function getSerializer($object, $type = self::SerializerType_Public) + { + if (is_null($object)) + return null; + + $class = class_basename(is_string($object) ? $object : get_class($object)); + if (!isset($this->registry[$class])) + throw new \InvalidArgumentException('Serializer not found for ' . $class); $serializer_class = $this->registry[$class]; - if(is_array($serializer_class)){ - if(!isset($serializer_class[$type])) + if (is_array($serializer_class)) { + if (!isset($serializer_class[$type])) throw new \InvalidArgumentException(sprintf('Serializer not found for %s , type %s', $class, $type)); $serializer_class = $serializer_class[$type]; }