Skip to content

Commit 2d2c0e8

Browse files
authored
Pivots (#8)
1 parent f9f7d3d commit 2d2c0e8

21 files changed

+795
-22
lines changed

README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,98 @@ public function getAuditLogIgnoredFields() : array
6363
}
6464
```
6565

66+
####Pivots
67+
Audit log can also support changes on pivot models as well.
68+
69+
In this example we have a `posts` and `tags` table with a `post_tags` pivot table containing a `post_id` and `tag_id`.
70+
71+
Modify the audit log migration replacing the `subject_id` column to use the two pivot columns.
72+
```php
73+
Schema::create('post_tag_auditlog', function (Blueprint $table) {
74+
$table->bigIncrements('id');
75+
$table->unsignedInteger('post_id')->index();
76+
$table->unsignedInteger('tag_id')->index();
77+
$table->unsignedTinyInteger('event_type')->index();
78+
$table->unsignedInteger('user_id')->nullable()->index();
79+
$table->string('field_name')->index();
80+
$table->text('field_value_old')->nullable();
81+
$table->text('field_value_new')->nullable();
82+
$table->timestamp('occurred_at')->index()->default('CURRENT_TIMESTAMP');
83+
});
84+
```
85+
86+
Create a model for the pivot table that extends Laravel's Pivot class. This class must use the AuditLoggablePivot trait and have a defined `$audit_loggable_keys` variable, which is used to map the pivot to the audit log table.
87+
88+
```php
89+
class PostTag extends Pivot
90+
{
91+
use AuditLoggablePivot;
92+
93+
/**
94+
* The array keys are the composite key in the audit log
95+
* table while the pivot table columns are the values.
96+
*
97+
* @var array
98+
*/
99+
protected $audit_loggable_keys = [
100+
'post_id' => 'post_id',
101+
'tag_id' => 'tag_id',
102+
];
103+
}
104+
```
105+
Side note: if a column shares the same name in the pivot and a column already in the audit log table (ex: `user_id`), change the name of the column in the audit log table (ex: `audit_user_id`) and define the relationship as `'audit_user_id' => 'user_id'`.
106+
107+
The two models that are joined by the pivot will need to be updated so that events fire on the pivot model. Currently Laravel doesn't support pivot events so a third party package is required.
108+
```php
109+
composer require fico7489/laravel-pivot
110+
```
111+
112+
Have both models use the PivotEventTrait
113+
```php
114+
use Fico7489\Laravel\Pivot\Traits\PivotEventTrait;
115+
use Illuminate\Database\Eloquent\Model;
116+
117+
class Post extends Model
118+
{
119+
use PivotEventTrait;
120+
```
121+
122+
Modify the belongsToMany join on both related models to include the using function along with the pivot model.
123+
In the Post model:
124+
```php
125+
public function tags()
126+
{
127+
return $this->belongsToMany(Tag::class)
128+
->using(PostTag::class);
129+
}
130+
```
131+
In the Tag model:
132+
```php
133+
public function posts()
134+
{
135+
return $this->belongsToMany(Post::class)
136+
->using(PostTag::class);
137+
}
138+
```
139+
140+
When a pivot record is deleted through `detach` or `sync`, an audit log record for each of the keys (ex: `post_id` and `tag_id`) will added to the audit log table. The `field_value_old` will be the id of the record and the `field_value_new` will be null. The records will have an event type of `PIVOT_DELETED` (id: 6).
141+
142+
If you need to pull the audit logs through the `auditLogs` relationship (ex: $post_tag->auditLogs()->get()), support for composite keys is required.
143+
```php
144+
composer require awobaz/compoships
145+
```
146+
Then use the trait on the pivot audit log model:
147+
```php
148+
use Awobaz\Compoships\Compoships;
149+
use OrisIntel\AuditLog\Models\BaseModel;
150+
151+
class PostTagAuditLog extends BaseModel
152+
{
153+
use Compoships;
154+
```
155+
156+
For a working example of pivots with the audit log, see `laravel-model-auditlog/tests/Fakes`, which contains working migrations and models.
157+
66158
### Testing
67159

68160
``` bash

composer.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@
2424
],
2525
"require": {
2626
"php": "^7.1",
27-
"laravel/framework": "^5.6",
28-
"orisintel/laravel-process-stamps": "^1.1"
27+
"awobaz/compoships": "^1.1",
28+
"fico7489/laravel-pivot": "^3.0.1",
29+
"laravel/framework": "^5.8",
30+
"orisintel/laravel-process-stamps": "^1.2"
2931
},
3032
"require-dev": {
3133
"doctrine/dbal": "^2.9",
3234
"larapack/dd": "^1.0",
3335
"mockery/mockery": "~1.0",
34-
"orchestra/testbench": "^3.7",
36+
"orchestra/testbench": "^3.8",
3537
"phpunit/phpunit": "^7.0"
3638
},
3739
"autoload": {

src/EventType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ class EventType
99
const DELETED = 3;
1010
const RESTORED = 4;
1111
const FORCE_DELETED = 5;
12+
const PIVOT_DELETED = 6;
1213
}

src/Models/BaseModel.php

Lines changed: 129 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Database\Eloquent\Model;
66
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
use Illuminate\Support\Collection;
78
use OrisIntel\AuditLog\EventType;
89

910
/**
@@ -19,24 +20,26 @@ abstract class BaseModel extends Model
1920
* @param int $event_type
2021
* @param Model $model
2122
*/
22-
public static function recordChanges(int $event_type, $model) : void
23+
public function recordChanges(int $event_type, $model) : void
2324
{
24-
switch ($event_type) {
25-
default:
26-
$changes = $model->getDirty();
27-
break;
28-
case EventType::CREATED:
29-
$changes = $model->getAttributes();
30-
break;
31-
case EventType::RESTORED:
32-
$changes = $model->getChanges();
33-
break;
34-
case EventType::FORCE_DELETED:
35-
return; // if force deleted we want to stop execution here as there would be nothing to correlate records to
36-
break;
37-
}
25+
$changes = self::getChangesByType($event_type, $model);
3826

39-
collect($changes)
27+
$this->saveChanges(
28+
$this->passingChanges($changes, $model),
29+
$event_type,
30+
$model
31+
);
32+
}
33+
34+
/**
35+
* @param array $changes
36+
* @param $model
37+
*
38+
* @return Collection
39+
*/
40+
public function passingChanges(array $changes, $model) : Collection
41+
{
42+
return collect($changes)
4043
->except(config('model-auditlog.global_ignored_fields'))
4144
->except($model->getAuditLogIgnoredFields())
4245
->except([
@@ -45,24 +48,132 @@ public static function recordChanges(int $event_type, $model) : void
4548
'updated_at',
4649
'date_created',
4750
'date_modified',
48-
])
51+
]);
52+
}
53+
54+
public function saveChanges(Collection $passing_changes, int $event_type, $model) : void
55+
{
56+
$passing_changes
4957
->each(function ($change, $key) use ($event_type, $model) {
5058
$log = new static();
5159
$log->event_type = $event_type;
52-
$log->subject_id = $model->getKey();
5360
$log->occurred_at = now();
5461

62+
foreach ($model->getAuditLogForeignKeyColumns() as $k => $v) {
63+
$log->setAttribute($k, $model->$v);
64+
}
65+
5566
if (config('model-auditlog.enable_user_foreign_keys')) {
5667
$log->user_id = \Auth::{config('model-auditlog.auth_id_function', 'id')}();
5768
}
5869

5970
$log->setAttribute('field_name', $key);
6071
$log->setAttribute('field_value_old', $model->getOriginal($key));
6172
$log->setAttribute('field_value_new', $change);
73+
74+
$log->attributes;
6275
$log->save();
6376
});
6477
}
6578

