Year2009

Yet Another EventManager Class in PHP (5.3)

This EventManager class is for PHP 5.3. It has got the same features as Yet Another EventManager Class in PHP (<5.3):

  1. EventManager is a Singleton
  2. EventManager events are strings
  3. EventManager bind() can attach handlers to regular expressions
  4. EventManager event handler arguments are loosely structured
  5. EventManager event handler return value is loosely structured
  6. EventManager gathers, stores, and eventually returns all handlers results

plus the following:

  1. EventManager belongs to the AndoCore namespace
  2. EventManager event handlers can be any PHP callable expression
  3. EventManager bind() / unbind() work with Callable objects
EventManager belongs to the AndoCore namespace

As a namespace for my projects I chose Ando, which is short, and tells a bit about me (Andrea), and a bit about Miho, my wife, which once called me Andorea.

EventManager event handlers can be any PHP callable expression

Global functions, class methods, object methods, and anonymous functions are all supported.

EventManager bind() / unbind() work with Callable objects

Not only bind() accepts any callable expression, but also accepts and returns an object of the class AndoCoreCallable. This way, for unbinding an event handler, the Callable object that bind() returns must be passed to unbind().

Callable objects are just a wrapper around callable expressions, that allows for a uniform access and storage.

namespace Ando\Core;

class Callable
{
    private $_callable = null;
    
    public function __construct( $callable )
    {
        if (! is_callable( $callable ))
        {
            throw new Exception('Expected a callable expression');
        }
        $this->_callable = $callable;
    }
    
    public function __invoke($args, & $stack)
    {
        $result = call_user_func_array($this->_callable,
                array_merge($args, array(& $stack)));
        return $result;
    }
}

How to use many EventManager singletons at once

require_once 'event_manager.php';

class AnotherEventManager extends Ando\Core\EventManager {}
Ando\Core\Singleton::instance('AnotherEventManager');

function hello($event, $world)
{
    echo "Hello $world !<br>";
}

EventManager()->bind('hello', 'hello');
//AnotherEventManager()->bind('hello', 'hello');

AnotherEventManager()->trigger('hello', 'Popeye');

As is, the previous scripts outputs nothing, but uncommenting the commented line you get

Hello Popeye !
Action Wrapping

class Foo
{
  public function foo()
  {
    //...
    EventManager()->trigger('action:one:before', 3);
    sleep(3); //an action you want to wrap
    EventManager()->trigger('action:one:after', null);
    //...
  }
}

class Bar
{
  public function bar()
  {
    //...
    EventManager()->trigger('action:two:before', 2);
    sleep(2); //another action you want to wrap
    EventManager()->trigger('action:two:after', null);
    //...
  }
}

class ActionTracerPlugin
{
    public function trace($event, $data)
    {
        if (preg_match('/^action:(.*)$/', $event, $matches))
        {
            $action = $matches[1];
            $time = date('Y-m-d H:i:s');
            $data = var_export($data, true);
            file_put_contents('trace.txt', "$time: $action: $data\n", FILE_APPEND);
        }
    }
}



require_once 'event_manager.php';

$tracer = new ActionTracerPlugin();
EventManager()->bind('/:(before|after)$/', array($tracer, 'trace'));

$foo = new Foo();
$foo->foo();

$bar = new Bar();
$bar->bar();

$foo->foo();

echo '<pre>', file_get_contents('trace.txt'), '</pre>';

Trace

2009-11-16 07:17:29: one:before: 3
2009-11-16 07:17:32: one:after: NULL
2009-11-16 07:17:32: one:before: 3
2009-11-16 07:17:35: one:after: NULL
2009-11-16 07:17:35: two:before: 2
2009-11-16 07:17:37: two:after: NULL
2009-11-16 07:17:37: two:before: 2
2009-11-16 07:17:39: two:after: NULL
2009-11-16 07:17:39: one:before: 3
2009-11-16 07:17:42: one:after: NULL
Runtime Class Extension

