Editors Note, February 14th 2022: This project is now ABANDONED and no longer supported or updated.

asherwunk/phabstractic now implements a common event object, useful in the “Universal Event System”.

The Universal Event System

What I call the universal event system is basically several constructs built on top of the Publisher/Observer design pattern that asherwunk/phabstractic also implements.  The idea is that certain events happen in code, and objects listening for those events are notified.  I have built objects that include an actor, a conduit, a handler, which act on events, funnel events, categorize and fire off lists of actions, etc.

The Generic Event

The core of this system is the event object which I have designed to be very versatile.  The event object has many fields on it, including line number, file, tags, categories, and such, all of which are optional.  I decided events would be the “state” change that is observed in the Publisher/Observer design pattern I implemented.  If you remember, the StateInterface, in that case, looks like this (github):

namespace Phabstractic\Patterns\Resource
{
    
    interface StateInterface
    {
        
        public function getState();
        
        public function setState($stateData);
    }
    
}

As usual, with my PHP designs, I’ve implemented an interface for a generic event first.  I’ve decided I want to keep track of a couple of things.  First, we’ll give each event an identifier that is going to be unique or should be unique, to each event object.  Then I thought about how events are used in other frameworks, and one of the things that stood out to me was that an event usually has a ‘target’ that links to the object or memory address that fired the event, or is associated with the event.  So I have a target.  On top of that, I took a cue from WordPress actually and decided that each event would have categories and tags, much like a WordPress post.  Also, an event should be able to carry some kind of ‘payload’ or data associated with that event, so I added a data property.  The event can also indicate what class, method, and namespace fired the event.  I took some inspiration from other event systems, including the JavaScript browser event system that “bubbles.”  In this event system events that are fired will ‘bubble’ up to parents.  I took some liberty with this idea in later constructs of the Universal Event System, but the idea that an event would continue through a chain of things stuck with me, so I decided there should be functions to force/subdue an event.  I ended up with this interface (github):

namespace Phabstractic\Event\Resource
{
    // ...

    use Phabstractic\Patterns\Resource as PatternsResource;
    
    /**
     * Event Interface - Defines basic event functionality
     * 
     * This interface defines the basic functionality and members
     * that all events share.  This serves as the universal
     * event type checker.  All recognizable event in the
     * event system must implement this interface.
     * 
     * Technically an event is an object 'state', when the
     * state changes in a publisher, it's like 'triggering'
     * an event.
     * 
     * ...
     *
     */
    interface EventInterface extends PatternsResource\StateInterface
    {
        public function getState();
        
        public function setState($state);
        
        /**
         * This sets or morphs an event with new information
         * 
         * If morph is set, the event doesn't clear it's
         * information, and instead overwrites whats already there.
         * 
         * @param Phabstractic\Event\Resource\EventInterface $event
         *                  The state information encapsulated in an object
         * @param bool $morph
         *                  Whether we should replace or overwrite the object state
         * 
         */
        public function setStateWithEvent(EventInterface $event, $morph = true);
        
        /**
         * This sets or morphs an event with new information
         * 
         * If morph is set, the event doesn't clear it's
         * information, and instead overwrites whats already there.
         * 
         * @param array $event
         *                  The state information in an associative array
         * @param bool $morph
         *                  Whether we should replace or overwrite the object state
         * 
         */
        public function setStateWithArray(array $state, $morph = true);
        
        /**
         * Retrieve the event identifier
         * ... 
         */
        public function getIdentifier();
        
        /**
         * Return the categories of the event
         * ...
         */
        public function getCategories();
        
        /**
         * Add a category to the event
         * ...
         */
        public function addCategory($category);
        
        /**
         * Set a bunch of categories at once
         * ...
         */
        public function setCategories(array $categories);
        
        /**
         * Remove a category from the event
         * ...
         */
        public function removeCategory($category);

        /**
         * Does this event have a particular category?
         * ...
         */
        public function isCategory($category);

        /**
         * Get the tags associated with this event
         * ...
         */
        public function getTags();

        /**
         * Associate a tag with this event
         * ...
         */
        public function addTag($tag);

        /**
         * Set a bunch of tags to be associated with this event
         * ...
         */
        public function setTags($tags);