79+
/**
80+
* @param int $event_type
81+
* @param $model
82+
* @param string $relationName
83+
* @param array $pivotIds
84+
*/
85+
public function recordPivotChanges(int $event_type, $model, string $relationName, array $pivotIds) : void
86+
{
87+
$pivot = $model->{$relationName}()->getPivotClass();
88+
89+
$changes = $this->getPivotChanges($pivot, $model, $pivotIds);
90+
91+
foreach ($changes as $change) {
92+
$this->savePivotChanges(
93+
$this->passingChanges($change, $model),
94+
$event_type,
95+
(new $pivot())
96+
);
97+
}
98+
}
99+
100+
/**
101+
* @param $pivot
102+
* @param $model
103+
* @param $pivotIds
104+
*
105+
* @return array
106+
*/
107+
public function getPivotChanges($pivot, $model, array $pivotIds) : array
108+
{
109+
$changes = [];
110+
foreach ((new $pivot())->getAuditLogForeignKeyColumns() as $auditColumn => $pivotColumn) {
111+
foreach ($pivotIds as $id => $pivotId) {
112+
if ($pivotColumn !== $model->getForeignKey()) {
113+
$changes[$id][$auditColumn] = $pivotId;
114+
} else {
115+
$changes[$id][$auditColumn] = $model->getKey();
116+
}
117+
}
118+
}
119+
120+
return $changes;
121+
}
122+
123+
/**
124+
* @param Collection $passing_changes
125+
* @param int $event_type
126+
* @param $pivot
127+
*/
128+
public function savePivotChanges(Collection $passing_changes, int $event_type, $pivot) : void
129+
{
130+
$passing_changes
131+
->each(function ($change, $key) use ($event_type, $passing_changes, $pivot) {
132+
$log = $pivot->getAuditLogModelInstance();
133+
$log->event_type = $event_type;
134+
$log->occurred_at = now();
135+
136+
foreach ($passing_changes as $k => $v) {
137+
$log->setAttribute($k, $v);
138+
}
139+
140+
if (config('model-auditlog.enable_user_foreign_keys')) {
141+
$log->user_id = \Auth::{config('model-auditlog.auth_id_function', 'id')}();
142+
}
143+
144+
$log->setAttribute('field_name', $key);
145+
$log->setAttribute('field_value_old', $change);
146+
$log->setAttribute('field_value_new', null);
147+
148+
$log->attributes;
149+
$log->save();
150+
});
151+
}
152+
153+
/**
154+
* @param int $event_type
155+
* @param $model
156+
*
157+
* @return array
158+
*/
159+
public static function getChangesByType(int $event_type, $model) : array
160+
{
161+
switch ($event_type) {
162+
case EventType::CREATED:
163+
return $model->getAttributes();
164+
break;
165+
case EventType::RESTORED:
166+
return $model->getChanges();
167+
break;
168+
case EventType::FORCE_DELETED:
169+
return []; // if force deleted we want to stop execution here as there would be nothing to correlate records to
170+
break;
171+
default:
172+
return $model->getDirty();
173+
break;
174+
}
175+
}
176+
66177
/**
67178
* @return BelongsTo|null
68179
*/

