Class::Workflow - Light weight workflow system.



NAME

Class::Workflow - Light weight workflow system.


SYNOPSIS

        use Class::Workflow;
        # ***** NOTE *****
        #
        # This is a pretty long and boring example
        #
        # you probably want to see some flashy flash videos, so look in SEE ALSO
        # first ;-)
        #
        # ****************
        # a workflow object assists you in creating state/transition objects
        # it lets you assign symbolic names to the various objects to ease construction
        my $wf = Class::Workflow->new;
        # ( you can still create the state, transition and instance objects manually. )
        # create a state, and set the transitions it can perform
        $wf->state(
                name => "new",
                transitions => [qw/accept reject/],
        );
        # set it as the initial state
        $wf->initial_state("new");
        # create a few more states
        $wf->state(
                name => "open",
                transitions => [qw/claim_fixed reassign/],
        );
        $wf->state(
                name => "rejected",
        );
        # transitions move instances from state to state
        
        # create the transition named "reject"
        # the state "new" refers to this transition
        # the state "rejected" is the target state
        $wf->transition(
                name => "reject",
                to_state => "rejected",
        );
        # create a transition named "accept",
        # this transition takes a value from the context (which contains the current acting user)
        # the context is used to set the current owner for the bug
        $wf->transition(
                name => "accept",
                to_state => "opened",
                body => sub {
                        my ( $transition, $instance, $context ) = @_;
                        return (
                                owner => $context->user, # assign to the use who accepted it
                        );
                },
        );
        # hooks are triggerred whenever a state is entered. They cannot change the instance
        # this hook calls a hypothetical method on the submitter object
        $wf->state( "reject" )->add_hook(sub {
                my ( $state, $instance ) = @_;
                $instance->submitter->notify("Your item has been rejected");
        });
        # the rest of the workflow definition is omitted for brevity
        # finally, use this workflow in the action that handles bug creation
        sub new_bug {
                my ( $submitter, %params ) = @_;
                return $wf->new_instance(
                        submitter => $submitter,
                        %params,
                );
        }


DESCRIPTION

Workflow systems let you build a state machine, with transitions between states.


EXAMPLES

There are several examples in the examples directory, worth looking over to help you understand and to learn some more advanced things.

The most important example is probably how to store a workflow definition (the states and transitions) as well as the instances using the DBIx::Class manpage in a database.

Bug Tracker Example

One of the simplest examples of a workflow which you've probably used is a bug tracking application:

The initial state is 'new'

new
New bugs arrive here.
reject
This bug is not valid.

Target state: rejected.

accept
This bug needs to be worked on.

Target state: open.

rejected
This is the state where deleted bugs go, it has no transitions.

open
The bug is being worked on right now.
reassign
Pass the bug to someone else.

Target state: unassigned.

fixed
The bug looks fixed, and needs verifification.

Target state: awaiting_approval.

unassigned
The bug is waiting for a developer to take it.
take
Volunteer to handle the bug.

Target state: open.

awaiting_approval
The submitter needs to verify the bug.
resolved
The bug is resolved and can be closed.

Target state: closed

unresolved
The bug needs more work.

Target state: open

closed
This is, like rejected, an end state (it has no transitions).

If you read through this very simple state machine you can see that it describes the steps and states a bug can go through in a bug tracking system. The core of every workflow is a state machine.


INSTANCES

On the implementation side, the core idea is that every ``item'' in the system (in our example, a bug) has a workflow instance. This instance represents the current position of the item in the workflow, along with history data (how did it get here).

In this implementation, the instance is usually a consumer of the Class::Workflow::Instance manpage, typically the Class::Workflow::Instance::Simple manpage.

So, when you write your MyBug class, it should look like this (if it were written in Moose):

        package MyBug;
        use Moose;
        has workflow_instance => (
                does => "Class::Workflow::Instance", # or a more restrictive constraint
                is   => "rw",
        );

Since this system is purely functional (at least if your transitions are), you need to always set the instance after applying a transition.

