Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save menjaraz/7ba1d96926827d9cfd0b to your computer and use it in GitHub Desktop.
Save menjaraz/7ba1d96926827d9cfd0b to your computer and use it in GitHub Desktop.

FiniteAuditTrail Trait

Support for keeping an audit trail for any state machine, borrowed from the Ruby StateMachine audit trail gem. Having an audit trail gives you a complete history of the state changes in your model. This history allows you to investigate incidents or perform analytics, like: “How long does it take on average to go from state a to state b?”, or “What percentage of cases goes from state a to b via state c?”

Requirements

  1. PHP >= 5.4
  2. Laravel 4 framework
  3. Install Finite package
  4. Use FiniteStateMachine in your Eloquent model

Usage

In your Stateful Class use this trait after FiniteStateMachine trait, like this use FiniteAuditTrail;. Then call initAuditTrail() method at the end of initialization (__contruct() method) after initStateMachine() and parent::__construct() methods call. Then create or complete the static boot() method in your model like this:

<?php
    public static function boot()
    {
        parent::boot();
        static::finiteAuditTrailBoot();
    }

Example

Check MyStatefulModel.php, MyStatefulModelStateTransition.php and Usage.php.

And the migration files: 2013_10_01_124809_create_my_stateful_models_table.php and 2013_10_01_125509_create_my_stateful_model_state_transitions_table.

You should migrate your database with this command:

php artisan migrate

License

MIT

Use At Your Own Risk

