Class::Workflow::Cookbook - Common recipes with L<Class::Workflow>. |
Class::Workflow::Cookbook - Common recipes with the Class::Workflow manpage.
the Class::Workflow manpage is a generic, abstract system. This document is supposed to fill the gap between that and the practical, for a few simple examples.
The most common usage for workflows is adding arbitrary, complex state to data objects, thus making them stateful. A canonical example of a data object is an issue tracker ticket. Its fields are changed over the course of it's lifetime, but must be changed within a very specific workflow.
There are several approaches for this:
This is my favourite method. Basically your own derivative of the Class::Workflow::Instance manpage is a delegate of your item:
warn "The ticket is: " . $ticket->workflow_instance->state->name; # the state
Applying transitions amounts to:
my $instance = $ticket->workflow_instance;
my $new_instance = $transition->apply( $instance, @args );
$ticket->workflow_instance($new_instance);
With the history encapsulated in $ticket->workflow_instance->prev
.
This is easily captured in a pattern:
sub apply_transition { my ( $self, $transition, @args ) = @_;
$self->_workflow_txn(sub{ my ( $self, $instance ) = @_; $transition->apply( $instance, @args ); }); }
sub _workflow_txn { my ( $self, $sub ) = @_;
my $new_instance = eval { $self->$sub($self->workflow_instance) };
if ( defined $new_instance ) { $self->workflow_instance($new_instance); } elsif ( $@ ) { die $@; } else { die "$sub did not return a new workflow instance"; } }
See examples/dbic for a more complicated implementation of _workflow_txn
,
tying in database transactions.
In this pattern the fields of the instance are translated back to the item, mutating it.
Here is a trivial example:
my $instance = MyInstance->new( $ticket->get_fields, # return all the fields of the ticket state => $ticket->state, # assumes ->state returns an object );
my $new_instance = $transition->apply( $instance, @args );
$ticket->set_fields( $instance->get_fields ); $ticket->state( $instance->state );
Keeping an audit log becomes a little tricker here, but might be eased by the the Class::Workflow::Util::Delta manpage object, which computes the changed Moose attributes from an instance to a derived instance (multiple levels of ancestry are allowed).
Yet another way is to just not use the workflow as a data object, instead using it just to keep state.
This way requires the least code changes from the items you are adding statefulness to, but provides the least value back.
my $take_ownership = Class::Workflow::Transition::Simple->new( to_state => $taken, body => sub { my ( $self, %args ) = @_;
my $ticket = $args->{ticket};
$ticket->set_owner( $args{user} ); } );
# ...
my $instance = MyInstance->new( state => $ticket->state );
my $new_instance = $take_ownership->apply( $instance, ticket => $ticket, user => $user, );
# the ticket is now owned by $user $ticket->state( $new_instance->state );
This is useful when you are more concerned about the validation of the control flow. Look into the Class::Workflow::Transition::Validate::Simple manpage for facilities that may interest you.
Using that role validation and application become two somewhat distinct steps, and errors in validation prevent actual application from happening, allowing a consistent state to be preserved.
Serialzing your instances is a problem related to PERSISTENCE.
However, there are more solutions than just keeping the instance in the database.
If your workflow is dynamic and the transition bodies are not closure
attributes of the transition class, but a real method, then theoretically you
can just serialize the instance, and the workflow definition will be serialized
along side it due to the links to state
, transition
and prev
.
Usually this solution isn't so useful though, and you want just the instance definitions, so the best solution is to go through the instance fields and just serialize that:
sub unpack_instance { my ( $self, $instance ) = @_; my %attrs = map { $_->name => $_ } instance->meta->compute_all_applicable_attributes; my ( $state, $transition, $prev ) = delete @attrs{qw(state transition prev)}; return $self->__deflate_workflow_instance( $instance, $state, $transition, $prev, values %attrs ); }
sub _unpack_instance { my ( $self, $instance, @attrs ) = @_; my ( $state, $transition, $prev, @reg_attrs ) = @attrs; return unless $instance; $state = $state->get_value($instance); $transition = $transition->get_value($instance); $prev = $self->__deflate_workflow_instance( $prev->get_value($instance), @attrs ); return { $prev ? ( prev => $prev ) : (), $state ? ( state => $state->name ) : (), $transition ? ( transition => $transition->name ) : (), map { $_->name => $_->get_value($instance) } @reg_attrs } }
sub pack_instance => as { my ( $self, $instance, $wf ) = @_; return unless $instance; my $prev = $self->_inflate_workflow_instance( $instance->{prev}, $wf );
$wf->instance_class->new( %$instance, $prev ? ( prev => $prev ) : (), $instance->{state} ? ( state => $wf->get_state( $instance->{state} ) ) : (), $instance->{transition} ? ( transition => $wf->get_transition( $instance->{transition} ) ) : (), ); }
If you're doing this in a web application make sure to use the Digest::HMAC manpage or something like that to authenticate the data, or the user could inject data into your workflow.
There are two levels of persistence you could be concerned about. The first, more trivial one is that of the workflow data.
This means that the workflow definition (states and transitions) is defined and
redefined from some static location every time the program starts (probably
it's own .pm
file, or the Class::Workflow::YAML manpage), but the
the Class::Workflow::Instance manpage objects live in a database.
There are two general approaches for storing the Class::Workflow::Instance manpage type objects in a data store of some sort.
See examples/dbic for actual working code.
If you elected to use a workflow instance delegate for your data items, which presumably also live in the database, then a simple relationship where the item has a foreign key pointing to the current workflow instance in the instances table is going to do the trick.
In this case you have the full instance history preserved in the instances table.
An alternative approach is to maintain just the current instances live, and use the Class::Workflow::Util::Delta manpage to write changes to an audit log instead, skipping unchanged fields.
When the data is saved to the database, the chain of workflow instances is walked and the delta between each one is computed, and saved to the log.
Then the workflow instance is overwritten with the new data.
When the data is loaded prev
is undefined, but could be lazily reconstructed
from the audit log if necessary.
This solution is more compact on disk but involves a lot more work.
If you'd like to additionally have an editable, persistent workflow definition in the database, look at examples/dbic.
Complex workflows may involve branch and sync points for instances.
Branching is trivial due to the purely functional design of the Class::Workflow::Instance manpage:
my $instance = ...;
my $branch_a_instance = $transition_a->apply( $instance, ... );
my $branch_b_instance = $transition_b->apply( $instance, ... );
$branch_b_instance->prev == $branch_a_instance->prev == $instance;
There are no built in facilities for synchronization though. The process generally involves two instances converging on a single state:
my ( $instance_a, $instance_b ) = ...; # two separate instances
my $new_a = $transition_a->apply( $instance_a, ... );
my $new_b = $transition_b->apply( $instance_b, ... );
$new_a == $new_b; # the transitions point to the same state
my $merged = $some_helper->merge( $new_a, $new_b );
this is very specific to your changing needs, so no reusable solution is packaged with the Class::Workflow manpage.
The simplest way to go about this is to make a custom type of instance that
provides multiple prev
entries.
Merging of fields is yet another concern. 3 way merge algorithms exist but will probably not naively work.
If a concrete pattern does emerge from your work please feel free to submit it to the cookbook, release it on the CPAN, etc.
Class::Workflow::Cookbook - Common recipes with L<Class::Workflow>. |