File Coverage

blib/lib/Workflow/Condition.pm
Criterion Covered Total %
statement 56 56 100.0
branch 10 10 100.0
condition 18 22 81.8
subroutine 12 12 100.0
pod 3 3 100.0
total 99 103 96.1


line stmt bran cond sub pod time code
1             package Workflow::Condition;
2              
3 35     35   2670138 use warnings;
  35         78  
  35         2389  
4 35     35   206 use strict;
  35         69  
  35         1239  
5              
6 35     35   227 use parent qw( Workflow::Base );
  35         92  
  35         222  
7 35     35   2557 use v5.14.0;
  35         146  
8 35     35   194 use Carp qw(croak);
  35         95  
  35         2286  
9 35     35   239 use Log::Any qw( $log );
  35         66  
  35         311  
10 35     35   44530 use Workflow::Exception qw( workflow_error );
  35         108  
  35         2359  
11 35     35   18190 use Workflow::Condition::IsFalse;
  35         111  
  35         361  
12 35     35   18535 use Workflow::Condition::IsTrue;
  35         109  
  35         432  
13              
14             $Workflow::Condition::CACHE_RESULTS = 1;
15             $Workflow::Condition::VERSION = '2.09';
16              
17             $Workflow::Condition::STRICT_BOOLEANS = 1;
18              
19             my @FIELDS = qw( name class );
20             __PACKAGE__->mk_accessors(@FIELDS);
21              
22             sub init {
23 70     70 1 255 my ( $self, $params ) = @_;
24 70         522 $self->name( $params->{name} );
25 70         2020 $self->class( $params->{class} );
26             }
27              
28             sub evaluate {
29 1     1 1 1464 my ($self) = @_;
30 1         18 croak "Class ", ref($self), " must implement 'evaluate()'!\n";
31             }
32              
33             sub evaluate_condition {
34 132     132 1 8058 my ( $class, $wf, $condition_or_name) = @_;
35 132         475 $wf->type;
36              
37 132         1825 my $factory = $wf->_factory();
38 132 100       1730 my $condition_name =
39             (ref $condition_or_name) ? $condition_or_name->name : $condition_or_name;
40 132 100       997 my $condition = (ref $condition_or_name) ? $condition_or_name : undef;
41              
42 132         713 $log->debug("Checking condition $condition_name");
43              
44             local $wf->{'_condition_result_cache'} =
45 132   100     891 $wf->{'_condition_result_cache'} || {};
46              
47 132 100 100     710 if ( $Workflow::Condition::CACHE_RESULTS
48             && exists $wf->{'_condition_result_cache'}->{$condition_name} ) {
49              
50 20         59 my $cache_value = $wf->{'_condition_result_cache'}->{$condition_name};
51             # The condition has already been evaluated and the result
52             # has been cached
53 20   100     137 $log->debug(
54             "Condition has been cached: '$condition_name', cached result: ",
55             $cache_value || ''
56             );
57              
58 20         156 return $cache_value;
59             } else {
60              
61             # we did not evaluate the condition yet, we have to do
62             # it now
63 112   66     476 $condition //= $wf->_factory()
64             ->get_condition( $condition_name, $wf->type );
65 112         528 $log->debug( "Evaluating condition '$condition_name'" );
66              
67 112         413 my $return_value;
68 112         547 my $result = $condition->evaluate($wf);
69 111 100 100     5679 if (ref $result eq 'Workflow::Condition::IsFalse'
    100 100        
      33        
      66        
70             or (not $Workflow::Condition::STRICT_BOOLEANS and not $result)) {
71 53         2304 $log->info( "Got false result with '$result' on '$condition_name'");
72 53         1406 $return_value = 0;
73             } elsif (ref $result eq 'Workflow::Condition::IsTrue'
74             or (not $Workflow::Condition::STRICT_BOOLEANS and $result)) {
75 56         2141 $log->info( "Got true result with '$result' on '$condition_name'");
76 56         1273 $return_value = 1;
77             } else {
78 2         13 $log->fatal( "Evaluate on '$condition_name' did not return a valid result object" );
79 2         18 $log->trace( 'Eval result', { result => $result } );
80 2         56 croak "Evaluate on '$condition_name' did not return a valid result object";
81             }
82              
83 109         410 $wf->{'_condition_result_cache'}->{$condition_name} = $return_value;
84              
85 109         1021 return $return_value;
86             }
87             }
88              
89             1;
90              
91             __END__
92              
93             =pod
94              
95             =head1 NAME
96              
97             Workflow::Condition - Evaluate a condition depending on the workflow state and environment
98              
99             =head1 VERSION
100              
101             This documentation describes version 2.09 of this package
102              
103             =head1 SYNOPSIS
104              
105             # First declare the condition in a 'workflow_condition.yaml'...
106              
107             condition:
108             - name: IsAdminUser
109             class: MyApp::Condition::IsAdminUser
110             param:
111             - name: admin_group_id
112             value: '5'
113             - name: admin_group_id
114             value: '6'
115              
116             # Reference the condition in an action of the state/workflow definition...
117             state:
118             - name: SomeAdminAction
119             ...
120             condition:
121             - name: IsAdminUser
122             - name: AnotherAdminAction
123             ...
124             condition:
125             - name: IsAdminUser
126             - name: AUserAction
127             ...
128             condition:
129             - name: !IsAdminUser
130              
131             # Then implement the condition
132              
133             package MyApp::Condition::IsAdminUser;
134              
135             use strict;
136             use parent qw( Workflow::Condition );
137             use Workflow::Exception qw( configuration_error );
138              
139             __PACKAGE__->mk_accessors( 'admin_group_id' );
140              
141             sub init {
142             my ( $self, $params ) = @_;
143             $self->SUPER::init( $params );
144             unless ( $params->{admin_group_id} ) {
145             configuration_error
146             "You must define one or more values for 'admin_group_id' in ",
147             "declaration of condition ", $self->name;
148             }
149             my @admin_ids = $self->_normalize_array( $params->{admin_group_id} );
150             $self->admin_group_id( { map { $_ => 1 } @admin_ids } );
151             }
152              
153             sub evaluate {
154             my ( $self, $wf ) = @_;
155             my $admin_ids = $self->admin_group_id;
156             my $current_user = $wf->context->param( 'current_user' );
157             unless ( $current_user ) {
158             return ''; # return false
159             }
160             foreach my $group ( @{ $current_user->get_groups } ) {
161             return 1 if ( $admin_ids->{ $group->id } ); # return true
162             }
163             return ''; # return false
164             }
165              
166             =head1 DESCRIPTION
167              
168             Conditions are used by the workflow to see whether actions are
169             available in a particular context. So if user A asks the workflow for
170             the available actions she might get a different answer than user B
171             since they determine separate contexts.
172              
173             B<NOTE>: The condition is enforced by Workflow::State. This means that
174             the condition name must be visible inside of the state definition. If
175             you specify the reference to the condition only inside of the full
176             action specification in a seperate file then nothing will happen. The
177             reference to the condition must be defined inside of the state/workflow
178             specification.
179              
180             =head1 CONFIGURATION
181              
182             While some conditions apply to all workflows, you may have a case where
183             a condition has different implementations for different workflow types.
184             For example, IsAdminUser may look in two different places for two
185             different workflow types, but you want to use the same condition name
186             for both.
187              
188             You can accomplish this by adding a type in the condition configuration.
189              
190             type: Ticket
191             condition:
192             - name: IsAdminUser
193             class: MyApp::Condition::IsAdminUser
194             param:
195             - name: admin_group_id
196             value: '5'
197             - name: admin_group_id
198             value: '6'
199              
200             The type must match a loaded workflow type, or the condition won't work.
201             When the workflow looks for a condition, it will look for a typed condition
202             first. If it doesn't find one, it will look for non-typed conditions.
203              
204             =head1 SUBCLASSING
205              
206             =head2 Strategy
207              
208             The methods below specify an interface. Classes used as conditions need
209             to implement these methods. The easiest way to achieve that is by
210             inheriting from C< Workflow::Condition >. This is not required, though.
211              
212             The idea behind conditions is that they are be stateless. So when the
213             L<Workflow::Factory> object reads in the condition configuration it
214             creates the condition objects and initializes them with whatever
215             information is passed in.
216              
217             Then when the condition is evaluated we just call C<evaluate()> on the
218             condition. Hopefully the operation can be done very quickly since the
219             condition may be called many, many times during a workflow lifecycle
220             -- they are typically used to show users what options they have given
221             the current state of the workflow for things like menu options. So
222             keep it short!
223              
224              
225             =head2 Interface methods
226              
227             When implementing a condition in a class that doesn't have this class
228             as a super-class, you must implement these methods. If you implement
229             a condition that I< does > have this class as a super-class, you I< may >
230             need to override these methods.
231              
232             =head3 evaluate( $workflow )
233              
234             Determine whether your condition fails by returning a false value or
235             a true value upon success. You can get the application context information
236             necessary to process your condition from the C<$workflow> object.
237              
238             B<NOTE> Callers wanting to evaluate a condition, should not call
239             this method directly, but rather use the
240             C<< Workflow::Condition->evaluate_condition >> class method described below.
241              
242             =head2 Other methods
243              
244             To create your own condition based on this class, these methods are available
245             and can be overridden to do specific tasks.
246              
247             =head3 init( \%params )
248              
249             This is optional, but called when the condition is first
250             initialized. It may contain information you will want to initialize
251             your condition with in C<\%params>, which are all the declared
252             parameters in the condition declaration except for 'class' and 'name'.
253              
254             You may also do any initialization here -- you can fetch data from the
255             database and store it in the class or object, whatever you need.
256              
257             If you do not have sufficient information in C<\%params> you should
258             throw an exception (preferably 'configuration_error' imported from
259             L<Workflow::Exception>).
260              
261             =head2 Caching and inverting the result
262              
263             If in one state, you ask for the same condition again, Workflow uses
264             the cached result, so that within one list of available actions, you
265             will get a consistent view. Note that if we would not use caching,
266             this might not necessary be the case, as something external might
267             change between the two evaluate() calls.
268              
269             Caching is also used with an inverted condition, which you can specify
270             in the definition using C<<condition name="!some_condition">>.
271             This condition returns the negation of the original one, i.e.
272             if the original condition fails, this one does not and the other way
273             round. As caching is used, you can model "yes/no" decisions using this
274             feature - if you have both C<<condition name="some_condition">> and
275             C<<condition name="!some_condition">> in your workflow state definition,
276             exactly one of them will succeed and one will fail - which is particularly
277             useful if you use "autorun" a lot.
278              
279             Caching can be disabled by changing C<$Workflow::Condition::CACHE_RESULTS>
280             to zero (0):
281              
282             $Workflow::Condition::CACHE_RESULTS = 0;
283              
284             All versions before 1.49 used a mechanism that effectively caused global
285             state. To address the problems that resulted (see GitHub issues #9 and #7),
286             1.49 switched to a new mechanism with a cache per workflow instance.
287              
288              
289             =head3 $class->evaluate_condition( $WORKFLOW, $CONDITION_OR_NAME )
290              
291             Users call this method to evaluate a condition by name or by condition
292             instance; subclasses call this method to evaluate a nested condition.
293              
294             If the condition name starts with an '!', the result of the condition
295             is negated. Note that a side-effect of this is that the return
296             value of the condition is ignored. Only the negated boolean-ness
297             is preserved.
298              
299             This does implement a trick that is not a convention in the underlying
300             Workflow library: by default, workflow conditions throw an error when
301             the condition is false and just return when the condition is true. To
302             allow for counting the true conditions, we also look at the return
303             value here. If a condition returns zero or an undefined value, but
304             did not throw an exception, we consider it to be '1'. Otherwise, we
305             consider it to be the value returned.
306              
307             =head1 SEE ALSO
308              
309             =over
310              
311             =item * L<Workflow::Base>
312              
313             =item * L<Log::Any>
314              
315             =item * L<Workflow::Exception>
316              
317             =back
318              
319             =head1 COPYRIGHT
320              
321             Copyright (c) 2003-2024 Chris Winters. All rights reserved.
322              
323             This library is free software; you can redistribute it and/or modify
324             it under the same terms as Perl itself.
325              
326             Please see the F<LICENSE>
327              
328             =head1 AUTHORS
329              
330             Please see L<Workflow>
331              
332             =cut