For example, let's say you have a handler for the ``accept'' action, to change the instance's state it would do something like this:

        sub accept {
                my $bug = shift;
                my $wi = $bug->workflow_instance;
                my $current_state = $wi->state;
                # if your state supports named transitions      
                my $accept = $current_state->get_transition( "accept" )
                        or die "There's no 'accept' transition in the current state";
                my $wi_accepted = $accept->apply( $wi );
                $bug->workflow_instance( $wi_accepted );
        }


RESTRICTIONS

Now let's decsribe some restrictions on this workflow.

A workflow system will not only help in modelying the state machine, but also help you create restrictions on how states need to be changed, etc.

The implementation of restrictions is explained after the next section.


CONTEXTS

In order to implement these restrictions cleanly you normally use a context object (a default one is provided in the Class::Workflow::Context manpage but you can use anything).

This is typically the first (and sometimes only) argument to all transition applications, and it describes the context that the transition is being applied in, that is who is applying the transition, what are they applying it with, etc etc.

In our bug system we typically care about the user, and not much else.

Imagine that we have a user class:

        package MyUser;
        has id => (
                isa => "Num",
                is  => "ro",
                default => sub { next_unique_id() };
        );
        has name => (
                ...
        );

We can create a context like this:

        package MyWorkflowContext;
        use Moose;
        extends "Class::Workflow::Context";
        has user => (
                isa => "MyUser",
                is  => "rw",
        );

to contain the ``current'' user.

Then, when we apply the transition a bit differently:

        sub accept {
                my ( $bug, $current_user ) = @_;
                my $wi = $bug->workflow_instance;
                my $current_state = $wi->state;
                # if your state supports named transitions      
                my $accept = $current_state->get_transition( "accept" )
                        or croak "There's no 'accept' transition in the current state";
                my $c = MyWorkflowContext->new( user => $current_user );
                my $wi_accepted = $accept->apply( $wi, $c );
                $bug->workflow_instance( $wi_accepted );
        }

And the transition has access to our $c object, which references the current user.


IMPLEMENTING RESTRICTIONS

In order to implement the restrictions we specified above we need to know who the submitter and owner of the item are.

For this we create our own instance class as well:

        package MyWorkflowInstance;
        use Moose;
        extends "Class::Workflow::Instance::Simple";
        has owner => (
                isa => MyUser",
                is  => "ro", # all instance fields should be read only
        );
        has submitter => (
                isa => MyUser",
                is  => "ro", # all instance fields should be read only
        );

When the first instance is created the current user is set as the submitter.

Then, as transitions are applied they can check for the restrictions.

This is typically not done in the actual transition body, but rather in validation hooks. the Class::Workflow::Transition::Validate manpage provides a stanard hook, and the Class::Workflow::Transition::Simple manpage provides an even easier interface for this:

        my $fixed = Class::Workflow::Transition::Simple->new(
                name          => 'fixed',
                to_transition => $awaiting_approval,
                validators    => [
                        sub {
                                my ( $self, $instance, $c ) = @_;
                                die "Not owner" unless $self->instance->owner->id == $c->user->id;
                        },
                ],
                body => sub {
                        # ...
                },
        );


PERSISTENCE

Persistence in workflows involves saving the workflow instance as a relationship of the item whose state it represents, or even treating the instance as the actual item.

In any case, right now there are no persistence layers available. In the close future advanced the DBIx::Class manpage persistence of both workflow definitions and instances will be available.

For the mean while I suggest you simply serialize the instance object's fields. If you need history, serialize recursively with prev (although you may not want to do this every time).

Additionally the instance has the fields state and transition, which contain references to the current state and the transition which was applied to arrive at the current state. These two fields should probably be serialized as symbolic references (for example, $instance->state->name), unless you want a copy some of the workflow definitions store in the instance as well.


ROLES AND CLASSES

Most of the Class::Workflow system is implemented using roles to specify interfaces with reusable behavior, and then ::Simple classes which mash up a bunch of useful roles.

This means that you have a very large amount of flexibility in how you compose your state/transition objects, allowing good integration with most existing software.

This is achieved using Moose, specifically the Moose::Role manpage.


THIS CLASS

