Evented-Object
view release on metacpan or search on metacpan
lib/Evented/Object.pm view on Meta::CPAN
# Copyright (c) 2011-17, Mitchell Cooper
#
# Evented::Object: a simple yet featureful base class event framework.
#
# Evented::Object is based on the libuic UIC::Evented::Object:
# ... which is based on Evented::Object from foxy-java IRC Bot,
# ... which is based on Evented::Object from Arinity IRC Services,
# ... which is based on Evented::Object from ntirc IRC Client,
# ... which is based on IRC::Evented::Object from libirc IRC Library.
#
# Evented::Object and its very detailed documentation can be found
# in their latest versions at https://github.com/cooper/evented-object.
#
package Evented::Object;
use warnings;
use strict;
use utf8;
use 5.010;
# these must be set before loading EventFire.
our ($events, $props, %monitors);
BEGIN {
$events = 'eventedObject.events';
$props = 'eventedObject.props';
}
use Scalar::Util qw(weaken blessed);
use Evented::Object::EventFire;
use Evented::Object::Collection;
our $VERSION = '5.68';
# creates a new evented object.
sub new {
my ($class, %opts) = @_;
bless \%opts, $class;
}
#############################
### REGISTERING CALLBACKS ###
#############################
# ->register_callback()
#
# aliases: ->register_event(), ->on()
# attaches an event callback.
#
# $eo->register_callback(myEvent => sub {
# ...
# }, 'some.callback.name', priority => 200);
#
sub register_callback {
my ($eo, $event_name, $code, @opts_) = @_;
# if there is an odd number of options, the first is the callback name.
# this also implies with_eo.
my %opts;
if (@opts_ % 2) {
%opts = (
name => shift @opts_,
with_eo => 1,
@opts_
);
}
else {
%opts = @opts_;
}
# no name was provided, so we shall construct one using pure hackery.
# this is one of the most criminal things I've ever done.
my @caller = caller;
if (!defined $opts{name}) {
state $c = -1; $c++;
$opts{name} = "$event_name:$caller[0]($caller[2],$c)";
}
# determine the event store.
my $event_store = _event_store($eo);
# before/after a callback.
my $priority = delete $opts{priority} || 0;
if (defined $opts{before} or defined $opts{after}) {
$priority = 'nan';
# nan priority indicates it should be determined at a later time.
}
# add the callback.
lib/Evented/Object.pm view on Meta::CPAN
return $collection;
}
#####################
### FIRING EVENTS ###
#####################
# ->fire_event()
#
# prepares an event and then fires it.
#
sub fire_event {
shift->prepare_event(shift, @_)->fire(caller => [caller 1]);
}
# ->fire_events_together()
# fire_events_together()
#
# prepares several events and then fires them together.
#
sub fire_events_together {
prepare_together(@_)->fire(caller => [caller 1]);
}
# ->fire_once()
#
# prepares an event, fires it, and deletes all callbacks afterward.
#
sub fire_once {
my ($eo, $event_name, @args) = @_;
# fire with this caller.
my $fire = $eo->prepare_event($event_name, @args)->fire(
caller => [caller 1]
);
# delete the event.
$eo->delete_event($event_name);
return $fire;
}
########################
### LISTENER OBJECTS ###
########################
# ->add_listener()
#
# adds an object as a listener of another object's events.
# see "listeners" in the documentation.
#
sub add_listener {
my ($eo, $obj, $prefix) = @_;
# find listeners list.
my $listeners = $eo->{$props}{listeners} ||= [];
# store this listener.
push @$listeners, [$prefix, $obj];
# weaken the reference to the listener.
weaken($listeners->[$#$listeners][1]);
return 1;
}
# ->delete_listener()
#
# removes an object which was listening to another object's events.
# see "listeners" in the documentation.
#
sub delete_listener {
my ($eo, $obj) = @_;
return 1 unless my $listeners = $eo->{$props}{listeners};
@$listeners = grep {
ref $_->[1] eq 'ARRAY' and $_->[1] != $obj
} @$listeners;
return 1;
}
######################
### CLASS MONITORS ###
######################
# for objective use $eo->monitor_events($pkg)
sub monitor_events { add_class_monitor(reverse @_) }
sub stop_monitoring { delete_class_monitor(reverse @_) }
# add_class_monitor()
#
# set the monitor object of a class.
#
# TODO: honestly class monitors need to track individual callbacks so that the
# monitor is notified of all deletes of callbacks added by the class being
# monitored even if the delete action was not committed by that package.
#
sub add_class_monitor {
my ($pkg, $obj) = @_;
# ensure it's an evented object.
return unless $obj->isa(__PACKAGE__);
# it's already in the list.
my $m = $monitors{$pkg} ||= [];
return if grep { $_ == $obj } @$m = grep { defined } @$m;
# hold a weak reference to the monitor.
push @$m, $obj;
weaken($monitors{$pkg}[$#$m]);
return 1;
}
# delete_class_monitor()
#
# remove a class monitor object from a class.
#
sub delete_class_monitor {
my ($pkg, $obj) = @_;
my $m = $monitors{$pkg} or return;
@$m = grep { defined && $_ != $obj } @$m;
}
#######################
### CLASS FUNCTIONS ###
#######################
# safe_fire($obj, event => ...)
#
# checks that an object is blessed and that it is an evented object.
# if so, prepares and fires an event with optional arguments.
#
sub safe_fire {
my $obj = shift;
return if !blessed $obj || !$obj->isa(__PACKAGE__);
return $obj->fire_event(@_);
}
#########################
### INTERNAL ROUTINES ###
#########################
# access package storage.
sub _package_store {
my $package = shift;
no strict 'refs';
my $ref = "${package}::__EO__";
if (!keys %$ref) {
%$ref = ();
}
return *$ref{HASH};
}
# fetch the event store of object or package.
sub _event_store {
my $eo = shift;
return $eo->{$events} ||= {} if blessed $eo;
my $store = _package_store($eo);
return $store->{events} ||= {} if not blessed $eo;
}
# fetch the property store of object or package.
sub _prop_store {
my $eo = shift;
return $eo->{$props} ||= {} if blessed $eo;
my $store = _package_store($eo);
return $store->{props} ||= {} if not blessed $eo;
}
# fetch a callback from its name.
sub _get_callback_named {
my ($eo, $event_name, $callback_name) = @_;
foreach my $callback (@{ _get_callbacks($eo, $event_name) }) {
return $callback if $callback->[2]{name} eq $callback_name
}
return;
}
# fetches callbacks of an event.
# internal use only.
sub _get_callbacks {
my ($eo, $event_name, @args) = @_;
my (%callbacks, %callback_names);
# start out with two stores: the object and the package.
my @stores = (
[ $event_name => $eo->{$events} ],
[ $event_name => _event_store(blessed $eo) ]
);
# if there are any listening objects, add those stores.
if (my $listeners = $eo->{$props}{listeners}) {
my @delete;
LISTENER: foreach my $i (0 .. $#$listeners) {
my $l = $listeners->[$i] or next;
my ($prefix, $lis) = @$l;
my $listener_event_name = $prefix.q(.).$event_name;
# object has been deallocated by garbage disposal,
# so we can delete this listener.
if (!$lis) {
push @delete, $i;
next LISTENER;
}
push @stores, [ $listener_event_name => $lis->{$events} ];
}
# delete listeners if necessary.
splice @$listeners, $_, 1 foreach @delete;
}
# add callbacks from each store.
foreach my $st (@stores) {
my ($event_name, $event_store) = @$st;
my $store = $event_store->{$event_name} or next;
foreach my $priority (keys %$store) {
# create a group reference.
my $group_id = "$eo/$event_name";
my $group = [ $eo, $event_name, \@args, $group_id ];
weaken($group->[0]);
# add each callback set. inject callback name.
foreach my $cb_ref (@{ $store->{$priority} }) {
my %cb = %$cb_ref; # make a copy
$cb{id} = "$group_id/$cb{name}";
$callbacks{ $cb{id} } = [ $priority, $group, \%cb ];
$callback_names{$group_id}{ $cb{name} } = $cb{id};
}
}
}
return wantarray ? (\%callbacks, \%callback_names) : \%callbacks;
}
# fire a class monitor event.
sub _monitor_fire {
my ($pkg, $event_name, @args) = @_;
my $m = $monitors{$pkg} or return;
safe_fire($_, "monitor:$event_name" => @args) foreach @$m;
}
sub DESTROY { shift->delete_all_events }
###############
### ALIASES ###
###############
sub register_event;
sub register_events;
sub delete_event;
sub on;
sub del;
sub fire;
BEGIN {
*register_event = *register_callback;
*register_events = *register_callbacks;
*delete_event = *delete_callback;
*on = *register_callback;
*del = *delete_callback;
*fire = *fire_event;
}
1;
=head1 NAME
B<Evented::Object> - base class which allows you to attach callbacks to
objects and then fire events on them.
=head1 SYNOPSIS
package Person;
use warnings;
use strict;
use 5.010;
use parent 'Evented::Object';
( run in 0.957 second using v1.01-cache-2.11-cpan-39bf76dae61 )