panoscape / history
Eloquent model history tracking for Laravel
Installs: 102 348
Dependents: 0
Suggesters: 0
Security: 0
Stars: 161
Watchers: 4
Forks: 25
Open Issues: 1
Requires
- php: ^7.2|^7.2.5|^7.3|^8.0|^8.1|^8.2
- illuminate/support: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
Requires (Dev)
- mockery/mockery: ^1.2
- orchestra/testbench: ^4.8|^5.2|^6.2|^7.5|^8.0|^9.0
- php-coveralls/php-coveralls: ^2.1
- phpunit/phpunit: ^8.3|^8.4|^9.0|^10.5
README
History
Eloquent model history tracking for Laravel
Installation
Composer
Laravel 6.x and above
composer require panoscape/history
Laravel 5.6.x
composer require "panoscape/history:^1.0"
Service provider and alias
Only required for Laravel 5.6.x
config/app.php
'providers' => [ ... Panoscape\History\HistoryServiceProvider::class, ]; 'aliases' => [ ... 'App\History' => Panoscape\History\History::class, ];
Migration
php artisan vendor:publish --provider="Panoscape\History\HistoryServiceProvider" --tag=migrations
Config
php artisan vendor:publish --provider="Panoscape\History\HistoryServiceProvider" --tag=config
Localization
php artisan vendor:publish --provider="Panoscape\History\HistoryServiceProvider" --tag=translations
Usage
Add HasOperations
trait to user model that performs operations.
<?php namespace App; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\SoftDeletes; use Panoscape\History\HasOperations; class User extends Authenticatable { use Notifiable, SoftDeletes, HasOperations; }
Add HasHistories
trait to the model that will be tracked.
<?php namespace App; use Illuminate\Database\Eloquent\Model; use Panoscape\History\HasHistories; class Article extends Model { use HasHistories; public function getModelLabel() { return $this->display_name; } }
Remember that you'll need to implement the abstract getModelLabel
method from the trait.
This provides the model instance's display name in histories (as Who
in Who did what
).
Get histories of a model
$model->histories(); //or dynamic property $model->histories;
Get operations of a user
$user->operations(); //or dynamic property $user->operations;
Additional query conditions
Both histories
and operations
return Eloquent relationships which also serve as query builders. You can add further constraints by chaining conditions:
// get the lastest 10 records $model->histories()->orderBy('performed_at', 'desc')->take(10) // filter by user id $model->histories()->where('user_id', 10010)
History
//get the associated model $history->model(); //get the associated user //the user is the authenticated user when the action is being performed //it might be null if the history is performed unauthenticatedly $history->user(); //check user existence $history->hasUser(); //get the message $history->message; //get the meta(only available when it's an updating operation) //the meta will be an array with the properties changing information $history->meta; //get the timestamp the action was performed at $history->performed_at;
Example message
Created Project my_project
│ │ │
│ │ └───── instance name(returned from `getModelLabel`)
│ └─────────────── model name(class name or localized name)
└─────────────────────── event name(default or localized name)
Example meta
[ ['key' => 'name', 'old' => 'myName', 'new' => 'myNewName'], ['key' => 'age', 'old' => 10, 'new' => 100], ... ]
Custom history
Besides the built in created/updating/deleting/restoring
events, you may track custom history record by firing an ModelChanged
event.
use Panoscape\History\Events\ModelChanged; ... //fire a model changed event event(new ModelChanged($user, 'User roles updated', $user->roles()->pluck('id')->toArray()));
The ModelChanged
constructor accepts two/three/four arguments. The first is the associated model instance; the second is the message; the third is optional, which is the meta(array); the fourth is also optional, being the translation key of the event(see Localization).
Localization
You may localize the model's type name.
To do that, add the language line to the models
array in the published language file, with the key being the class's base name in snake case.
Example language config
/* |-------------------------------------------------------------------------- | Tracker Language Lines |-------------------------------------------------------------------------- | | The following language lines are used across application for various | messages that we need to display to the user. You are free to modify | these language lines according to your application's requirements. | */ 'created' => '创建:model:label', 'updating' => 'actualizar :model :label', 'deleting' => ':model :label löschen', 'restored' => ':model:labelを復元', //you may add your own model name language line here 'models' => [ 'project' => '项目', 'component_template' => '组件模板', // 'model_base_name_in_snake_case' => 'translation', ]
This will translate your model history into
创建项目project_001
You can also translate custom history messages from ModelChanged
events
/* |-------------------------------------------------------------------------- | Tracker Language Lines |-------------------------------------------------------------------------- */ 'switched_role' => ':model switched role',
// if you specified the translation key, the message argument will be ignored, simply just pass `null` event(new ModelChanged($user, null, $user->roles()->pluck('id')->toArray()), 'switched_role');
Filters
You may set whitelist and blacklist in config file. Please follow the description guide in the published config file.
/* |-------------------------------------------------------------- | Events whitelist |-------------------------------------------------------------- | | Events in this array will be recorded. | Available events are: created, updating, deleting, restored | */ 'events_whitelist' => [ 'created', 'updating', 'deleting', 'restored', ], /* |-------------------------------------------------------------- | Attributes blacklist |-------------------------------------------------------------- | | Please add the whole class names. Example: \App\User:class | For each model, attributes in its respect array will NOT be recorded into meta when performing update operation. | */ 'attributes_blacklist' => [ // \App\User::class => [ // 'password' // ], ], /* |-------------------------------------------------------------- | User type blacklist |-------------------------------------------------------------- | | Operations performed by user types in this array will NOT be recorded. | Please add the whole class names. Example: \App\Admin:class | Use 'nobody' to bypass unauthenticated operations | */ 'user_blacklist' => [ // \App\Admin:class, // 'nobody' ], /* |-------------------------------------------------------------- | Enviroments blacklist |-------------------------------------------------------------- | | When application's environment is in the list, tracker will be disabled | */ 'env_blacklist' => [ // 'test' ],
Auth guards
If your users are using non-default auth guards, you might see all $history->hasUser()
become false
even though the history sources were generated by authenticated users.
To fix this, you'll need to enable custom auth guards scanning in config file:
/* |-------------------------------------------------------------- | Enable auth guards scanning |-------------------------------------------------------------- | | You only need to enable this if your users are using non-default auth guards. | In that case, all tracked user operations will be anonymous. | | - Set to `true` to use a full scan mode: all auth guards will be checked. However this does not ensure guard priority. | - Set to an array to scan only specific auth guards(in the given order). e.g. `['web', 'api', 'admin']` | */ 'auth_guards' => null
Custom meta
You can define your own method for meta data. By default for updating
event meta consists of modified keys and for other events meta is null
.
Just redefine the method getModelMeta
for the trait.
Example:
class Article extends Model { // if you want to use default trait method, you need to redeclare it with a new name use HasHistories { getModelMeta as protected traitGetModelMeta; }; ... public function getModelMeta($event) { // using defaults for updating if($event == 'updating') return $this->traitGetModelMeta($event); // passing full model to meta // ['key1' => 'value1', 'key2' => 'value2', ...] else return $this; } }
Known issues
- When updating a model, if its model label(attributes returned from
getModelLabel
) has been modified, the history message will use its new attributes, which might not be what you expect.
class Article extends Model { use HasHistories; public function getModelLabel() { return $this->title; } } // original title is 'my title' // modify title $article->title = 'new title'; $article->save(); // the updating history message // expect: Updating Article my title // actual: Updating Article new title
A workaround
public function getModelLabel() { return $this->getOriginal('title', $this->title); }