Skip to content

Commit 7e817d7

Browse files
chore/refactor to strategy formatters (#468)
* feat: add news formatter for entitys related to Presentation for speakers * feat: add a formatter for entities related to speaker presentations * chore: replace the otlp interface with abstractFactory. Add the user context to the formatters * chore: refactor getUserInfo to AbstractAuditLog * chore: refactor strategy for get Formatters * chore: change path to actual routes * fix: change path to actual routes * fix: OpenAPI doc * fix: add missing param --------- Co-authored-by: Matias Perrone <[email protected]>
1 parent ac44af7 commit 7e817d7

17 files changed

+1182
-44
lines changed

app/Audit/AbstractAuditLogFormatter.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,22 @@ final public function setContext(AuditContext $ctx): void
2424
$this->ctx = $ctx;
2525
}
2626

27+
protected function getUserInfo(): string
28+
{
29+
if (!$this->ctx) {
30+
return 'Unknown (unknown)';
31+
}
32+
33+
$user_name = 'Unknown';
34+
if ($this->ctx->userFirstName || $this->ctx->userLastName) {
35+
$user_name = trim(sprintf("%s %s", $this->ctx->userFirstName ?? '', $this->ctx->userLastName ?? '')) ?: 'Unknown';
36+
} elseif ($this->ctx->userEmail) {
37+
$user_name = $this->ctx->userEmail;
38+
}
39+
40+
$user_id = $this->ctx->userId ?? 'unknown';
41+
return sprintf("%s (%s)", $user_name, $user_id);
42+
}
43+
2744
abstract public function format($subject, array $change_set): ?string;
2845
}