class Foo
{
    public function __call($method, $arguments)
    {
        action('__call:start', $method);
        
        $stack = EventManager()->trigger(__METHOD__, $this, $method, $arguments);
        if (! count($stack))
        {
            action('__call:exception');
            throw new Exception(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
        }

        $last = array_pop($stack);
        $result = $last['result'];
        action('__call:end', $result);
        return $result;
    }
}

class Bar
{
    public function barMethodForFoo($event, $foo, $method, $arguments, $stack)
    {
        action('barMethodForFoo:start', $arguments);
        if (! ($foo instanceof Foo && 'bar' == $method))
        {
            array_pop($stack);
            return;
        }
        action('barMethodForFoo:begin');
        
        $someValue = 'Hello ' . $arguments[0] . ' !';

        action('barMethodForFoo:end', $someValue);
        return $someValue;
    }
}

class Baz
{
    public function bazMethodForFoo($event, $foo, $method, $arguments, $stack)
    {
        action('bazMethodForFoo:start', $arguments);
        if (! ($foo instanceof Foo && 'baz' == $method))
        {
            array_pop($stack);
            return;
        }
        action('bazMethodForFoo:begin');
        
        $someValue = 'I love ' . $arguments[0] . ' !';

        action('bazMethodForFoo:end', $someValue);
        return $someValue;
    }
}

class ActionTracerPlugin
{
    public function trace($event, $data)
    {
        if (preg_match('/^action:(.*)$/', $event, $matches))
        {
            $action = $matches[1];
            $time = date('Y-m-d H:i:s');
            $data = var_export($data, true);
            file_put_contents('trace.txt', "$time: $action: $data\n", FILE_APPEND);
        }
    }
}

function action($action, $data = null)
{
    EventManager()->trigger("action:$action", $data);
}



require_once 'event_manager.php';

$tracer = new ActionTracerPlugin();
EventManager()->bind('/^action:/', array($tracer, 'trace'));

$foo = new Foo();
$bar = new Bar();
$baz = new Baz();

EventManager()->bind('Foo::__call', array($bar, 'barMethodForFoo'));

$foo->bar("Olivia");
    
try
{
    action('baz without bind:before');
    $foo->baz("spinach");
    action('baz without bind:after');
}
catch (Exception $e)
{
    EventManager()->bind('Foo::__call', array($baz, 'bazMethodForFoo'));
    
    action('baz with bind:before');
    $foo->baz("you");
    action('baz with bind:after');
}

echo '<pre>', file_get_contents('trace.txt'), '</pre>';

Trace

2009-11-16 07:14:54: __call:start: 'bar'
2009-11-16 07:14:54: barMethodForFoo:start: array (
  0 => 'Olivia',
)
2009-11-16 07:14:54: barMethodForFoo:begin: NULL
2009-11-16 07:14:54: barMethodForFoo:end: 'Hello Olivia !'
2009-11-16 07:14:54: __call:end: 'Hello Olivia !'
2009-11-16 07:14:54: baz without bind:before: NULL
2009-11-16 07:14:54: __call:start: 'baz'
2009-11-16 07:14:54: barMethodForFoo:start: array (
  0 => 'spinach',
)
2009-11-16 07:14:54: __call:exception: NULL
2009-11-16 07:14:54: baz with bind:before: NULL
2009-11-16 07:14:54: __call:start: 'baz'
2009-11-16 07:14:54: barMethodForFoo:start: array (
  0 => 'you',
)
2009-11-16 07:14:54: bazMethodForFoo:start: array (
  0 => 'you',
)
2009-11-16 07:14:54: bazMethodForFoo:begin: NULL
2009-11-16 07:14:54: bazMethodForFoo:end: 'I love you !'
2009-11-16 07:14:54: __call:end: 'I love you !'
2009-11-16 07:14:54: baz with bind:after: NULL

Filters Pipeline

class Foo
{
    public function show($someThing)
    {
        $stack = EventManager()->trigger('filter:show', $someThing);
        $result = array_pop($stack);
        echo $result['result'];
    }
}

class Bar
{
    static public function the_to_a($event, $someThing, $stack)
    {
        if (! preg_match('/^filter:/', $event))
        {
            return array_pop($stack);
        }
        $result = EventManager()->pipeline($someThing, $stack);
        $result = preg_replace('/\bthe\b/i', 'a', $result);
        return array('result' => $result, 'stopPropagation' => true);
    }
    