        /**
         * Dissasociate a particular tag from this event
         * ...
         */
        public function removeTag($tag);

        /**
         * Is a tag associated with this particular event
         * ...
         */
        public function isTag($tag);

        /**
         * Returns the generator of the event
         * ...
         */
        public function getTarget();

        /**
         * Returns the generator of the event's reference
         * ...
         */
        public function &getTargetReference();

        /**
         * Sets the generator of the event
         * ...
         */
        public function setTarget($target);

        /**
         * Sets the generator of the event's reference
         * ...
         */
        public function setTargetReference(&$target);

        /**
         * Retrieve the data associated with the event
         * ...
         */
        public function getData();

        /**
         * Retrieve the data associated with the event as a reference
         * ...
         */
        public function &getDataReference();

        /**
         * Set the data associated with the event
         * ...
         */
        public function setData($data);
        
        /**
         * Set the data associated with the event as a reference
         * ...
         */
        public function setDataReference(&$data);
        
        /**
         * Retrieve the event originating function
         * ...
         */
        public function getFunction();
        
        /**
         * Retrieve the event originating class
         * ...
         */
        public function getClass();

        /**
         * Retrieve the event originating namespace
         * ...
         */
        public function getNamespace();

        /**
         * Stop propagation of the event
         * ...
         */
        public function stop();

        /**
         * Allow the event to continue propogating
         * 
         */
        public function proceed();

        /**
         * Has the propogation of this event stopped?
         * ...
         */
        public function isStopped();

        /**
         * Make this event unstoppable
         * 
         */
        public function force();

        /**
         * Make this event stoppable
         * 
         */
        public function subdue();

        /**
         * Is this event unstoppable?
         * ...
         */
        public function isUnstoppable();
        
    }
    
}

From there it’s a hop to the AbstractEvent.  This implements the basic functionality of a GenericEvent object that applies to all implementations.  For instance, the AbstractEvent handles the implementation of the tags and categories as Sets (see the asherwunk/phabstractic abstract data type Sets post).  As well, it handles all the non-implementation-specific property setting and getting that we outlined above in the interface.  This includes the stoppable, subdue-able, data, tags, etc. properties.  The identifier and how to set the data of the event are really the most implementation-specific things not quite covered by the AbstractEvent.  Below you can see how I implemented the tags, the translations from objects to arrays and such, and the constructor in AbstractEvent (github):

namespace Phabstractic\Event\Resource
{
    // ...

    use Phabstractic\Data\Types;
    use Phabstractic\Event\Exception;
    
    abstract class AbstractEvent implements EventInterface
    {
        
        protected $identifier;
        
        protected $tags;
        
        protected $categories;
        
        protected $target;
        
        protected $data;
        
        protected $class;
        
        protected $function;
        
        protected $namespace;
        
        protected $stop = false;
        
        protected $force = false;
        
        public function __construct(
            $target,
            $function,
            $class,
            $namespace,
            $data = null,
            array $tags = array(),
            array $categories = array()
        ) {
            $this->tags = new Types\Set($tags, array('unique' => true));
            $this->categories = new Types\Set($categories, array('unique' => true));
            
            $this->setTarget($target);
            $this->function = $function;
            $this->class = $class;
            $this->namespace = $namespace;
            $this->setData($data);
        }
        
        public function getState()
        {
            $tags = $this->tags->getPlainArray();
            $categories = $this->categories->getPlainArray();
            $fields = get_class_vars('Phabstractic\\Event\\Resource\\AbstractEvent');
            
            // Populate the fields with this object's values
            foreach ($fields as $key => $val) {
                if ($key != 'categories' && $key != 'tags') {
                    $fields[$key] = $this->$key;
                }
            }
            
            unset($fields['tags']);
            unset($fields['categories']);
            
            $ret = array(); // Populate the object with additional information
            
            $ret['tags'] = $tags;
            $ret['categories'] = $categories;
            $ret['fields'] = $fields;
            return $ret;
        }
        
        public function setState($state)
        {
            if (is_array($state)) {
                $this->setStateWithArray($state);
            } elseif ($state instanceof EventInterface) {
                $this->setStateWithEvent($state);
            }
            
        }
        
        public function setStateWithArray(array $state, $morph = true)
        {
            /* state: identifier, tags, categories, target, data,
               class, function, namespace */
            
            if (!$morph) {
                $this->tags->clear();
                $this->categories->clear();
                $this->target = null;
                $this->data = null;
                $this->class = '';
                $this->function = '';
                $this->namespace = '';
            }
            
            if (isset($state['fields'])) {
                $state = array_merge($state, $state['fields']);
                unset($state['fields']);
            }
            
            $fields = array('tags', 'categories');
            
            // This code is the same for both tags and categories so, use same code
            foreach($fields as $field) {
                if (isset($state[$field])) {
                    if (is_array($state[$field])) {
                        if (!$morph) {
                            $this->$field = new Types\Set(
                                                    $state[$field],
                                                    array( 'unique' => true ));
                        } else {
                            foreach( $state[$field] as $datum ) {
                                $this->$field->add($datum);
                            }
                        }
                            
                    } else if (is_object($state[$field]) &&
                               method_exists($state[$field], 'getArray')) {
                        if (!$morph) {
                            $this->$field = new Types\Set(
                                                array($state[$field]->getArray()),
                                                array('unique' => true));
                        } else {
                            $this->$field = new Types\Set(
                                                Types\Set::union(
                                                    $state[$field],
                                                    $this->$field),
                                                array('unique' => true));
                        }
                    } else {
                        throw new Exception\InvalidArgumentException(
                            'Phabstractic\\Event\\Resource\\AbstractEvent->' .
                            "setState: invalid $fields argument");
                    }
                } else {
                    if (!$morph) {
                        $this->$field->clear();
                    }
                }
            }
            
            if (isset($state['target']) && $state['target']) {
                $this->target = $state['target'];
            }
            
            if (isset($state['data']) && $state['data']) {
                $this->data = $state['data'];
            }
            
            if (isset($state['class']) && $state['class']) {
                $this->class = $state['class'];
            }
            
            if (isset($state['function']) && $state['function']) {
                $this->function = $state['function'];
            }
            
            if (isset($state['namespace']) && $state['namespace']) {
                $this->namespace = $state['namespace'];
            }
            
            if (isset($state['stop'])) {
                $this->stop = $state['stop'];
            }
            
            if (isset($state['force'])) {
                $this->force = $state['force'];
            }
        }
        
        public function setStateWithEvent(
            EventInterface $state,
            $morph = true)
        {
            if (!$morph) {
                $this->tags->clear();
                $this->categories->clear();
                $this->target = null;
                $this->data = null;
                $this->class = '';
                $this->function = '';
                $this->namespace = '';
            }
            
            $fields = array('tags', 'categories');
            
            // This code is the same for both tags and categories so, use same code
            foreach($fields as $field) {
                $accessor = 'get' . ucfirst($field);
                $this->$field = new Types\Set(
                                        Types\Set::union(
                                            $this->$field,
                                            $state->$accessor()),
                                        array('unique' => true));
            }
            
            if (($target = $state->getTarget()) != null) {
                $this->target = $target;
            }
            
            if (($data = $state->getData()) != null) {
                $this->data = $data;
            }
            
            if (($class = $state->getClass()) != null) {
                $this->class = $class;
            }
            
            if (($function = $state->getFunction()) != null) {
                $this->function = $function;
            }
            
            if (($namespace = $state->getNamespace()) != null) {
                $this->namespace = $namespace;
            }
            
            $this->stop = $state->isStopped();
            $this->force = $state->isUnstoppable();
            
        }
        
        abstract public function getIdentifier();
        
        public function getCategories()
        {
            return $this->categories->getPlainArray();
        }
        
        public function getCategoriesSet()
        {
            return $this->categories;
        }
        
        public function addCategory($category)
        {
            $this->categories->add($category);
        }
        
        public function addCategories(array $categories)
        {
            foreach ($categories as $category) {
                $this->addCategory($category);
            }
        }
        
        public function setCategories(array $categories)
        {
            $this->categories->clear();
            
            $this->addCategories($categories);
        }
        
        public function removeCategory($category)
        {
            $this->categories->remove($category);
        }
        
        public function isCategory($category)
        {
            return $this->categories->in($category);
        }
        
        public function getTags()
        {
            return $this->tags->getPlainArray();
        }
        
