Skip to content

Commit

Permalink
Merge pull request #601 from ReeceM/feature/log-json-attributes
Browse files Browse the repository at this point in the history
Feature/log json attributes
  • Loading branch information
Gummibeer authored Oct 6, 2019
2 parents a63f6c3 + 76b9810 commit a28239b
Show file tree
Hide file tree
Showing 4 changed files with 383 additions and 0 deletions.
78 changes: 78 additions & 0 deletions docs/advanced-usage/logging-model-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,84 @@ class NewsItem extends Model
Changing only `name` means only the `name` attribute will be logged in the activity, and `text` will be left out.



## Logging only a specific JSON attribute sub-key

If you would like to log only the changes to a specific JSON objects sub-keys. You can use the same method for logging specific columns with the difference of choosing the json key to log.

```php
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity;

class NewsItem extends Model
{
use LogsActivity;

protected $fillable = ['preferences', 'name'];

protected static $logAttributes = ['preferences->notifications->status', 'preferences->hero_url'];

protected $casts = [
'preferences' => 'collection' // casting the JSON database column
];
}
```

Changing only `preferences->notifications->status` or `preferences->hero_url` means only the `preferences->notifications->status` or `preferences->hero_url` attribute will be logged in the activity, and everything else `preferences` will be left out.

The output of this in a activity entry would be as follows:

```php
// Create a news item.
$newsItem = NewsItem::create([
'name' => 'Title',
'preferences' => [
'notifications' => [
'status' => 'on',
],
'hero_url' => ''
],
]);

// Update the json object
$newsItem->update([
'preferences' => [
'notifications' => [
'status' => 'on',
],
'hero_url' => 'http://example.com/hero.png'
],
]);

$lastActivity = Activity::latest()->first();

$lastActivity->properties->toArray();
```

```php
// output
[
"attributes" => [
"preferences" => [ // the updated values
"notifications" => [
"status" => "on",
],
"hero_url" => "http://example.com/hero.png",
],
],
"old" => [
"preferences" => [ // the old settings
"notifications" => [
"status" => "off",
],
"hero_url" => "",
],
],
]
```

The result in the log entry key for the attribute will be what is in the `$logAttributes`.

## Prevent save logs items that have no changed attribute

Setting `$submitEmptyLogs` to `false` prevents the package from storing empty logs. Storing empty logs can happen when you only want to log a certain attribute but only another changes.
Expand Down
16 changes: 16 additions & 0 deletions src/Traits/DetectsChanges.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Spatie\Activitylog\Traits;

use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Exceptions\CouldNotLogChanges;
Expand Down Expand Up @@ -125,6 +126,12 @@ public static function logChanges(Model $model): array
foreach ($attributes as $attribute) {
if (Str::contains($attribute, '.')) {
$changes += self::getRelatedModelAttributeValue($model, $attribute);
} elseif (Str::contains($attribute, '->')) {
Arr::set(
$changes,
str_replace('->', '.', $attribute),
static::getModelAttributeJsonValue($model, $attribute)
);
} else {
$changes[$attribute] = $model->getAttribute($attribute);

Expand Down Expand Up @@ -156,4 +163,13 @@ protected static function getRelatedModelAttributeValue(Model $model, string $at

return ["{$relatedModelName}.{$relatedAttribute}" => $relatedModel->$relatedAttribute ?? null];
}

protected static function getModelAttributeJsonValue(Model $model, string $attribute)
{
$path = explode('->', $attribute);
$modelAttribute = array_shift($path);
$modelAttribute = collect($model->getAttribute($modelAttribute));

return data_get($modelAttribute, implode('.', $path));
}
}
238 changes: 238 additions & 0 deletions tests/DetectsChangesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,244 @@ public function it_can_use_nullable_date_as_loggable_attributes()
$this->assertEquals($expectedChanges, $this->getLastActivity()->changes()->toArray());
}

/** @test */
public function it_can_store_the_changes_of_json_attributes()
{
$articleClass = new class() extends Article {
protected static $logAttributes = ['name', 'json->data'];
public static $logOnlyDirty = true;
protected $casts = [
'json' => 'collection',
];

use LogsActivity;
};

$article = new $articleClass();
$article->json = ['data' => 'test'];
$article->name = 'I am JSON';
$article->save();

$expectedChanges = [
'attributes' => [
'name' => 'I am JSON',
'json' => [
'data' => 'test',
],
],
];

$changes = $this->getLastActivity()->changes()->toArray();

$this->assertSame($expectedChanges, $changes);
}

/** @test */
public function it_will_not_store_changes_to_untracked_json()
{
$articleClass = new class() extends Article {
protected static $logAttributes = ['name', 'json->data'];
public static $logOnlyDirty = true;
protected $casts = [
'json' => 'collection',
];

use LogsActivity;
};

$article = new $articleClass();
$article->json = ['unTracked' => 'test'];
$article->name = 'a name';
$article->save();

$article->name = 'I am JSON';
$article->json = ['unTracked' => 'different string'];
$article->save();

$expectedChanges = [
'attributes' => [
'name' => 'I am JSON',
],
'old' => [
'name' => 'a name',
],
];

$changes = $this->getLastActivity()->changes()->toArray();

$this->assertSame($expectedChanges, $changes);
}

/** @test */
public function it_will_return_null_for_missing_json_attribute()
{
$articleClass = new class() extends Article {
protected static $logAttributes = ['name', 'json->data->missing'];
public static $logOnlyDirty = true;
protected $casts = [
'json' => 'collection',
];

use LogsActivity;
};

$jsonToStore = [];

$article = new $articleClass();
$article->json = $jsonToStore;
$article->name = 'I am JSON';
$article->save();

data_set($jsonToStore, 'data.missing', 'I wasn\'t here');

$article->json = $jsonToStore;
$article->save();

$expectedChanges = [
'attributes' => [
'json' => [
'data' => [
'missing' => 'I wasn\'t here',
],
],
],
'old' => [
'json' => [
'data' => [
'missing' => null,
],
],
],
];

$changes = $this->getLastActivity()->changes()->toArray();

$this->assertSame($expectedChanges, $changes);
}

/** @test */
public function it_will_return_an_array_for_sub_key_in_json_attribute()
{
$articleClass = new class() extends Article {
protected static $logAttributes = ['name', 'json->data'];
public static $logOnlyDirty = true;
protected $casts = [
'json' => 'collection',
];

use LogsActivity;
};

$jsonToStore = [
'data' => [
'data_a' => 1,
'data_b' => 2,
'data_c' => 3,
'data_d' => 4,
'data_e' => 5,
],
];

$article = new $articleClass();
$article->json = $jsonToStore;
$article->name = 'I am JSON';
$article->save();

data_set($jsonToStore, 'data.data_c', 'I Got The Key');

$article->json = $jsonToStore;
$article->save();

$expectedChanges = [
'attributes' => [
'json' => [
'data' => [
'data_a' => 1,
'data_b' => 2,
'data_c' => 'I Got The Key',
'data_d' => 4,
'data_e' => 5,
],
],
],
'old' => [
'json' => [
'data' => [
'data_a' => 1,
'data_b' => 2,
'data_c' => 3,
'data_d' => 4,
'data_e' => 5,
],
],
],
];

$changes = $this->getLastActivity()->changes()->toArray();

$this->assertSame($expectedChanges, $changes);
}

/** @test */
public function it_will_access_further_than_level_one_json_attribute()
{
$articleClass = new class() extends Article {
protected static $logAttributes = ['name', 'json->data->can->go->how->far'];
public static $logOnlyDirty = true;
protected $casts = [
'json' => 'collection',
];

use LogsActivity;
};

$jsonToStore = [];
// data_set($jsonToStore, 'data.can.go.how.far', 'Data');

$article = new $articleClass();
$article->json = $jsonToStore;
$article->name = 'I am JSON';
$article->save();

data_set($jsonToStore, 'data.can.go.how.far', 'This far');

$article->json = $jsonToStore;
$article->save();

$expectedChanges = [
'attributes' => [
'json' => [
'data' => [
'can' => [
'go' => [
'how' => [
'far' => 'This far',
],
],
],
],
],
],
'old' => [
'json' => [
'data' => [
'can' => [
'go' => [
'how' => [
'far' => null,
],
],
],
],
],
],
];

$changes = $this->getLastActivity()->changes()->toArray();

$this->assertSame($expectedChanges, $changes);
}

protected function createArticle(): Article
{
$article = new $this->article();
Expand Down
Loading

0 comments on commit a28239b

Please sign in to comment.