src/Observers/AuditLogObserver.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function deleted($model) : void
3434
* If a model is hard deleting, either via a force delete or that model does not implement
3535
* the SoftDeletes trait we should tag it as such so logging doesn't occur down the pipe.
3636
*/
37-
if (! method_exists($model, 'isForceDeleting') || $model->isForceDeleting()) {
37+
if ((! method_exists($model, 'isForceDeleting') || $model->isForceDeleting())) {
3838
$event = EventType::FORCE_DELETED;
3939
}
4040

@@ -51,6 +51,17 @@ public function restored($model) : void
5151
->recordChanges(EventType::RESTORED, $model);
5252
}
5353

54+
/**
55+
* @param $model
56+
* @param $relationName
57+
* @param $pivotIds
58+
*/
59+
public function pivotDetached($model, string $relationName, array $pivotIds)
60+
{
61+
$this->getAuditLogModel($model)
62+
->recordPivotChanges(EventType::PIVOT_DELETED, $model, $relationName, $pivotIds);
63+
}
64+
5465
/**
5566
* Returns an instance of the AuditLogModel for the specific
5667
* model you provide.

src/Traits/AuditLoggable.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,36 @@ public function getAuditLogIgnoredFields() : array
5353
return [];
5454
}
5555

56+
/**
57+
* Get fields that should be used as keys on the auditlog for this model.
58+
*
59+
* @return array
60+
*/
61+
public function getAuditLogForeignKeyColumns() : array
62+
{
63+
return ['subject_id' => $this->getKeyName()];
64+
}
65+
66+
/**
67+
* Get the columns used in the foreign key on the audit log table.
68+
*
69+
* @return array
70+
*/
71+
public function getAuditLogForeignKeyColumnKeys() : array
72+
{
73+
return array_keys($this->getAuditLogForeignKeyColumns());
74+
}
75+
76+
/**
77+
* Get the columns used in the unique index on the model table.
78+
*
79+
* @return array
80+
*/
81+
public function getAuditLogForeignKeyColumnValues() : array
82+
{
83+
return array_values($this->getAuditLogForeignKeyColumns());
84+
}
85+
5686
/**
5787
* Get the audit logs for this model.
5888
*

0 commit comments

Comments
 (0)