<?php
// app/database/migrations/2013_10_01_124809_create_my_stateful_models_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateMyStatefulModelsTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('my_stateful_models', function(Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->text('body');
$table->string('state')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('my_stateful_models');
}
}
<?php
// app/database/migrations/2013_10_01_125509_create_my_stateful_model_state_transitions_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateMyStatefulModelStateTransitionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('my_stateful_model_state_transitions', function(Blueprint $table) {
$table->increments('id');
$table->string('event')->nullable();
$table->string('from')->nullable();
$table->string('to');
// $table->string('custom_field')->nullable();
$table->integer('my_stateful_model_id');
// $table->foreign('my_stateful_model_id')->references('id')->on('my_stateful_models');//Optional foreign key
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('my_stateful_model_state_transitions');
}
}
<?php
use Finite\StateMachine\StateMachine;
/**
* The FiniteAuditTrail Trait.
* This plugin for the Finite package (see https://github.com/yohang/Finite) adds support for keeping an audit trail for any state machine.
* Having an audit trail gives you a complete history of the state changes in your stateful model.
* Prerequisites:
* 1. Install Finite package (https://github.com/yohang/Finite#readme)
* 2. Use FiniteStateMachine in your model (https://gist.github.com/tortuetorche/6365575)
* Usage: in your Stateful Class use this trait after FiniteStateMachine trait, like this "use FiniteAuditTrail;".
* Then call initAuditTrail() method at the end of initialization (__contruct() method) after initStateMachine() and parent::__construct() call.
* Then create or complete the static boot() method in your model like this:
*
* public static function boot()
* {
* parent::boot();
* static::finiteAuditTrailBoot();
* }
*
* @author Tortue Torche <tortuetorche@spam.me>
*/
trait FiniteAuditTrail
{
public static function finiteAuditTrailBoot()
{
static::saveInitialState();
}
protected static function saveInitialState()
{
static::created(function($model) {
$transition = new Finite\Transition\Transition(null, null, $model->findInitialState());
$model->storeAuditTrail($model, $transition, false);
});
}
protected $auditTrailModel;
protected $auditTrailAttributes;// We can't set an empty array as default value here, maybe a PHP Trait bug ?
/**
* @param mixed|array|args $args
* if $args is an array:
* initAuditTrail(['to' => 'ModelAuditTrail', 'attributes' => ['name', 'email']]);
* else: initAuditTrail('ModelAuditTrail', ['name', 'email']);
* first param: string $to Model name who stores the history
* second param: string|array $attributes Attribute(s) or method(s) from stateful model to save
*/
protected function initAuditTrail($args = null)
{
// Default options
$options = [ 'attributes' => (array) $this->auditTrailAttributes, 'to' => "\\".get_called_class()."StateTransition" ];
if (func_num_args() === 2) {
$args = func_get_args();
list($options['to'], $options['attributes']) = $args;
} elseif ( func_num_args() === 1) {
if (is_array($args)) {
$newOptions = array_extract_options($args);
if(empty($newOptions)) {
$options['attributes'] = $args;
} else {
$options = array_merge($options, $newOptions);
}
} elseif ( is_string($args) ) {
$options['to'] = $args;
}
}
$this->auditTrailAttributes = (array) $options['attributes'];
$this->auditTrailModel = $options['to'];
$this->addAfter([$this, 'storeAuditTrail']);
}
/**
* Create a new model instance that is existing.
*
* @param array $attributes
* @return \Illuminate\Database\Eloquent\Model|static
*/
public function newFromBuilder($attributes = [])
{
$instance = parent::newFromBuilder($attributes);
$this->restoreAuditTrail($instance);
return $instance;
}
/**
* @param \Illuminate\Database\Eloquent\Model|static $instance
*/
protected function restoreAuditTrail($instance)
{
$instance->getStateMachine()->initialize();
}
/**
* @param object $self
* @param Finite\Event\TransitionEvent|Finite\Transition\Transition $transitionEvent
* @param boolean $save Optional, default: true
*/
public function storeAuditTrail($self, $transitionEvent, $save = true)
{
if ($save === true || $this->exists === false) {
$this->save();
}
if ($transitionEvent instanceof Finite\Event\TransitionEvent) {
$transition = $transitionEvent->getTransition();
} else {
$transition = $transitionEvent;
}
$values = [];
$values['event'] = $transition->getName();
$initialStates = $transition->getInitialStates();
if (! empty($initialStates)) {
$values['from'] = $transitionEvent->getInitialState()->getName();
}
$values['to'] = $transition->getState();
$values[snake_case(get_called_class()).'_id'] = $this->getKey();
$auditTrail = new $this->auditTrailModel;
foreach ((array) $this->auditTrailAttributes as $attribute) {
if ($this->getAttribute($attribute)) {
$values[$attribute] = $this->getAttribute($attribute);
}
}
$auditTrail->fill($values);
$auditTrail->save();
}
}
<?php
// app/models/MyStatefulModel.php
class MyStatefulModel extends Eloquent implements Finite\StatefulInterface
{
use FiniteStateMachine;
use FiniteAuditTrail;
public static function boot()
{
parent::boot();
static::finiteAuditTrailBoot();
}
public function __construct($attributes = [])
{
$this->initStateMachine();
parent::__construct($attributes);
$this->initAuditTrail();
}
protected function stateMachineConfig()
{
return [
'states' => [
's1' => [
'type' => 'initial',
'properties' => ['deletable' => true, 'editable' => true],
],
's2' => [
'type' => 'normal',
'properties' => [],
],
's3' => [
'type' => 'final',
'properties' => [],
]
],
'transitions' => [
't12' => ['from' => ['s1'], 'to' => 's2'],
't23' => ['from' => ['s2'], 'to' => 's3'],
't21' => ['from' => ['s2'], 'to' => 's1'],
],
'callbacks' => [
'before' => [
['on' => 't12', 'do' => [$this, 'beforeTransitionT12']],
['from' => 's2', 'to' => 's3', 'do' => function($myStatefulInstance, $transitionEvent) {
echo "Before callback from 's2' to 's3'";// debug
}],
['from' => '-s3', 'to' => ['s3' ,'s1'], 'do' => [$this, 'fromStatesS1S2ToS1S3']],
],
'after' => [
['from' => 'all', 'to' => 'all', 'do' => [$this, 'afterAllTransitions']],
],
],
];
}
public function beforeTransitionT12($myStatefulInstance, $transitionEvent)
{
echo "Function called before transition: '".$transitionEvent->getTransition()->getName()."' !";// debug
}
public function fromStatesS1S2ToS1S3()
{
echo "Before callback from states 's1' or 's2' to 's1' or 's3'";// debug
}
public function afterAllTransitions($myStatefulInstance, $transitionEvent)
{
echo "After All Transitions !";// debug
}
}
<?php
// app/models/MyStatefulModelStateTransition.php
class MyStatefulModelStateTransition extends Eloquent
{
protected $guarded = ['id', 'created_at', 'updated_at'];
public static function boot()
{
parent::boot();
// Pro tips:
static::creating(function($model) {
// You can save fields/additional attributes in your database here:
// $model->custom_field = "Hello world!";
});
}
}
<?php
$myStatefulObject = new MyStatefulModel;
$myStatefulObject->getState(); // → "s1"
$myStatefulObject->can('t23'); // → false
$myStatefulObject->can('t12'); // → true
$myStatefulObject->apply('t12'); // $myStatefulObject is saved in the database
$myStatefulObject->is('s2'); // → true
MyStatefulModel::orderBy('id', 'desc')->first()->getState();// → "s2"
MyStatefulModel::orderBy('id', 'desc')->first()->is('s2'); // → true
MyStatefulModelStateTransition::orderBy('id', 'desc')->first()->event;// → "t12"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment