יובל קוג'מן (Yuval Kogman) > Class-Workflow-0.11 > Class::Workflow::Cookbook

Download:
Class-Workflow-0.11.tar.gz

Annotate this POD

CPAN RT

Open  0
View/Report Bugs
Source   Latest Release: Class-Workflow-0.11_01

NAME ^

Class::Workflow::Cookbook - Common recipes with Class::Workflow.

DESCRIPTION ^

Class::Workflow is a generic, abstract system. This document is supposed to fill the gap between that and the practical, for a few simple examples.

ADDING STATE TO AN OBJECT ^

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:

Delegate instance

This is my favourite method. Basically your own derivative of Class::Workflow::Instance 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.

Translation

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 Class::Workflow::Util::Delta object, which computes the changed Moose attributes from an instance to a derived instance (multiple levels of ancestry are allowed).

Data-less instance

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 Class::Workflow::Transition::Validate::Simple 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.

SERIALIZATION ^

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->get_all_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 Digest::HMAC or something like that to authenticate the data, or the user could inject data into your workflow.

PERSISTENCE ^

KiokuDB

For KiokuDB based storage you needn't do anything to get started.

You probably want to add the KiokuDB::Lazy trait to the various attributes to prevent loading of unnecessary objects.

General concerns

There are two levels of persistence you could choose from. The more trivial one is that of the workflow state data.

This means that the workflow definition (states and transitions) is defined and from some static location every time the program starts (probably it's own .pm file, or Class::Workflow::YAML), but the Class::Workflow::Instance objects live in a database. The links to the states must be inflated using the static workflow definition.

The other way would be storing the entire workflow definition in persistent storage as well (all the state and transition definitions).

DBIx::Class

Storing Instances

There are two general approaches for storing Class::Workflow::Instance 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 Class::Workflow::Util::Delta 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.

Storing Everything

If you'd like to additionally have an editable, persistent workflow definition in the database, look at examples/dbic.

MULTIPLE INSTANCES ^

Complex workflows may involve branch and sync points for instances.

Branching is trivial due to the purely functional design of Class::Workflow::Instance:

        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 Class::Workflow.

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 on the CPAN but will probably not work out of the box.

If a concrete pattern does emerge from your work please feel free to submit it to the cookbook, release it on the CPAN, etc.

syntax highlighting: