File Coverage

lib/UR/Observer.pm
Criterion Covered Total %
statement 121 143 84.6
branch 38 54 70.3
condition 15 17 88.2
subroutine 16 20 80.0
pod 3 10 30.0
total 193 244 79.1


line stmt bran cond sub pod time code
1             package UR::Observer;
2              
3 266     266   1122 use strict;
  266         443  
  266         7675  
4 266     266   1032 use warnings;
  266         397  
  266         11750  
5              
6             BEGIN {
7 266     266   961 require UR;
8 266         97032 require UR::Context::Transaction;
9             };
10              
11             our $VERSION = "0.46"; # UR $VERSION;
12              
13             UR::Object::Type->define(
14             class_name => __PACKAGE__,
15             has => [
16             subject_class_name => { is => 'Text', is_optional => 1, default_value => 'UR::Object' },
17             subject_id => { is => 'SCALAR', is_optional => 1, default_value => '' },
18             aspect => { is => 'String', is_optional => 1, default_value => '' },
19             priority => { is => 'Number', is_optional => 1, default_value => 1 },
20             note => { is => 'String', is_optional => 1, default_value => '' },
21             once => { is => 'Boolean', is_optional => 1, default_value => 0 },
22             subject => { is => 'UR::Object', id_by => 'subject_id', id_class_by => 'subject_class_name' },
23             ],
24             is_transactional => 1,
25             );
26              
27             # This is not implemented as a "real" observer via create() because at the point during bootstrapping
28             # that this module is loaded, we're not yet ready to start creating objects
29             __PACKAGE__->_insert_record_into_all_change_subscriptions('UR::Observer', 'priority', '',
30             [\&_modify_priority, '', 0, UR::Object::Type->autogenerate_new_object_id_uuid]);
31              
32             sub create {
33 269     269 1 4669 my $class = shift;
34              
35 269         1017 $class->_create_or_define('create', @_);
36             }
37              
38             sub __define__ {
39 0     0   0 my $class = shift;
40              
41 0         0 $class->_create_or_define('__define__', @_);
42             }
43              
44             my @required_params_for_register = qw(aspect callback note once priority subject_class_name subject_id id);
45 1     1 0 1230 sub required_params_for_register { @required_params_for_register }
46              
47             sub _create_or_define {
48 269     269   388 my $class = shift;
49 269         362 my $method = shift;
50 269         740 my %params = @_;
51              
52 269         463 my $callback = delete $params{callback};
53              
54             my $self = UR::Context::Transaction::do {
55 269     269   304 my $inner_self;
56 269 50       688 if ($method eq 'create') {
    0          
57 269         1347 $inner_self = $class->SUPER::create(%params);
58             } elsif ($method eq '__define__') {
59 0         0 $inner_self = $class->SUPER::__define__(%params);
60             } else {
61 0         0 Carp::croak('Instantiating a UR::Observer with some method other than create() or __define__() is not supported');
62             }
63 268         615 $inner_self->{callback} = $callback;
64 268         561 $inner_self->register_callback(map { $_ => $inner_self->$_ } @required_params_for_register);
  2144         4091  
65 258         929 return $inner_self;
66 269         1852 };
67              
68 258         2210 return $self;
69             }
70              
71              
72             {
73             my @has_defaults = qw(aspect note once priority subject_class_name subject_id);
74 1     1 0 1056 sub has_defaults { @has_defaults }
75              
76             my %defaults =
77             map {
78             $_ => __PACKAGE__->__meta__->{has}->{$_}->{default_value}
79             }
80             grep {
81             exists __PACKAGE__->__meta__->{has}->{$_}
82             && exists __PACKAGE__->__meta__->{has}->{$_}->{default_value}
83             } @has_defaults;
84 544     544 0 8372 sub defaults_for_register_callback { %defaults }
85             }
86              
87             sub register_callback {
88 537     537 0 1513 my $class = shift;
89 537         3431 my %params = @_;
90              
91 537 100       1808 unless (defined $params{id}) {
92 269         1372 $params{id} = UR::Object::Type->autogenerate_new_object_id_uuid;
93             }
94              
95 537         2246 my %values = $class->defaults_for_register_callback();
96 537         1538 my @specified_params = grep { exists $params{$_} } @required_params_for_register;
  4296         5731  
97 537         1026 @values{@specified_params} = map { delete $params{$_} } @specified_params;
  4278         5554  
98              
99 537         1426 my @bad_params = keys %params;
100 537 50       1759 if (@bad_params) {
101 0         0 Carp::croak('invalid params: ' . join(', ', @bad_params));
102             }
103              
104 537         968 my @missing_params = grep { not exists $values{$_} } @required_params_for_register;
  4296         4709  
105 537 50       1545 if (@missing_params) {
106 0         0 Carp::croak('missing required params: ' . join(', ', @missing_params));
107             }
108              
109 537         1532 my @undef_values = grep { not defined $values{$_} } keys %values;
  4296         4464  
110 537 100       1633 if (@undef_values) {
111 8         1808 Carp::croak('undefined values: ' . join(', ', @undef_values));
112             }
113              
114 529         1013 my $subject_class_name = $values{subject_class_name};
115 529         848 my $subject_class_meta = eval { $subject_class_name->__meta__ };
  529         2559  
116 529 100       1540 if ($@) {
117 1         118 Carp::croak("Can't create observer with subject_class_name '$subject_class_name': Can't get class metadata for class '$subject_class_name': $@");
118             }
119 528 50       1499 unless ($subject_class_meta) {
120 0         0 Carp::croak("Class $subject_class_name cannot be the subject class for an observer because there is no class metadata");
121             }
122              
123 528         917 my $aspect = $values{aspect};
124 528         814 my $subject_id = $values{subject_id};
125 528 100       3146 unless ($subject_class_meta->_is_valid_signal($aspect)) {
126 4     1   16 my $croak = sub { Carp::croak("'$aspect' is not a valid aspect for class $subject_class_name") };
  1         214  
127 4 50       17 unless ($subject_class_name->can('validate_subscription')) {
128 0         0 $croak->();
129             }
130 4 100       51 unless ($subject_class_name->validate_subscription($aspect, $subject_id, $values{callback})) {
131 1         2 $croak->();
132             }
133             }
134              
135             $class->_insert_record_into_all_change_subscriptions(
136             @values{qw(subject_class_name aspect subject_id)},
137 527         3347 [@values{qw(callback note priority id once)}],
138             );
139              
140 527         2330 return $values{id};
141             }
142              
143             sub _insert_record_into_all_change_subscriptions {
144 793     793   1887 my($class,$subject_class_name, $aspect,$subject_id, $new_record) = @_;
145              
146 793 100       2609 if ($subject_class_name eq 'UR::Object') {
147 15         25 $subject_class_name = '';
148             };
149              
150 793   100     5386 my $list = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id} ||= [];
151 793         1883 push @$list, $new_record;
152             }
153              
154             sub _modify_priority {
155 1     1   3 my($self, $aspect, $old_val, $new_val) = @_;
156              
157 1         3 my $subject_class_name = $self->subject_class_name;
158 1         2 my $subject_aspect = $self->aspect;
159 1         3 my $subject_id = $self->subject_id;
160              
161 1         3 my $list = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$subject_aspect}->{$subject_id};
162 1 50       5 return unless $list; # this is probably an error condition
163              
164 1         2 my $data;
165 1         4 for (my $i = 0; $i < @$list; $i++) {
166 1 50       3 if ($list->[$i]->[3] eq $self->id) {
167 1         3 ($data) = splice(@$list,$i, 1);
168 1         2 last;
169             }
170             }
171 1 50       3 return unless $data; # This is probably an error condition...
172              
173 1         2 $data->[2] = $new_val;
174              
175 1         2 $self->_insert_record_into_all_change_subscriptions($subject_class_name, $subject_aspect, $subject_id, $data);
176             }
177              
178             sub callback {
179 268     268 1 573 shift->{callback};
180             }
181              
182             sub subscription {
183             shift->{subscription}
184 0     0 0 0 }
185              
186             sub unregister_callback {
187 40     40 0 560 my $class = shift;
188 40         179 my %params = @_;
189              
190 40         85 my $id = delete $params{id};
191 40 50       156 unless (defined $id) {
192 0         0 Carp::croak('missing required parameter: id');
193             }
194              
195 40         112 my @undef_params = grep { not defined $params{$_} } keys %params;
  117         180  
196 40 50       97 if (@undef_params) {
197 0         0 Carp::croak('undefined params: ' . join(', ', @undef_params));
198             }
199              
200 40         67 my $aspect = delete $params{aspect};
201 40         66 my $subject_class_name = delete $params{subject_class_name};
202 40         51 my $subject_id = delete $params{subject_id};
203              
204 40         83 my @bad_params = keys %params;
205 40 50       108 if (@bad_params) {
206 0         0 Carp::croak('invalid params: ' . join(', ', @bad_params));
207             }
208              
209 40   66     117 my @subject_class_names = $subject_class_name || keys %{$UR::Context::all_change_subscriptions};
210 40         134 for my $subject_class_name (@subject_class_names) {
211 819   100     1065 my @aspects = $aspect || keys %{$UR::Context::all_change_subscriptions->{$subject_class_name}};
212 819         627 for my $aspect (@aspects) {
213 835   100     982 my @subject_ids = $subject_id || keys %{$UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}};
214 835         921 for my $subject_id (@subject_ids) {
215 54         82 my $arrayref = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id};
216 54         140 for (my $i = 0; $i < @$arrayref; $i++) {
217 58 100       185 if ($arrayref->[$i]->[3] eq $id) {
218 30         61 splice(@$arrayref, $i, 1);
219 30 100       111 if (@$arrayref == 0) {
220 26         36 $arrayref = undef;
221 26         44 delete $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id};
222 26 100       25 if (not keys %{ $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect} }) {
  26         87  
223 21         31 delete $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect};
224             }
225             }
226 30         159 return 1;
227             }
228             }
229             }
230             }
231             }
232              
233 10         39 return;
234             }
235              
236             sub delete {
237 39     39 1 44 my $self = shift;
238             #$DB::single = 1;
239              
240 39         172 my $subject_class_name = $self->subject_class_name;
241 39         105 my $subject_id = $self->subject_id;
242 39         166 my $aspect = $self->aspect;
243              
244 39 100 100     221 $subject_class_name = '' if (! $subject_class_name or $subject_class_name eq 'UR::Object');
245 39 100       93 $subject_id = '' unless (defined $subject_id);
246 39 100       121 $aspect = '' unless (defined $aspect);
247              
248 39         129 my $unregistered = $self->unregister_callback(
249             aspect => $aspect,
250             id => $self->id,
251             subject_class_name => $subject_class_name,
252             subject_id => $subject_id,
253             );
254 39 100       89 if ($unregistered) {
255 29 50 66     185 unless ($subject_class_name eq '' || $subject_class_name->inform_subscription_cancellation($aspect, $subject_id, $self->{'callback'})) {
256 0         0 Carp::confess("Failed to validate requested subscription cancellation for aspect '$aspect' on class $subject_class_name");
257             }
258             }
259 39         224 $self->SUPER::delete();
260             }
261              
262             sub __rollback__ {
263 0     0     my $self = shift;
264 0           return UR::Observer::delete($self);
265             }
266              
267             sub get_with_special_parameters {
268 0     0 0   my($class,$rule,%extra) = @_;
269              
270 0           my $callback = delete $extra{'callback'};
271 0 0         if (keys %extra) {
272 0           Carp::croak("Unrecognized parameters in get(): " . join(', ', keys(%extra)));
273             }
274 0           my @matches = $class->get($rule);
275 0           return grep { $_->callback eq $callback } @matches;
  0            
276             }
277              
278             1;
279              
280              
281             =pod
282              
283             =head1 NAME
284              
285             UR::Observer - bind callbacks to object changes
286              
287             =head1 SYNOPSIS
288              
289             $rocket = Acme::Rocket->create(
290             fuel_level => 100
291             );
292            
293             $observer = $rocket->add_observer(
294             aspect => 'fuel_level',
295             callback =>
296             sub {
297             print "fuel level is: " . shift->fuel_level . "\n"
298             },
299             priority => 2,
300             );
301              
302             $observer2 = UR::Observer->create(
303             subject_class_name => 'Acme::Rocket',
304             subject_id => $rocket->id,
305             aspect => 'fuel_level',
306             callback =>
307             sub {
308             my($self,$changed_aspect,$old_value,$new_value) = @_;
309             if ($new_value == 0) {
310             print "Bail out!\n";
311             }
312             },
313             priority => 0
314             );
315              
316              
317             for (3 .. 0) {
318             $rocket->fuel_level($_);
319             }
320             # fuel level is: 3
321             # fuel level is: 2
322             # fuel level is: 1
323             # Bail out!
324             # fuel level is: 0
325            
326             $observer->delete;
327              
328             =head1 DESCRIPTION
329              
330             UR::Observer implements the observer pattern for UR objects. These observers
331             can be attached to individual object instances, or to whole classes. They
332             can send notifications for changes to object attributes, or to other state
333             changes such as when an object is loaded from its datasource or deleted.
334              
335             =head1 CONSTRUCTOR
336              
337             Observers can be created either by using the method C on
338             another class, or by calling C on the UR::Observer class.
339              
340             my $o1 = Some::Other::Class->add_observer(...);
341             my $o2 = $object_instance->add_observer(...);
342             my $o3 = UR::Observer->create(...);
343              
344             The constructor accepts these parameters:
345              
346             =over 2
347              
348             =item subject_class_name
349              
350             The name of the class the observer is watching. If this observer is being
351             created via C, then it figures out the subject_class_name
352             from the class or object it is being called on.
353              
354             =item subject_id
355              
356             The ID of the object the observer is watching. If this observer is being
357             created via C, then it figures out the subject_id from the
358             object it was called on. If C was called as a class method,
359             then subject_id is omitted, and means that the observer should fire for
360             changes on any instance of the class or sub-class.
361              
362             =item priority
363              
364             A numeric value used to determine the order the callbacks are fired. Lower
365             numbers are higher priority, and are run before callbacks with a numerically
366             higher priority. The default priority is 1. Negative numbers are ok.
367              
368             =item aspect
369              
370             The attribute the observer is watching for changes on. The aspect is commonly
371             one of the properties of the class. In this case, the callback is fired after
372             the property's value changes. aspect can be omitted, which means the observer
373             should fire for any change in the object state. If both subject_id and aspect
374             are omitted, then the observer will fire for any change to any instance of the
375             class.
376              
377             There are other, system-level aspects that can be watched for that correspond to other types
378             of state change:
379              
380             =over 2
381              
382             =item create
383              
384             After a new object instance is created
385              
386             =item delete
387              
388             After an n object instance is deleted
389              
390             =item load
391              
392             After an object instance is loaded from its data source
393              
394             =item commit
395              
396             After an object instance has changes saved to its data source
397              
398             =back
399              
400             =item callback
401              
402             A coderef that is called after the observer's event happens. The coderef is
403             passed four parameters: $self, $aspect, $old_value, $new_value. In this case,
404             $self is the object that is changing, not the UR::Observer instance (unless,
405             of course, you have created an observer on UR::Observer). The return value of
406             the callback is ignored.
407              
408             =item once
409              
410             If the 'once' attribute is true, the observer is deleted immediately after
411             the callback is run. This has the effect of running the callback only once,
412             no matter how many times the observer condition is triggered.
413              
414             =item note
415              
416             A text string that is ignored by the system
417              
418             =back
419              
420             =head2 Custom aspects
421              
422             You can create an observer for an aspect that is neither a property nor one
423             of the system aspects by listing the aspect names in the metadata for the
424             class.
425              
426             class My::Class {
427             has => [ 'prop_a', 'another_prop' ],
428             valid_signals => ['custom', 'pow' ],
429             };
430              
431             my $o = My::Class->add_observer(
432             aspect => 'pow',
433             callback => sub { print "POW!\n" },
434             );
435             My::Class->__signal_observers__('pow'); # POW!
436              
437             my $obj = My::Class->create(prop_a => 1);
438             $obj->__signal_observers__('custom'); # not an error
439              
440             To help catch typos, creating an observer for a non-standard aspect throws an
441             exception unless the named aspect is in the list of 'valid_signals' in the
442             class metadata. Nothing in the system will trigger these observers, but they
443             can be triggered in your own code using the C<__signal_observers()__> class or
444             object method. Sending a signal for an aspect that no observers are watching
445             for is not an error.
446              
447             =cut
448