File Coverage

blib/lib/Class/Workflow.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             package Class::Workflow;
4 4     4   114703 use Moose;
  0            
  0            
5              
6             use Class::Workflow::State::Simple;
7             use Class::Workflow::Transition::Simple;
8             use Class::Workflow::Instance::Simple;
9              
10             our $VERSION = "0.11_01";
11              
12             use Carp qw/croak/;
13             use Scalar::Util qw/refaddr/;
14              
15             has initial_state => (
16             isa => "Str | Object",
17             is => "rw",
18             );
19              
20             has instance_class => (
21             isa => "Str",
22             is => "rw",
23             default => "Class::Workflow::Instance::Simple",
24             );
25              
26             sub new_instance {
27             my ( $self, %attrs ) = @_;
28              
29             if ( !$attrs{state} ) {
30             if ( my $initial_state = $self->state( $self->initial_state ) ) {
31             $attrs{state} = $initial_state;
32             } else {
33             croak "Explicit state not specified and no initial state is set in the workflow.";
34             }
35             }
36              
37             $self->instance_class->new( %attrs );
38             }
39              
40             has state_class => (
41             isa => "Str",
42             is => "rw",
43             default => "Class::Workflow::State::Simple",
44             );
45              
46             has transition_class => (
47             isa => "Str",
48             is => "rw",
49             default => "Class::Workflow::Transition::Simple",
50             );
51              
52             has _state_fields => (
53             isa => "HashRef",
54             is => "ro",
55             traits => [ 'Hash' ],
56             default => sub { return {} },
57             handles => {
58             states => 'values',
59             state_names => 'keys',
60             get_state => 'get',
61             get_states => 'get',
62             add_state => 'set',
63             delete_state => 'delete',
64             has_state => 'exists',
65             },
66             );
67              
68             has _transition_fields => (
69             isa => "HashRef",
70             is => "ro",
71             traits => [ 'Hash' ],
72             default => sub { return {} },
73             handles => {
74             transitions => 'values',
75             transition_names => 'keys',
76             get_transition => 'get',
77             get_transitions => 'get',
78             add_transition => 'set',
79             delete_transition => 'delete',
80             has_transition => 'exists',
81             },
82             );
83              
84             sub rename_state {
85             my ( $self, $name, $new_name ) = @_;
86             my $obj = $self->delete_state( $name );
87             $obj->name( $new_name ) if $obj->can("name");
88             $self->add_state( $new_name => $obj );
89             }
90              
91             sub rename_transition {
92             my ( $self, $name, $new_name ) = @_;
93             my $obj = $self->delete_transition( $name );
94             $obj->name( $new_name ) if $obj->can("name");
95             $self->add_transition( $new_name => $obj );
96             }
97              
98             sub create_state {
99             my ( $self, $name, @attrs ) = @_;
100             $self->add_state( $name => $self->construct_state( @attrs ) );
101             }
102              
103             sub create_transition {
104             my ( $self, $name, @attrs ) = @_;
105             $self->add_transition( $name => $self->construct_transition( @attrs ) );
106             }
107              
108             sub construct_state {
109             my ( $self, %attrs ) = @_;
110             my $class = delete($attrs{class}) || $self->state_class;
111             $class->new( %attrs );
112             }
113              
114             sub construct_transition {
115             my ( $self, %attrs ) = @_;
116             my $class = delete($attrs{class}) || $self->transition_class;
117             $class->new( %attrs );
118             }
119              
120             sub autovivify_states {
121             my ( $self, $thing ) = @_;
122              
123             no warnings 'uninitialized';
124             if ( ref $thing eq "ARRAY" ) {
125             return [ map { $self->state( $_ ) } @$thing ];
126             } else {
127             return $self->state( $thing );
128             }
129             }
130              
131             sub autovivify_transitions {
132             my ( $self, $thing ) = @_;
133              
134             no warnings 'uninitialized';
135             if ( ref $thing eq "ARRAY" ) {
136             return [ map { $self->transition( $_ ) } @$thing ];
137             } else {
138             return $self->transition( $thing );
139             }
140             }
141              
142             sub create_or_set_state {
143             my ( $self, %attrs ) = @_;
144              
145             my $name = $attrs{name} || croak "Every state must have a name";
146              
147             $self->expand_attrs( \%attrs );
148              
149             if ( my $obj = $self->get_state( $name ) ) {
150             delete $attrs{name};
151             foreach my $attr ( keys %attrs ) {
152             $obj->$attr( $attrs{$attr} );
153             }
154              
155             return $obj;
156             } else {
157             return $self->create_state( $name, %attrs );
158             }
159             }
160              
161             sub create_or_set_transition {
162             my ( $self, %attrs ) = @_;
163              
164             my $name = $attrs{name} || croak "Every transition must have a name";
165              
166             $self->expand_attrs( \%attrs );
167              
168             if ( my $obj = $self->get_transition( $name ) ) {
169             delete $attrs{name};
170             foreach my $attr ( keys %attrs ) {
171             $obj->$attr( $attrs{$attr} );
172             }
173              
174             return $obj;
175             } else {
176             return $self->create_transition( $name, %attrs );
177             }
178             }
179              
180             sub state {
181             my ( $self, @params ) = @_;
182              
183             if ( @params == 1 ) {
184             if ( ref($params[0]) eq "HASH" ) {
185             @params = %{ $params[0] };
186             } elsif ( ref($params[0]) eq "ARRAY" ) {
187             @params = @{ $params[0] };
188             }
189             }
190              
191             if ( !blessed($params[0]) and !blessed($params[1]) and @params % 2 == 0 ) {
192             # $wf->state( name => "foo", transitions => [qw/bar gorch/] )
193             return $self->create_or_set_state( @params );
194             } elsif ( !ref($params[0]) and @params % 2 == 1 ) {
195             # my $state = $wf->state("new", %attrs); # create new by name, or just get_foo
196             return $self->create_or_set_state( name => @params )
197             } elsif ( @params == 1 and blessed($params[0]) and $params[0]->can("name") ) {
198             # $wf->state( $state ); # set by object (if $object->can("name") )
199             return $self->add_state( $params[0]->name => $params[0] );
200             } elsif ( @params == 2 and blessed($params[1]) and !ref($params[0]) ) {
201             # $wf->state( foo => $state ); # set by name
202             return $self->add_state( @params );
203             } else {
204             if ( @params == 1 and blessed($params[0]) ) {
205             croak "The state $params[0] must support the 'name' method.";
206             } else {
207             croak "'state' was called with invalid parameters. Please consult the documentation.";
208             }
209             }
210             }
211              
212             sub transition {
213             my ( $self, @params ) = @_;
214              
215             if ( @params == 1 ) {
216             if ( ref($params[0]) eq "HASH" ) {
217             @params = %{ $params[0] };
218             } elsif ( ref($params[0]) eq "ARRAY" ) {
219             @params = @{ $params[0] };
220             }
221             }
222              
223             if ( !blessed($params[0]) and !blessed($params[1]) and @params % 2 == 0 ) {
224             # $wf->state( name => "foo", transitions => [qw/bar gorch/] )
225             return $self->create_or_set_transition( @params );
226             } elsif ( !ref($params[0]) and @params % 2 == 1 ) {
227             # my $state = $wf->state("new", %attrs); # create new by name, or just get_foo
228             return $self->create_or_set_transition( name => @params )
229             } elsif ( @params == 1 and blessed($params[0]) and $params[0]->can("name") ) {
230             # $wf->state( $state ); # set by object (if $object->can("name") )
231             return $self->add_transition( $params[0]->name => $params[0] );
232             } elsif ( @params == 2 and blessed($params[1]) and !ref($params[0]) ) {
233             # $wf->state( foo => $state ); # set by name
234             return $self->add_transition( @params );
235             } else {
236             if ( @params == 1 and blessed($params[0]) ) {
237             croak "The transition $params[0] must support the 'name' method.";
238             } else {
239             croak "'transition' was called with invalid parameters. Please consult the documentation.";
240             }
241             }
242             }
243              
244             sub expand_attrs {
245             my ($self, $attrs ) = @_;
246              
247             foreach my $key ( keys %$attrs ) {
248             if ( my ( $type ) = ( $key =~ /(transition|state)/ ) ) {
249             my $method = "autovivify_${type}s";
250             $attrs->{$key} = $self->$method( $attrs->{$key} );
251             }
252             }
253             }
254              
255             __PACKAGE__;
256              
257             __END__
258              
259             =pod
260              
261             =head1 NAME
262              
263             Class::Workflow - Light weight workflow system.
264              
265             =head1 SYNOPSIS
266              
267             use Class::Workflow;
268              
269             # ***** NOTE *****
270             #
271             # This is a pretty long and boring example
272             #
273             # you probably want to see some flashy flash videos, so look in SEE ALSO
274             # first ;-)
275             #
276             # ****************
277              
278             # a workflow object assists you in creating state/transition objects
279             # it lets you assign symbolic names to the various objects to ease construction
280              
281             my $wf = Class::Workflow->new;
282              
283             # ( you can still create the state, transition and instance objects manually. )
284              
285              
286             # create a state, and set the transitions it can perform
287              
288             $wf->state(
289             name => "new",
290             transitions => [qw/accept reject/],
291             );
292              
293             # set it as the initial state
294              
295             $wf->initial_state("new");
296              
297              
298             # create a few more states
299              
300             $wf->state(
301             name => "open",
302             transitions => [qw/claim_fixed reassign/],
303             );
304              
305             $wf->state(
306             name => "rejected",
307             );
308              
309              
310             # transitions move instances from state to state
311            
312             # create the transition named "reject"
313             # the state "new" refers to this transition
314             # the state "rejected" is the target state
315              
316             $wf->transition(
317             name => "reject",
318             to_state => "rejected",
319             );
320              
321              
322             # create a transition named "accept",
323             # this transition takes a value from the context (which contains the current acting user)
324             # the context is used to set the current owner for the bug
325              
326             $wf->transition(
327             name => "accept",
328             to_state => "opened",
329             body => sub {
330             my ( $transition, $instance, $context ) = @_;
331             return (
332             owner => $context->user, # assign to the use who accepted it
333             );
334             },
335             );
336              
337              
338             # hooks are triggerred whenever a state is entered. They cannot change the instance
339             # this hook calls a hypothetical method on the submitter object
340              
341             $wf->state( "reject" )->add_hook(sub {
342             my ( $state, $instance ) = @_;
343             $instance->submitter->notify("Your item has been rejected");
344             });
345              
346              
347             # the rest of the workflow definition is omitted for brevity
348              
349              
350             # finally, use this workflow in the action that handles bug creation
351              
352             sub new_bug {
353             my ( $submitter, %params ) = @_;
354              
355             return $wf->new_instance(
356             submitter => $submitter,
357             %params,
358             );
359             }
360              
361             =head1 DESCRIPTION
362              
363             Workflow systems let you build a state machine, with transitions between
364             states.
365              
366             =head1 EXAMPLES
367              
368             There are several examples in the F<examples> directory, worth looking over to
369             help you understand and to learn some more advanced things.
370              
371             The most important example is probably how to store a workflow definition (the
372             states and transitions) as well as the instances using L<DBIx::Class> in a
373             database.
374              
375             =head2 Bug Tracker Example
376              
377             One of the simplest examples of a workflow which you've probably used is a bug
378             tracking application:
379              
380             =over 4
381              
382             =item The initial state is 'new'
383              
384             =item new
385              
386             New bugs arrive here.
387              
388             =item reject
389              
390             This bug is not valid.
391              
392             Target state: C<rejected>.
393              
394             =item accept
395              
396             This bug needs to be worked on.
397              
398             Target state: C<open>.
399              
400             =item rejected
401              
402             This is the state where deleted bugs go, it has no transitions.
403              
404             =item open
405              
406             The bug is being worked on right now.
407              
408             =item reassign
409              
410             Pass the bug to someone else.
411              
412             Target state: C<unassigned>.
413              
414             =item fixed
415              
416             The bug looks fixed, and needs verifification.
417              
418             Target state: C<awaiting_approval>.
419              
420             =item unassigned
421              
422             The bug is waiting for a developer to take it.
423              
424             =item take
425              
426             Volunteer to handle the bug.
427              
428             Target state: C<open>.
429              
430             =item awaiting_approval
431              
432             The submitter needs to verify the bug.
433              
434             =item resolved
435              
436             The bug is resolved and can be closed.
437              
438             Target state: C<closed>
439              
440             =item unresolved
441              
442             The bug needs more work.
443              
444             Target state: C<open>
445              
446             =item closed
447              
448             This is, like rejected, an end state (it has no transitions).
449              
450             =back
451              
452             If you read through this very simple state machine you can see that it
453             describes the steps and states a bug can go through in a bug tracking system.
454             The core of every workflow is a state machine.
455              
456             =head1 INSTANCES
457              
458             On the implementation side, the core idea is that every "item" in the system
459             (in our example, a bug) has a workflow B<instance>. This instance represents
460             the current position of the item in the workflow, along with history data (how
461             did it get here).
462              
463             In this implementation, the instance is usually a consumer of
464             L<Class::Workflow::Instance>, typically L<Class::Workflow::Instance::Simple>.
465              
466             So, when you write your MyBug class, it should look like this (if it were written
467             in L<Moose>):
468              
469             package MyBug;
470             use Moose;
471              
472             has workflow_instance => (
473             does => "Class::Workflow::Instance", # or a more restrictive constraint
474             is => "rw",
475             );
476              
477             Since this system is purely functional (at least if your transitions are), you
478             need to always set the instance after applying a transition.
479              
480             For example, let's say you have a handler for the "accept" action, to change
481             the instance's state it would do something like this:
482              
483             sub accept {
484             my $bug = shift;
485              
486             my $wi = $bug->workflow_instance;
487             my $current_state = $wi->state;
488              
489             # if your state supports named transitions
490             my $accept = $current_state->get_transition( "accept" )
491             or die "There's no 'accept' transition in the current state";
492              
493             my $wi_accepted = $accept->apply( $wi );
494              
495             $bug->workflow_instance( $wi_accepted );
496             }
497              
498             =head1 RESTRICTIONS
499              
500             Now let's decsribe some restrictions on this workflow.
501              
502             =over 4
503              
504             =item *
505              
506             Only the submitter can approve the bug as resolved.
507              
508             =item *
509              
510             Only the developer can claim the bug was fixed, and reassign the bug.
511              
512             =item *
513              
514             Any developer (but not the submitter) can accept a bug as valid, into the
515             'open' state.
516              
517             =back
518              
519             A workflow system will not only help in modelying the state machine, but also
520             help you create restrictions on how states need to be changed, etc.
521              
522             The implementation of restrictions is explained after the next section.
523              
524             =head1 CONTEXTS
525              
526             In order to implement these restrictions cleanly you normally use a context
527             object (a default one is provided in L<Class::Workflow::Context> but you can
528             use B<anything>).
529              
530             This is typically the first (and sometimes only) argument to all transition
531             applications, and it describes the context that the transition is being applied
532             in, that is who is applying the transition, what are they applying it with, etc
533             etc.
534              
535             In our bug system we typically care about the user, and not much else.
536              
537             Imagine that we have a user class:
538              
539             package MyUser;
540              
541             has id => (
542             isa => "Num",
543             is => "ro",
544             default => sub { next_unique_id() };
545             );
546              
547             has name => (
548             ...
549             );
550              
551             We can create a context like this:
552              
553             package MyWorkflowContext;
554             use Moose;
555              
556             extends "Class::Workflow::Context";
557              
558             has user => (
559             isa => "MyUser",
560             is => "rw",
561             );
562              
563             to contain the "current" user.
564              
565             Then, when we apply the transition a bit differently:
566              
567             sub accept {
568             my ( $bug, $current_user ) = @_;
569              
570             my $wi = $bug->workflow_instance;
571             my $current_state = $wi->state;
572              
573             # if your state supports named transitions
574             my $accept = $current_state->get_transition( "accept" )
575             or croak "There's no 'accept' transition in the current state";
576              
577             my $c = MyWorkflowContext->new( user => $current_user );
578             my $wi_accepted = $accept->apply( $wi, $c );
579              
580             $bug->workflow_instance( $wi_accepted );
581             }
582              
583             And the transition has access to our C<$c> object, which references the current
584             user.
585              
586             =head1 IMPLEMENTING RESTRICTIONS
587              
588             In order to implement the restrictions we specified above we need to know who
589             the submitter and owner of the item are.
590              
591             For this we create our own instance class as well:
592              
593             package MyWorkflowInstance;
594             use Moose;
595              
596             extends "Class::Workflow::Instance::Simple";
597              
598             has owner => (
599             isa => MyUser",
600             is => "ro", # all instance fields should be read only
601             );
602              
603             has submitter => (
604             isa => MyUser",
605             is => "ro", # all instance fields should be read only
606             );
607              
608             When the first instance is created the current user is set as the submitter.
609              
610             Then, as transitions are applied they can check for the restrictions.
611              
612             This is typically not done in the actual transition body, but rather in
613             validation hooks. L<Class::Workflow::Transition::Validate> provides a stanard
614             hook, and L<Class::Workflow::Transition::Simple> provides an even easier
615             interface for this:
616              
617             my $fixed = Class::Workflow::Transition::Simple->new(
618             name => 'fixed',
619             to_transition => $awaiting_approval,
620             validators => [
621             sub {
622             my ( $self, $instance, $c ) = @_;
623             die "Not owner" unless $self->instance->owner->id == $c->user->id;
624             },
625             ],
626             body => sub {
627             # ...
628             },
629             );
630              
631             =head1 PERSISTENCE
632              
633             Persistence in workflows involves saving the workflow instance as a
634             relationship of the item whose state it represents, or even treating the
635             instance as the actual item.
636              
637             In any case, right now there are no turnkey persistence layers available.
638              
639             A fully working L<DBIx::Class> example can be found in the F<examples/dbic>
640             directory, but setup is manual. Serialization based persistence (with e.g.
641             L<Storable>) is trivial as well.
642              
643             See L<Class::Workflow::Cookbook> for more details.
644              
645             =head1 ROLES AND CLASSES
646              
647             Most of the Class::Workflow system is implemented using roles to specify
648             interfaces with reusable behavior, and then ::Simple classes which mash up a
649             bunch of useful roles.
650              
651             This means that you have a very large amount of flexibility in how you compose
652             your state/transition objects, allowing good integration with most existing
653             software.
654              
655             This is achieved using L<Moose>, specifically L<Moose::Role>.
656              
657             =head1 THIS CLASS
658              
659             L<Class::Workflow> objects are utility objects to help you create workflows and
660             instances without worrying too much about the state and transition objects.
661              
662             It's usage is overviewed in the L</SYNOPSIS> section.
663              
664             =head1 FIELDS
665              
666             =over 4
667              
668             =item instance_class
669              
670             =item state_class
671              
672             =item transition_class
673              
674             These are the classes to instantiate with.
675              
676             They default to L<Class::Workflow::Instance::Simple>,
677             L<Class::Workflow::State::Simple> and L<Class::Workflow::Transition::Simple>.
678              
679             =back
680              
681             =head1 METHODS
682              
683             =over 4
684              
685             =item new_instance
686              
687             Instantiate the workflow
688              
689             =item initial_state
690              
691             Set the starting state of instances.
692              
693             =item states
694              
695             =item transitions
696              
697             Return all the registered states or transitions.
698              
699             =item state_names
700              
701             =item transition_names
702              
703             Return all the registered state or transition names.
704              
705             =item state
706              
707             =item transition
708              
709             These two methods create update or retrieve state or transition objects.
710              
711             They have autovivification semantics for ease of use, and are pretty lax in
712             terms of what they accept.
713              
714             More formal methods are presented below.
715              
716             They have several forms:
717              
718             $wf->state("foo"); # get (and maybe create) a new state with the name "foo"
719              
720             $wf->state( foo => $object ); # set $object as the state by the name "foo"
721              
722             $wf->state( $object ); # register $object ($object must support the ->name method )
723              
724             # create or update the state named "foo" with the following attributes:
725             $wf->state(
726             name => "foo",
727             validators => [ sub { ... } ],
728             );
729              
730             # also works with implicit name:
731             $wf->state( foo =>
732             validators => [ sub { ... } ],
733             );
734              
735             (wherever ->state is used ->transition can also be used).
736              
737             Additionally, whenever you construct a state like this:
738              
739             $wf->state(
740             name => "foo",
741             transitions => [qw/t1 t2/],
742             );
743              
744             the parameters are preprocessed so that it's as if you called:
745              
746             my @transitions = map { $wf->state($_) } qw/t1 t2/;
747             $wf->state(
748             name => "foo",
749             transitions => [@transitions],
750             );
751              
752             so you don't have to worry about creating objects first.
753              
754             =item add_state $name, $object
755              
756             =item add_transition $name, $object
757              
758             Explicitly register an object by the name $name.
759              
760             =item delete_state $name
761              
762             =item delete_transition $name
763              
764             Remove an object by the name $name.
765              
766             Note that this will B<NOT> remove the object from whatever other object reference it, so that:
767              
768             $wf->state(
769             name => "foo",
770             transitions => ["bar"],
771             );
772              
773             $wf->delete_transition("bar");
774              
775             will not remove the object that was created by the name "bar" from the state
776             "foo", it's just that the name has been freed.
777              
778             Use this method with caution.
779              
780             =item rename_state $old, $new
781              
782             =item rename_transition $old, $new
783              
784             Change the name of an object.
785              
786             =item get_state $name
787              
788             =item get_transition $name
789              
790             Get the object by that name or return undef.
791              
792             =item create_state $name, @args
793              
794             =item create_transition $name, @args
795              
796             Call C<construct_state> or C<construct_transition> and then C<add_state> or
797             C<add_transition> with the result.
798              
799             =item construct_state @args
800              
801             =item construct_transition @args
802              
803             Call ->new on the appropriate class.
804              
805             =item expand_attrs \%attrs
806              
807             This is used by C<create_or_set_state> and C<create_or_set_transition>, and
808             will expand the attrs by the names C<to_state>, C<transition> and
809             C<transitions> to be objects instead of string names, hash or array references,
810             by calling C<autovivify_transitions> or C<autovivify_states>.
811              
812             In the future this method might be more aggressive, expanding suspect attrs.
813              
814             =item autovivify_states @things
815              
816             =item autovivify_transitions @things
817              
818             Coerce every element in @things into an object by calling
819             C<< $wf->state($thing) >> or C<< $wf->transition($thing) >>.
820              
821             =item create_or_set_state %attrs
822              
823             =item create_or_set_transition %attrs
824              
825             If the object by the name $attrs{name} exists, update it's attrs, otherwise
826             create a new one.
827              
828             =back
829              
830             =head1 SEE ALSO
831              
832             L<Workflow> - Chris Winters' take on workflows - it wasn't simple enough for me
833             (factoring out the XML/factory stuff was difficult and I needed a much more
834             dynamic system).
835              
836             L<http://is.tm.tue.nl/research/patterns/> - lots of explanation and lovely
837             flash animations.
838              
839             L<Class::Workflow::YAML> - load workflow definitions from YAML files.
840              
841             L<Class::Workflow::Transition::Simple>, L<Class::Workflow::State::Simple>,
842             L<Class::Workflow::Instance::Simple> - easy, useful classes that perform all
843             the base roles.
844              
845             L<Moose>
846              
847             =head1 VERSION CONTROL
848              
849             This module is maintained using Darcs. You can get the latest version from
850             L<http://nothingmuch.woobling.org/Class-Workflow/>, and use C<darcs send>
851             to commit changes.
852              
853             =head1 AUTHOR
854              
855             Yuval Kogman <nothingmuch@woobling.org>
856              
857             =head1 COPYRIGHT & LICENSE
858              
859             Copyright (c) 2006-2008 Infinity Interactive, Yuval Kogman. All rights
860             reserved. This program is free software; you can redistribute
861             it and/or modify it under the same terms as Perl itself.
862              
863             =cut
864              
865