    public function markdown($event, $someThing, $stack)
    {
        if (! preg_match('/^filter:/', $event))
        {
            return array_pop($stack);
        }
        EventManager()->pipeline($someThing, $stack);
        $result = preg_replace('/_([^_]+)_/', '<em>\1</em>', $result);
        $result = preg_replace('/\*([^*]+)\*/', '<strong>\1</strong>', $result);
        return $result;
    }
}



require_once 'event_manager.php';

function upper4 ($event, $someThing, $stack)
{
    if (! preg_match('/^filter:/', $event))
    {
        return array_pop($stack);
    }
    $result = EventManager()->pipeline($someThing, $stack);
    $result = preg_replace('/\b(\w{4})\b/e', 'strtoupper("\1")', $result);
    return $result;
};
$upper4 = EventManager()->bind('filter:show', 'upper4');

EventManager()->bind('filter:show', function ($event, $someThing, $stack)
{
    if (! preg_match('/^filter:/', $event))
    {
        return array_pop($stack);
    }
    $result = EventManager()->pipeline($someThing, $stack);
    $result = preg_replace('/\b(\w{3})\b/', '<strong>\1</strong>', $result);
    return $result;
});

EventManager()->bind('filter:show', array('Bar', 'the_to_a'));

$bar = new Bar();
EventManager()->bind('filter:show', array($bar, 'markdown'));

$foo = new Foo();
$foo->show('The *quick* red fox jumps over the _lazy_ brown dog.');

echo '<hr>';

EventManager()->unbind('filter:show', $upper4);
$foo->show('The *quick* red fox jumps over the _lazy_ brown dog.');

Output

a *quick* red fox jumps OVER a _lazy_ brown dog.
a *quick* red fox jumps over a _lazy_ brown dog.

EventManager class

Last but not least, here is the code of the EventManager class

namespace Ando\Core;

require_once "singleton.php";
require_once "callable.php";

class EventManager extends Singleton {
     
    /**
     * Stores bindings between events and their handlers
     *
     * @var array
     */
    protected $_bindings = array();
    
    /**
     * Counts bindings
     *
     * @var integer
     */
    protected $_count = 0;
    
    /**
     * Returns TRUE if the given $expression is a regular expression in PHP
     * Matching delimiters (), [], {}, <> are not supported
     *
     * @param $regex
     * @return boolean
     */
    protected function isRegExp( $expression )
    {
        if (is_string( $expression )
                && preg_match( '/^([^\w \\\\])[^\1]+\1$/', $expression ))
        {
            return true;
        }
        return false;
    }
    
    /**
     * Returns the order of a new handler
     *
     * @return integer
     */
    protected function order()
    {
        return ++$this->_count;
    }
     
     
    /**
     * Binds a $handler to an $event_template
     *
     * @param string $event_template
     * @param Callable|callable $handler
     * @return Ando\Core\Callable
     */
    public function bind( $event_template, $handler )
    {
        $callable = $handler instanceof Callable
                ? $handler
                : new Callable($handler);
        $handlers = isset($this->_bindings[ $event_template ])
                ? $this->_bindings[ $event_template ]
                : new \SplObjectStorage();
        $handlers[ $callable ] = $this->order();
        $this->_bindings[ $event_template ] = $handlers;
        return $callable;
    }
     
     
    /**
     * Unbinds a $handler from an $event_template
     *
     * @param string $event_template
     * @param Callable $handler
     */
    public function unbind( $event_template, Callable $handler = null )
    {
        if (is_null( $handler ))
        {
            unset( $this->_bindings[ $event_template ] );
        }
        else
        {
            unset( $this->_bindings[ $event_template ][ $handler ] );
            if (0 == $this->_bindings[ $event_template ]->count())
            {
                unset( $this->_bindings[ $event_template ] );
            }
        }
    }
    