        public function getTagsSet()
        {
            return $this->tags;
        }
        
        public function addTag($tag)
        {
            $this->tags->add($tag);
        }
        
        public function addTags($tags)
        {
            foreach ($tags as $tag) {
                $this->addTag($tag);
            }
        }
        
        public function setTags($tags)
        {
            $this->tags->clear();
            
            $this->addTags($tags);
        }
        
        public function removeTag($tag)
        {
            $this->tags->remove($tag);
        }
        
        public function isTag($tag)
        {
            return $this->tags->in($tag);
        }
        
        public function getTarget()
        {
            return $this->target;
        }
        
        public function &getTargetReference()
        {
            return $this->target;
        }
        
        public function setTarget($target)
        {
            $this->target = $target;
        }
        
        public function setTargetReference(&$target)
        {
            $this->target = &$target;
        }
        
        public function getData()
        {
            return $this->data;
        }
        
        public function &getDataReference()
        {
            return $this->data;
        }
        
        abstract public function setData($data);
        
        abstract public function setDataReference(&$data);
        
        public function getFunction()
        {
            return $this->function;
        }
        
        public function getClass()
        {
            return $this->class;
        }
        
        public function getNamespace()
        {
            return $this->namespace;
        }
        
        public function stop()
        {
            if (!$this->force) {
                $this->stop = true;
            }
            
        }
        
        public function proceed()
        {
            $this->stop = false;;
        }
        
        public function isStopped()
        {
            return $this->stop;
        }
        
        public function force()
        {
            $this->force = true;
        }
        
        public function subdue()
        {
            $this->force = false;
        }
        
        public function isUnstoppable()
        {
            return $this->force;
        }
        
    }
    
}

That brings us to the actual implementation of GenericEvent.  This is pretty simple, it basically picks up where AbstractEvent leaves off.  This means that we have to implement a constructor for the identifier, and we have to implement the methods that set the data ‘payload’ of the event object.  Pretty simple stuff (github):

namespace Phabstractic\Event
{
    // ...

    use Phabstractic\Event\Resource as EventResource;
    use Phabstractic\Features;
    
    class GenericEvent extends EventResource\AbstractEvent
    {
        use Features\IdentityTrait;
        
        public function __construct(
            $target,
            $function,
            $class,
            $namespace,
            $data = null,
            array $tags = array(),
            array $cats = array()
        ) {
            parent::__construct(
                $target,
                $function,
                $class,
                $namespace,
                $data,
                $tags,
                $cats);
            $this->identityPrefix = 'GenericEvent';
            $this->setIdentifier();
        }
        
        public function getIdentifier()
        {
            return $this->identifier;
        }
        
        protected function setIdentifier($identifier = '')
        {
            if ($identifier) {
                $this->identifier = $identifier;
            } else {
                $this->identifier = $this->getNewIdentity();
            }
        }
        
        public function setData($data)
        {
            $this->data = $data;
        }
        
        public function setDataReference(&$data)
        {
            $this->data = &$data;
        }
        
        // ...
        
    }
    
}

Conclusion

By associating so much information with a simple event, including arbitrary data, we can build an event framework with a lot of subtlety.  It is possible to build filters then that can decide when and where to further propagate events based on their tags or categories for instance (and in fact we do, stay tuned!)  It is possible then to build objects that act like a “Front Controller” for events in a whole system, what I call a “conduit”.

Events are powerful objects/concepts and enable a ‘reactive’ programming paradigm, where pieces of program code execute only when certain things happen.  You can think of it sort of like this: with procedural we go from start to finish, with object-oriented we go from object to object, and with events, we jump around based on what else happens in the rest of the program.  Events are a powerful flow-control mechanism that can be used for many purposes.

This is part of the Phabstractic Library.

photo credit: Alex-de-Haas The Musician. via photopin (license)

Asher Wolfstein

Metaverse Resident

About the Author

A metaverse resident, you can find me on Second Life (kadar.talbot) and other online platforms. I write about my digital life, my musings, and my projects as a programmer, webmaster, artist, and game designer. (exist (be wunk) (use rational imagination) (import artist coder maker furry) (conditional (if (eq you asshole) (me (block you))))

View Articles