Skip to content

Commit bc4c0f3

Browse files
author
farhadzand
committed
add custom event
1 parent 84dd392 commit bc4c0f3

File tree

5 files changed

+275
-0
lines changed

5 files changed

+275
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to `iamfarhad/laravel-audit-log` will be documented in this
88
- Comprehensive update to `README.md` with detailed documentation on features, installation, configuration, and usage.
99
- Added examples for using scopes in audit log retrieval, including `action()`, `dateBetween()`, and `causer()`.
1010
- Enhanced usage section with basic and advanced examples for audit logging.
11+
- Added Fluent API for custom audit events via the `audit()` method, allowing for more intuitive and readable code.
1112

1213
### Changed
1314
- Updated documentation structure for better readability and adherence to best practices.

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,28 @@ Event::dispatch(new ModelAudited(
256256
));
257257
```
258258

259+
#### Fluent API for Custom Audit Events
260+
261+
For a more intuitive and readable way to log custom actions, use the fluent API provided by the `audit()` method:
262+
263+
```php
264+
<?php
265+
266+
declare(strict_types=1);
267+
268+
use App\Models\Order;
269+
270+
$order = Order::find(1);
271+
$order->audit()
272+
->custom('status_transition')
273+
->from(['status' => 'pending'])
274+
->to(['status' => 'shipped'])
275+
->withMetadata(['ip' => request()->ip(), 'user_agent' => request()->userAgent()])
276+
->log();
277+
```
278+
279+
This fluent interface allows you to chain methods to define the custom action, old and new values, and additional metadata before logging the event. It respects the model's auditable attributes and merges any default metadata defined in `getAuditMetadata()` with custom metadata provided.
280+
259281
## Customizing Audit Logging
260282

261283
If you need to extend the audit logging functionality, you can implement a custom driver by adhering to the `AuditDriverInterface`. Register your custom driver in a service provider:

src/Services/AuditBuilder.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace iamfarhad\LaravelAuditLog\Services;
6+
7+
use Illuminate\Support\Facades\Event;
8+
use Illuminate\Database\Eloquent\Model;
9+
use iamfarhad\LaravelAuditLog\Events\ModelAudited;
10+
11+
/**
12+
* A fluent builder for creating custom audit logs for a model.
13+
*/
14+
final class AuditBuilder
15+
{
16+
private Model $model;
17+
18+
private string $action;
19+
20+
private array $oldValues = [];
21+
22+
private array $newValues = [];
23+
24+
private array $metadata = [];
25+
26+
/**
27+
* Create a new AuditBuilder instance.
28+
*
29+
* @param Model $model The model to audit
30+
*/
31+
public function __construct(Model $model)
32+
{
33+
$this->model = $model;
34+
$this->action = 'custom';
35+
}
36+
37+
/**
38+
* Set the custom action name for the audit log.
39+
*
40+
* @param string $action The action name
41+
*/
42+
public function custom(string $action): self
43+
{
44+
$this->action = $action;
45+
46+
return $this;
47+
}
48+
49+
/**
50+
* Set the old values for the audit log.
51+
*
52+
* @param array $values The old values
53+
*/
54+
public function from(array $values): self
55+
{
56+
$this->oldValues = $values;
57+
58+
return $this;
59+
}
60+
61+
/**
62+
* Set the new values for the audit log.
63+
*
64+
* @param array $values The new values
65+
*/
66+
public function to(array $values): self
67+
{
68+
$this->newValues = $values;
69+
70+
return $this;
71+
}
72+
73+
/**
74+
* Add custom metadata to the audit log.
75+
*
76+
* @param array $metadata Additional metadata
77+
*/
78+
public function withMetadata(array $metadata): self
79+
{
80+
$this->metadata = $metadata;
81+
82+
return $this;
83+
}
84+
85+
/**
86+
* Dispatch the audit event to log the custom action.
87+
*/
88+
public function log(): void
89+
{
90+
// Merge model metadata with custom metadata
91+
$metadata = array_merge($this->model->getAuditMetadata(), $this->metadata);
92+
93+
// If the model has getAuditableAttributes method, filter values
94+
if (method_exists($this->model, 'getAuditableAttributes')) {
95+
$this->oldValues = $this->model->getAuditableAttributes($this->oldValues);
96+
$this->newValues = $this->model->getAuditableAttributes($this->newValues);
97+
}
98+
99+
Event::dispatch(new ModelAudited(
100+
model: $this->model,
101+
action: $this->action,
102+
oldValues: $this->oldValues,
103+
newValues: $this->newValues
104+
));
105+
}
106+
}

src/Traits/Auditable.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Support\Facades\Log;
88
use Illuminate\Database\Eloquent\Model;
99
use iamfarhad\LaravelAuditLog\Events\ModelAudited;
10+
use iamfarhad\LaravelAuditLog\Services\AuditBuilder;
1011
use iamfarhad\LaravelAuditLog\Models\EloquentAuditLog;
1112

1213
/**
@@ -156,4 +157,12 @@ public function auditLogs()
156157
{
157158
return $this->morphMany(EloquentAuditLog::forEntity(static::class), 'auditable');
158159
}
160+
161+
/**
162+
* Initiate a fluent builder for custom audit logging.
163+
*/
164+
public function audit(): AuditBuilder
165+
{
166+
return new AuditBuilder($this);
167+
}
159168
}

tests/Unit/AuditBuilderTest.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace iamfarhad\LaravelAuditLog\Tests\Unit;
6+
7+
use Mockery;
8+
use Illuminate\Support\Facades\Event;
9+
use iamfarhad\LaravelAuditLog\Tests\TestCase;
10+
use iamfarhad\LaravelAuditLog\Events\ModelAudited;
11+
use iamfarhad\LaravelAuditLog\Services\AuditBuilder;
12+
13+
final class AuditBuilderTest extends TestCase
14+
{
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
Event::fake();
19+
}
20+
21+
public function test_can_build_and_log_custom_audit_event_with_fluent_api(): void
22+
{
23+
// Arrange
24+
$model = Mockery::mock('Illuminate\Database\Eloquent\Model');
25+
$model->shouldReceive('getAuditMetadata')->andReturn(['default' => 'meta']);
26+
$model->shouldReceive('getAuditableAttributes')->andReturnUsing(function ($attributes) {
27+
return $attributes;
28+
});
29+
30+
// Act
31+
$builder = new AuditBuilder($model);
32+
$builder
33+
->custom('status_change')
34+
->from(['status' => 'pending'])
35+
->to(['status' => 'approved'])
36+
->withMetadata(['ip' => '127.0.0.1'])
37+
->log();
38+
39+
// Assert
40+
Event::assertDispatched(ModelAudited::class, function (ModelAudited $event) use ($model) {
41+
return $event->model === $model
42+
&& $event->action === 'status_change'
43+
&& $event->oldValues === ['status' => 'pending']
44+
&& $event->newValues === ['status' => 'approved'];
45+
});
46+
}
47+
48+
public function test_uses_default_action_if_custom_not_specified(): void
49+
{
50+
// Arrange
51+
$model = Mockery::mock('Illuminate\Database\Eloquent\Model');
52+
$model->shouldReceive('getAuditMetadata')->andReturn([]);
53+
$model->shouldReceive('getAuditableAttributes')->andReturnUsing(function ($attributes) {
54+
return $attributes;
55+
});
56+
57+
// Act
58+
$builder = new AuditBuilder($model);
59+
$builder
60+
->from(['key' => 'old'])
61+
->to(['key' => 'new'])
62+
->log();
63+
64+
// Assert
65+
Event::assertDispatched(ModelAudited::class, function (ModelAudited $event) {
66+
return $event->action === 'custom';
67+
});
68+
}
69+
70+
public function test_merges_model_metadata_with_custom_metadata(): void
71+
{
72+
// Arrange
73+
$model = Mockery::mock('Illuminate\Database\Eloquent\Model');
74+
$model->shouldReceive('getAuditMetadata')->andReturn(['default' => 'meta']);
75+
$model->shouldReceive('getAuditableAttributes')->andReturnUsing(function ($attributes) {
76+
return $attributes;
77+
});
78+
79+
// Act
80+
$builder = new AuditBuilder($model);
81+
$builder
82+
->custom('test_action')
83+
->withMetadata(['custom' => 'data'])
84+
->log();
85+
86+
// Assert
87+
Event::assertDispatched(ModelAudited::class);
88+
}
89+
90+
public function test_filters_values_using_get_auditable_attributes_if_available(): void
91+
{
92+
// Create a concrete class with getAuditableAttributes method instead of using a mock
93+
$model = new class extends \Illuminate\Database\Eloquent\Model
94+
{
95+
public function getAuditMetadata(): array
96+
{
97+
return [];
98+
}
99+
100+
public function getAuditableAttributes(array $attributes): array
101+
{
102+
// Only return the 'allowed' key if it exists in the input
103+
return isset($attributes['allowed']) ? ['allowed' => $attributes['allowed']] : [];
104+
}
105+
};
106+
107+
// Directly test AuditBuilder behavior without event faking
108+
$oldValues = ['allowed' => 'value', 'disallowed' => 'secret'];
109+
$newValues = ['allowed' => 'new_value', 'disallowed' => 'new_secret'];
110+
111+
// Setup expectation that Event::dispatch will be called with filtered values
112+
Event::shouldReceive('dispatch')
113+
->once()
114+
->with(Mockery::on(function ($event) use ($model) {
115+
return $event instanceof ModelAudited
116+
&& $event->model === $model
117+
&& $event->oldValues === ['allowed' => 'value']
118+
&& $event->newValues === ['allowed' => 'new_value'];
119+
}));
120+
121+
// Act
122+
$builder = new AuditBuilder($model);
123+
$builder
124+
->from($oldValues)
125+
->to($newValues)
126+
->log();
127+
128+
// If we get here without Mockery exceptions, the test passes
129+
$this->assertTrue(true);
130+
}
131+
}
132+
133+
// Add a mock interface for the test to ensure method_exists works
134+
interface ModelWithGetAuditableAttributes
135+
{
136+
public function getAuditableAttributes(array $attributes): array;
137+
}

0 commit comments

Comments
 (0)