    /**
     * Returns true if the $event is compatible with the $event_template
     *
     * @param string $event_template
     * @param string $event
     * @return boolean
     */
    protected function isCompatible( $event_template, $event )
    {
        if ($this->isRegExp( $event_template ))
        {
            if (! preg_match( $event_template, $event ))
            {
                return false;
            }
        }
        else
        {
            if ($event_template != $event)
            {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Detects the result of an event handler and
     * if stopPropagation has been requested
     *
     * @param mixed $result
     * @return array
     */
    protected function detectResult( $result )
    {
        if (is_array($result) && isset($result['stopPropagation']))
        {
            $stopPropagation = $result['stopPropagation'];
            $result = isset($result['result']) ? $result['result'] : null;
            return array($result, $stopPropagation);
        }
        return array($result, false);
    }
    
    /**
     * Returns an array with all the handlers that must be called,
     * preserving the order in which the bind occurred
     *
     * @param $event
     * @return array
     */
    protected function filteredHandlers( $event )
    {
        $result = array();
        foreach ($this->_bindings as $event_template => $handlers)
        {
            if (! $this->isCompatible($event_template, $event))
            {
                continue;
            }
            foreach ($handlers as $handler)
            {
                $order = $handlers[ $handler ];
                $result[ $order ] = array(
                    'event_template' => $event_template,
                    'handler'        => $handler,
                );
            }
        }
        ksort($result);
        return $result;
    }
    
    /**
     * 1: Dispatches an $event to all of its matching handlers
     * 2: Passes to each matching handler all received arguments plus a stack
     * with previous outcomes (see below)
     * 3: Returns a stack with all the outcomes (see below)
     *
     * The same stack is passed to each handler by reference (all other
     * arguments are passed by value) and finally is returned to the trigger
     * caller.
     *
     * When the stack is returned, each element is a handler outcome, that is an
     * array with keys 'event_template', 'event', 'handler', and 'result'.
     * The order is the same in which the handlers were processed.
     *
     * When the stack is passed to a handler, its elements are all the previous
     * handlers outcomes as described above, plus a special last element. It is
     * an array with a key 'event_template', whose value is the event_template
     * matched by the $event.
     *
     * @param string $event
     * @return array
     */
    public function trigger( $event )
    {
        $stack = array();
        $args = func_get_args();
        $work = $this->filteredHandlers($event);
        foreach ($work as $task)
        {
            $event_template = $task['event_template'];
            $handler        = $task['handler'];
            
            $stack[] = array('event_template' => $event_template);
            $count = count($stack);
            $result = $handler($args, $stack);
            list($result, $stopPropagation) = $this->detectResult($result);
            if (count($stack) == $count)
            {
                $stack[ $count - 1 ] = array(
                    'event_template' => $event_template,
                    'event'          => $event,
                    'handler'        => $handler,
                    'result'         => $result,
                );
            }
            if ($stopPropagation)
            {
                break;
            }
        }
        return $stack;
    }
    
    /**
     * 1: Returns the second to last value of $stack, if it exists,
     * else returns $someThing
     * 2: It's a utility method for using with handlers that act as filters
     *
     * @param mixed $someThing
     * @param array $stack
     * @return mixed
     */
    public function pipeline($someThing, $stack)
    {
        if (is_array($stack) && count($stack) > 1)
        {
            $secondToLast = $stack[ count($stack) - 2 ];
            $result = $secondToLast['result'];
        }
        else
        {
            $result = $someThing;
        }
        return $result;
    }
}

Singleton::instance('Ando\Core\EventManager');

Submit Form on Enter Key

A wonderful trick.

Yet Another Singleton Class in PHP (5.3)

namespace Ando\Core;

class Exception extends \Exception
{}

abstract class Singleton
{
    /**
     * Is the repository for instances of subclasses
     *
     * @var array
     */
    static private $_instances = array();
    
    /**
     * 1: Disables the 'new' operation
     * 2: Calls Singleton::_construct() with all the arguments passed to
     * Singleton::instance(), except the first one, which is the class name
     * 3: Creates a shortcut for accessing the instance by the class name
     *
     * @param array $args
     */
    private function __construct( $args )
    {
        $init = array($this, '_construct');
        call_user_func_array( $init, $args );
        
        $class = get_class($this);
        preg_match('/^(\\\\?(?:[^\\\\]+\\\\)*)([^\\\\]+)$/', $class, $matches);
        $namespace = $matches[1];
        $function  = $matches[2];
        $shortcut = "
if (! function_exists('$function'))
{
    function $function()
    {
        return Ando\Core\Singleton::instance('$class');
    }
}
";
        eval($shortcut);
    }
    
    /**
     * Disables the 'clone' operation
     */
    private function __clone()
    {}
    
    /**
     * 1: Initializes the instance
     * 2: Gets called once and automatically by Singleton::instance()
     * 3: Receives all the arguments passed to Singleton::instance(), except the
     * first one, which is the class name
     */
    protected function _construct()
    {}
    
    /**
     * Returns true if there is an instance of the $class in the repository
     *
     * @param string $class
     * @return boolean
     */
    static public function instanceAvailable( $class )
    {
        return isset( self::$_instances[ $class ] );
    }
    
    /**
     * 1: Returns the instance of the $class
     * 2: Creates, initializes and checks the instance the first time it gets
     * called with a given $class name
     *
     * @param string $class
     * @return Singleton
     */
    static public function instance( $class )
    {
        if (! self::instanceAvailable( $class ))
        {
            $args = array_slice( func_get_args(), 1 );
            $instance = new $class( $args );
            if ($instance instanceof self)
            {
                self::$_instances[ $class ] = $instance;
            }
            else
            {
                throw new Exception('Expected a subclass of Ando\Core\Singleton');
            }
        }
        return self::$_instances[ $class ];
    }
}
Continue reading

Yet Another EventManager Class in PHP (

Some months ago I published an article about How to add events to a PHP project (1st EventManager). It should have been the basis for a later article about how to use events to index a site, still to be written.

The 1st EventManager class was ready to use, but some days ago I discovered the Event Dispatcher component of symfony (sfEventDispatcher class), whose Recipes page shows some usage examples. It proved that my EventManager class needed some reworking to have comparable power. Continue reading

Yet Another Singleton Class in PHP (

class SingletonException extends Exception
{}

abstract class Singleton {
    
    /**
     * Is the repository for instances of subclasses
     *
     * @var array
     */
    static private $_instances = array();
    
    /**
     * 1: Disables the 'new' operation
     * 2: Calls Singleton::_construct() with all the arguments passed to
     * Singleton::instance(), except the first one, which is the class name
     * 3: Creates a shortcut for accessing the instance by the class name
     *
     * @param array $args
     */
    private function __construct( $args )
    {
        $init = array($this, '_construct');
        call_user_func_array( $init, $args );
        
        $class = get_class($this);
        $shortcut = "
if (! function_exists('$class'))
{
    function $class()
    {
        return Singleton::instance('$class');
    }
}
";
        eval($shortcut);
    }
    
    /**
     * Disables the 'clone' operation
     */
    private function __clone()
    {}
    
    /**
     * 1: Initializes the instance
     * 2: Gets called once and automatically by Singleton::instance()
     * 3: Receives all the arguments passed to Singleton::instance(), except the
     * first one, which is the class name
     */
    protected function _construct()
    {}
    
    /**
     * Returns true if there is an instance of the $class in the repository
     *
     * @param string $class
     * @return boolean
     */
    static public function instanceAvailable( $class )
    {
        return isset( self::$_instances[ $class ] );
    }
    
    /**
     * 1: Returns the instance of the $class
     * 2: Creates, initializes and checks the instance the first time it gets
     * called with a given $class name
     *
     * @param string $class
     * @return Singleton
     */
    static public function instance( $class )
    {
        if (! self::instanceAvailable( $class ))
        {
            $args = array_slice( func_get_args(), 1 );
            $instance = new $class( $args );
            if ($instance instanceof self)
            {
                self::$_instances[ $class ] = $instance;
            }
            else
            {
                throw new SingletonException('Expected a Singleton subclass');
            }
        }
        return self::$_instances[ $class ];
    }
}
Continue reading

© 2017 Notes Log

Theme by Anders NorenUp ↑