app/Audit/AuditContext.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public function __construct(
2121
public ?string $uiApp = null,
2222
public ?string $uiFlow = null,
2323
public ?string $route = null,
24+
public ?string $rawRoute = null,
2425
public ?string $httpMethod = null,
2526
public ?string $clientIp = null,
2627
public ?string $userAgent = null,

app/Audit/AuditEventListener.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@
1616
use Doctrine\ORM\Event\OnFlushEventArgs;
1717
use Illuminate\Support\Facades\App;
1818
use Illuminate\Support\Facades\Log;
19+
use Illuminate\Support\Facades\Route;
1920

2021
/**
2122
* Class AuditEventListener
2223
* @package App\Audit
2324
*/
2425
class AuditEventListener
2526
{
27+
private const ROUTE_METHOD_SEPARATOR = '|';
2628

2729
public function onFlush(OnFlushEventArgs $eventArgs): void
2830
{
29-
if (app()->environment('testing')){
31+
if (app()->environment('testing')) {
3032
return;
3133
}
3234
$em = $eventArgs->getObjectManager();
@@ -67,7 +69,7 @@ public function onFlush(OnFlushEventArgs $eventArgs): void
6769
/**
6870
* Get the appropriate audit strategy based on environment configuration
6971
*/
70-
private function getAuditStrategy($em)
72+
private function getAuditStrategy($em): ?IAuditStrategy
7173
{
7274
// Check if OTLP audit is enabled
7375
if (config('opentelemetry.enabled', false)) {
@@ -97,7 +99,11 @@ private function buildAuditContext(): AuditContext
9799
//$ui = app()->bound('ui.context') ? app('ui.context') : [];
98100

99101
$req = request();
100-
102+
103+
$route = Route::getRoutes()->match($req);
104+
$method = $route->methods[0] ?? 'UNKNOWN';
105+
$rawRoute = $method . self::ROUTE_METHOD_SEPARATOR . $route->uri;
106+
101107
return new AuditContext(
102108
userId: $member?->getId(),
103109
userEmail: $member?->getEmail(),
@@ -109,6 +115,7 @@ private function buildAuditContext(): AuditContext
109115
httpMethod: $req?->method(),
110116
clientIp: $req?->ip(),
111117
userAgent: $req?->userAgent(),
118+
rawRoute: $rawRoute
112119
);
113120
}
114121
}

app/Audit/AuditLogFormatterFactory.php

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,27 +45,36 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
4545
if (count($subject) > 0) {
4646
$child_entity = $subject[0];
4747
}
48-
if (is_null($child_entity) && count($subject->getSnapshot()) > 0) {
48+
if (is_null($child_entity) && isset($subject->getSnapshot()[0]) && count($subject->getSnapshot()) > 0) {
4949
$child_entity = $subject->getSnapshot()[0];
5050
}
5151
$child_entity_formatter = $child_entity != null ? ChildEntityFormatterFactory::build($child_entity) : null;
5252
$formatter = new EntityCollectionUpdateAuditLogFormatter($child_entity_formatter);
5353
break;
5454
case IAuditStrategy::EVENT_ENTITY_CREATION:
55-
$formatter = $this->getStrategyClass($subject, $eventType);
55+
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
56+
if (is_null($formatter)) {
57+
$formatter = $this->getStrategyClass($subject, $eventType);
58+
}
5659
if(is_null($formatter)) {
5760
$formatter = new EntityCreationAuditLogFormatter();
5861
}
5962
break;
6063
case IAuditStrategy::EVENT_ENTITY_DELETION:
61-
$formatter = $this->getStrategyClass($subject, $eventType);
64+
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
65+
if (is_null($formatter)) {
66+
$formatter = $this->getStrategyClass($subject, $eventType);
67+
}
6268
if(is_null($formatter)) {
6369
$child_entity_formatter = ChildEntityFormatterFactory::build($subject);
6470
$formatter = new EntityDeletionAuditLogFormatter($child_entity_formatter);
6571
}
6672
break;
6773
case IAuditStrategy::EVENT_ENTITY_UPDATE:
68-
$formatter = $this->getStrategyClass($subject, $eventType);
74+
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
75+
if (is_null($formatter)) {
76+
$formatter = $this->getStrategyClass($subject, $eventType);
77+
}
6978
if(is_null($formatter)) {
7079
$child_entity_formatter = ChildEntityFormatterFactory::build($subject);
7180
$formatter = new EntityUpdateAuditLogFormatter($child_entity_formatter);
@@ -75,4 +84,39 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
7584
$formatter->setContext($ctx);
7685
return $formatter;
7786
}
87+
88+
private function getFormatterByContext(object $subject, string $event_type, AuditContext $ctx): ?IAuditLogFormatter
89+
{
90+
$class = get_class($subject);
91+
$entity_config = $this->config['entities'][$class] ?? null;
92+
93+
if (!$entity_config || !isset($entity_config['strategies'])) {
94+
return null;
95+
}
96+
97+
foreach ($entity_config['strategies'] as $strategy) {
98+
if (!$this->matchesStrategy($strategy, $ctx)) {
99+
continue;
100+
}
101+
102+
$formatter_class = $strategy['formatter'] ?? null;
103+
return $formatter_class ? new $formatter_class($event_type) : null;
104+
}
105+
106+
return null;
107+
}
108+
109+
private function matchesStrategy(array $strategy, AuditContext $ctx): bool
110+
{
111+
if (isset($strategy['route']) && !$this->routeMatches($strategy['route'], $ctx->rawRoute)) {
112+
return false;
113+
}
114+
115+
return true;
116+
}
117+
118+
private function routeMatches(string $route, string $actual_route): bool
119+
{
120+
return strcmp($actual_route, $route) === 0;
121+
}
78122
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace App\Audit\ConcreteFormatters;
4+
5+
/**
6+
* Copyright 2025 OpenStack Foundation
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
**/
17+
18+
use App\Audit\AbstractAuditLogFormatter;
19+
use App\Audit\Interfaces\IAuditStrategy;
20+
use App\Models\Foundation\Summit\Speakers\FeaturedSpeaker;
21+
use Illuminate\Support\Facades\Log;
22+
23+
class FeaturedSpeakerAuditLogFormatter extends AbstractAuditLogFormatter
24+
{
25+
private string $event_type;
26+
27+
public function __construct(string $event_type)
28+
{
29+
$this->event_type = $event_type;
30+
}
31+
32+
public function format($subject, array $change_set): ?string
33+
{
34+
if (!$subject instanceof FeaturedSpeaker) {
35+
return null;
36+
}
37+
38+
try {
39+
$speaker = $subject->getSpeaker();
40+
$speaker_email = $speaker ? ($speaker->getEmail() ?? 'unknown') : 'unknown';
41+
$speaker_name = $speaker ? sprintf("%s %s", $speaker->getFirstName() ?? '', $speaker->getLastName() ?? '') : 'Unknown';
42+
$speaker_name = trim($speaker_name) ?: $speaker_name;
43+
$speaker_id = $speaker ? ($speaker->getId() ?? 'unknown') : 'unknown';
44+
45+
$summit = $subject->getSummit();
46+
$summit_name = $summit ? ($summit->getName() ?? 'Unknown Summit') : 'Unknown Summit';
47+
48+
$order = $subject->getOrder();
49+
50+
switch ($this->event_type) {
51+
case IAuditStrategy::EVENT_ENTITY_CREATION:
52+
return sprintf(
53+
"Speaker '%s' (%s) added as featured speaker for Summit '%s' with display order %d by user %s",
54+
$speaker_name,
55+
$speaker_id,
56+
$summit_name,
57+
$order,
58+
$this->getUserInfo()
59+
);
60+
61+
case IAuditStrategy::EVENT_ENTITY_UPDATE:
62+
$changed_fields = [];
63+
64+
if (isset($change_set['Order'])) {
65+
$old_order = $change_set['Order'][0] ?? 'unknown';
66+
$new_order = $change_set['Order'][1] ?? 'unknown';
67+
$changed_fields[] = sprintf("display_order %s → %s", $old_order, $new_order);
68+
}
69+
if (isset($change_set['PresentationSpeakerID'])) {
70+
$changed_fields[] = "speaker";
71+
}
72+
73+
$fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties';
74+
return sprintf(
75+
"Featured speaker '%s' (%s) updated (%s changed) by user %s",
76+
$speaker_name,
77+
$speaker_id,
78+
$fields_str,
79+
$this->getUserInfo()
80+
);
81+
82+
case IAuditStrategy::EVENT_ENTITY_DELETION:
83+
return sprintf(
84+
"Speaker '%s' (%s) removed from featured speakers list of Summit '%s' by user %s",
85+
$speaker_name,
86+
$speaker_id,
87+
$summit_name,
88+
$this->getUserInfo()
89+
);
90+
}
91+
} catch (\Exception $ex) {
92+
Log::warning("FeaturedSpeakerAuditLogFormatter error: " . $ex->getMessage());
93+
}
94+
95+
return null;
96+
}
97+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
namespace App\Audit\ConcreteFormatters\PresentationFormatters;
4+
5+
/**
6+
* Copyright 2025 OpenStack Foundation
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
**/
17+
18+
use App\Audit\AbstractAuditLogFormatter;
19+
use App\Audit\Interfaces\IAuditStrategy;
20+
use models\summit\Presentation;
21+
use Illuminate\Support\Facades\Log;
22+
23+
abstract class BasePresentationAuditLogFormatter extends AbstractAuditLogFormatter
24+
{
25+
protected string $event_type;
26+
27+
public function __construct(string $event_type)
28+
{
29+
$this->event_type = $event_type;
30+
}
31+
32+
protected function extractChangedFields(array $change_set): array
33+
{
34+
$changed_fields = [];
35+
$old_status = null;
36+
$new_status = null;
37+
38+
if (isset($change_set['Title'])) {
39+
$changed_fields[] = "title";
40+
}
41+
if (isset($change_set['Abstract'])) {
42+
$changed_fields[] = "abstract";
43+
}
44+
if (isset($change_set['ProblemAddressed'])) {
45+
$changed_fields[] = "problem_addressed";
46+
}
47+
if (isset($change_set['AttendeesExpectedLearnt'])) {
48+
$changed_fields[] = "attendees_expected_learnt";
49+
}
50+
51+
if (isset($change_set['Status'])) {
52+
$changed_fields[] = "status";
53+
$old_status = $change_set['Status'][0] ?? null;
54+
$new_status = $change_set['Status'][1] ?? null;
55+
}
56+
if (isset($change_set['CategoryID']) || isset($change_set['category'])) {
57+
$changed_fields[] = "track";
58+
}
59+
if (isset($change_set['Published'])) {
60+
$changed_fields[] = "published";
61+
}
62+
if (isset($change_set['SelectionPlanID'])) {
63+
$changed_fields[] = "selection_plan";
64+
}
65+
66+
return [
67+
'fields' => !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties',
68+
'old_status' => $old_status,
69+
'new_status' => $new_status,
70+
];
71+
}
72+
73+
protected function getPresentationData(Presentation $subject): array
74+
{
75+
$creator = $subject->getCreator();
76+
$creator_name = $creator ? sprintf("%s %s", $creator->getFirstName() ?? '', $creator->getLastName() ?? '') : 'Unknown';
77+
$creator_name = trim($creator_name) ?: 'Unknown';
78+
79+
$category = $subject->getCategory();
80+
$category_name = $category ? $category->getTitle() : 'Unassigned Track';
81+
82+
$selection_plan = $subject->getSelectionPlan();
83+
$plan_name = $selection_plan ? $selection_plan->getName() : 'Unknown Plan';
84+
85+
return [
86+
'title' => $subject->getTitle() ?? 'Unknown Presentation',
87+
'id' => $subject->getId() ?? 'unknown',
88+
'creator_name' => $creator_name,
89+
'category_name' => $category_name,
90+
'plan_name' => $plan_name,
91+
];
92+
}
93+
94+
public function format($subject, array $change_set): ?string
95+
{
96+
if (!$subject instanceof Presentation) {
97+
return null;
98+
}
99+
100+
try {
101+
$data = $this->getPresentationData($subject);
102+
103+
switch ($this->event_type) {
104+
case IAuditStrategy::EVENT_ENTITY_CREATION:
105+
return $this->formatCreation($data);
106+
107+
case IAuditStrategy::EVENT_ENTITY_UPDATE:
108+
$extracted = $this->extractChangedFields($change_set);
109+
return $this->formatUpdate($data, $extracted);
110+
111+
case IAuditStrategy::EVENT_ENTITY_DELETION:
112+
return $this->formatDeletion($data);
113+
}
114+
} catch (\Exception $ex) {
115+
Log::warning(static::class . " error: " . $ex->getMessage());
116+
}
117+
118+
return null;
119+
}
120+
121+
abstract protected function formatCreation(array $data): string;
122+
123+
abstract protected function formatUpdate(array $data, array $extracted): string;
124+
125+
abstract protected function formatDeletion(array $data): string;
126+
}

0 commit comments

Comments
 (0)