the Class::Workflow manpage objects are utility objects to help you create workflows and instances without worrying too much about the state and transition objects.

It's usage is overviewed in the SYNOPSIS section.


FIELDS

instance_class
state_class
transition_class
These are the classes to instantiate with.

They default to the Class::Workflow::Instance::Simple manpage, the Class::Workflow::State::Simple manpage and the Class::Workflow::Transition::Simple manpage.


METHODS

new_instance
Instantiate the workflow

initial_state
Set the starting state of instances.

states
transitions
Return all the registered states or transitions.

state_names
transition_names
Return all the registered state or transition names.

state
transition
These two methods create update or retrieve state or transition objects.

They have autovivification semantics for ease of use, and are pretty lax in terms of what they accept.

More formal methods are presented below.

They have several forms:

        $wf->state("foo"); # get (and maybe create) a new state with the name "foo"
        $wf->state( foo => $object ); # set $object as the state by the name "foo"
        $wf->state( $object ); # register $object ($object must support the ->name method )
        # create or update the state named "foo" with the following attributes:
        $wf->state(
                name       => "foo",
                validators => [ sub { ... } ],
        );
        # also works with implicit name:
        $wf->state( foo =>
                validators  => [ sub { ... } ],
        );

(wherever ->state is used ->transition can also be used).

Additionally, whenever you construct a state like this:

        $wf->state(
                name        => "foo",
                transitions => [qw/t1 t2/],
        );

the parameters are preprocessed so that it's as if you called:

        my @transitions = map { $wf->state($_) } qw/t1 t2/;
        $wf->state(
                name        => "foo",
                transitions => [@transitions],
        );

so you don't have to worry about creating objects first.

add_state $name, $object
add_transition $name, $object
Explicitly register an object by the name $name.

delete_state $name
delete_transition $name
Remove an object by the name $name.

Note that this will NOT remove the object from whatever other object reference it, so that:

        $wf->state(
                name        => "foo",
                transitions => ["bar"],
        );
        $wf->delete_transition("bar");

will not remove the object that was created by the name ``bar'' from the state ``foo'', it's just that the name has been freed.

Use this method with caution.

rename_state $old, $new
rename_transition $old, $new
Change the name of an object.

get_state $name
get_transition $name
Get the object by that name or return undef.

create_state $name, @args
create_transition $name, @args
Call construct_state or construct_transition and then add_state or add_transition with the result.

construct_state @args
construct_transition @args
Call ->new on the appropriate class.

expand_attrs \%attrs
This is used by create_or_set_state and create_or_set_transition, and will expand the attrs by the names to_state, transition and transitions to be objects instead of string names, hash or array references, by calling autovivify_transitions or autovivify_states.

In the future this method might be more aggressive, expanding suspect attrs.

autovivify_states @things
autovivify_transitions @things
Coerce every element in @things into an object by calling $wf->state($thing) or $wf->transition($thing).

create_or_set_state %attrs
create_or_set_transition %attrs
If the object by the name $attrs{name} exists, update it's attrs, otherwise create a new one.


SEE ALSO

Workflow - Chris Winters' take on workflows - it wasn't simple enough for me (factoring out the XML/factory stuff was difficult and I needed a much more dynamic system).

http://is.tm.tue.nl/research/patterns/ - lots of explanation and lovely flash animations.

the Class::Workflow::YAML manpage - load workflow definitions from YAML files.

the Class::Workflow::Transition::Simple manpage, the Class::Workflow::State::Simple manpage, the Class::Workflow::Instance::Simple manpage - easy, useful classes that perform all the base roles.

Moose


VERSION CONTROL

This module is maintained using Darcs. You can get the latest version from http://nothingmuch.woobling.org/Class-Workflow/, and use darcs send to commit changes.


AUTHOR

Yuval Kogman <nothingmuch@woobling.org>


COPYRIGHT & LICENSE

        Copyright (c) 2006-2008 Infinity Interactive, Yuval Kogman. All rights
        reserved. This program is free software; you can redistribute
        it and/or modify it under the same terms as Perl itself.
 Class::Workflow - Light weight workflow system.