Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Event based programming #44

Open
Marlinc opened this Issue · 12 comments

7 participants

@Marlinc

Why not create a standard for event based programming in PHP?

So if you have a application with loop you can use the standard that describes events.

So you have a event like this:

<?php

abstract class Event
{

    public $nameLevel = 2;

    public function getName()
    {
        return end(explode('\\', get_class($this), $this->nameLevel + 1));
    }

}

And a event-handler like this:

<?php

class EventHandler
{

    static protected $callbacks = array();

    protected $events           = array();

    public static function raise(Event $event)
    {
        foreach (self::$callbacks[$event->getName()] as $callback) {
            call_user_func($callback, $event);
        }
    }

    public static function addCallback(event, $callback)
    {
        self::$callbacks[$event->getName()][] = $callback;
    }

}

So you can add a callback to a event with:

<?php

function onClick(Event $event)
{
    printf('Raised event %s', $event->getName) . PHP_EOL:
}

EventHandler::addCallback(new Clicked(), 'onClick');

And you're able to raise the event with:

<?php

EventHandler::raise(new Clicked());

I'm sorry if this doesn't look the way you wan't it but this is just my idea. I'm open to suggestions and would like to get some feedback. And possibly event a standard for event based applications.

Also how do you think about event-based data like a 'nickname' for a user when he joins a IRC channel. Should this information be saved in a extended event class. In the raise method. Or something else?

@rawebone

Hi @Marlinc :)

While I think that a standard for Signal Slots/Events would be a good, there are a couple of things I wanted to ask about your example:

  1. I'm fairly sure the add|raise(new Clicked()) method wouldn't work as the objects would have unique references, so you would be forced to have slots based on object types... Not necessarily the best way to go IMHO as that limits the flexibility of any implementation. Is there a reason for this I may have missed?
  2. Is the purpose of the Event object just to contain meta-data about an event?
  3. Does the EventHandler really need to be static?

An alternate take on the above could be:

<?php

interface IEventHandler
{
    public function addEvent($name);
    public function addEventCallback($name, $callback);

    /**
     * param array $meta Any parameters to be passed using call_user_func_array to the listeners
     */
    public function raise($name, array $meta = array());
}

?>

This in practice could then be used like:

<?php

$eh = new \EventHandler();

// Ensure the event is registered so that errors are not thrown in trying to hook a callback that does not exist
$eh->addEvent("exception"); 

$eh->addEventCallback("exception", 'OnException');
$eh->addEventCallback("exception", function (\Exception $e) { 
    // Close a resource related to the process i.e. log file, DB Connection
});

set_exception_handler(function (\Exception $e) use ($eh) {
    // ... Do some logging

    // ... Call any listeners
    $eh->raise("exception", array($e));
});

throw new \ErrorException("Something went wrong");

function OnException(\Exception $e) {
    echo $e;    
}

?>

Just my two-pennys! Good idea @marlinc :)

@Marlinc

Hey @nrawe, thank you for reacting this fast!

About your questions:
1. I was thinking about using objects so you could create for eample a 'Error' event. And you have a Notice, Warning and Fatal event extend this Error event. So you could listen on the Error event and get all the Notice, Warning and Fatal events too. (I know this isn't a practicle example but just to give a idea about the class extend).
2. I thought that it would be pretty nice to have the meta-data in the event specific class itself. Because this also allow's the use of a IDE's autocomplete functionallity.
3. It doesn't nessesarly have to be a static class but I thought it would be a easy way to access it across the intire application.

I also like your implementation especially the idea of creating a event for a Exception.
On the other hand I like objects for events more because like I said (and you asked) they can store meta-data about the specific event. And that way for example also support many IDE's auto-completing.

O and by the way I'm sorry if my English isn't that great :)

@rawebone

Hey @Marlinc

Not a problem :) You could still use objects to pass on meta data about an event, a fairly trivial example of which would be:

<?php
interface IClicked
{
    public function getTargetName();
    public function setTargetName();
}

$eh->addEvent("my_button_clicked");
$eh->addEventCallback("my_button_clicked", function (IClicked $event) { echo "Hello, {$event->getTargetName())}"; });

// ... Do some work

$click = new Clicked(); // Implements IClicked
$click->setTargetName("Mercury");
$eh->raise("my_button_clicked", array($click)); // echo "Hello, Mercury"

?>

The auto-completes will pick up that the $event is an IClick object, so you should get the methods getTargetName() and setTargetName() come up still to work off of. It would be pretty flexible as you could also have interface bound event handlers as the listeners:

<?php

interface IMyEventListener
{
    public function HookClickEvent(IClick $event);
}

$el = new MyEventListener();
$eh->addEventCallback("my_button_clicked", array($el, "HookClickEvent"));

?>

That should allow for big/small domain models to use the interface quite flexibly.

Not a worry, if you hadn't of said I wouldn't have known :)

@jschreuder

Doesn't PHP already offer this from SPL with the SplSubject and SplObserver interfaces?

@dave1010

A removeEvent() method would be useful too.

Something this could provide over SqlSubject / SqlObserver is a priority / order flag.

@philsturgeon

Other than the last suggestion we've not had anything happen here for almost a year. You guys got anything more to throw in?

@designermonkey

Just going to throw my two cents in here (or two pennies in the UK :D )

I personally agree that a decent interface pattern should be on the cards for the standards, and I've played with different options a few times to see what would be the best way to implement it.

Kind of working backwards through the post here...

@jschreuder about SplObserver and SplSubject: Initially I agreed with this, but then I implemented it as an EventHandler and EventListener pattern, and I have to say that it is not easy to get your head around, or to even implement. Using the pattern means that you can't use a Closure for quick portability, and everything must follow a very strict pattern using payload/transaction style objects to wrap what could be simple data, just to fire simple callbacks.

I've kept this pattern in the following branch: https://github.com/usebeagle/events/tree/observer-pattern for you to look at.

The Observer pattern definitely has it's place for large systems that require lots of strict coherence, but from my experience, it's too complex to apply as a standard, and you'd struggle to get buy-in.

About using objects as events, I have approached event handling from this angle too, and also didn't like the approach, as there is too much restriction again. Having an object that is extendable and listened to, sounds too much like PHP's Exception handling to me, and I opt for working with that properly. Setting proper exception and error handlers is the better way to go there, rather than using events. Obviously events can be fired during handling of these.

To me, events is more about allowing customisation code to be run at specific times during the lifecycle of an app, like hooks; And so, it should be kept a lot more simple.

There is a general approach that I've noticed while researching a decent approach to this, and I've implemented it in a package I've developed for an upcoming project.

For those interested, the package is here: https://github.com/usebeagle/events/tree/master, and is still in it's early stage (excuse the naive tests).

Quick highlights:

interface EventHandlerInterface
{
    /**
     * Register an event to be listened for
     * @param  string $event
     */
    public function register($event);

    /**
     * Check the handler has a registered event
     * @param  string  $event
     * @return boolean
     */
    public function has($event);

    /**
     * Listen to a registered event
     * @param  string   $event
     * @param  callable $callable
     */
    public function listen($event, callable $callable);

    /**
     * Trigger a registered event
     * @param  string   $event
     * @param  mixed    $payload
     * @param  callable $callback
     */
    public function trigger($event, &$payload, Closure $callback = null);
}

It's pretty obvious but keeps it simple.

A listenable event name is registered by the app, and any callable can be attached to be triggered on the event. When the event is triggered, then a payload is provided; This payload IMO is where you would use an object to maintain changes from one listener to another and out the other end. The only oddity from expectation is the callback in trigger, which allows the event queue to be terminated early if required.

interface PriorityHandlerInterface
{
    /**
     * Register an event to be listened for
     * @param  string $event
     */
    public function register($event);

    /**
     * Check the handler has a registered event
     * @param  string  $event
     * @return boolean
     */
    public function has($event);

    /**
     * Listen to a registered event
     * @param  string   $event
     * @param  callable $callable
     * @param  integer  $priority
     */
    public function listen($event, callable $callable, $priority);

    /**
     * Trigger a registered event
     * @param  string   $event
     * @param  mixed    $payload
     * @param  callable $callback
     */
    public function trigger($event, $payload, Closure $callback = null);
}

I've also opted to provide a priority based interface to allow for, well, priority based events.

In the package, the implementation is done using SplQueue and SplPriorityQueue, and also uses a set of classes to allow strings to be provided as callables, but hey, implementation matters not at this stage. I am also working on one that uses 'executables' to allow the listener to decide if it is capable of running correctly, and notifying the event handler, which would infinitely loop until every event has had a chance to run, but again, implementation over interface, so I'll shut up.

Anyway, all I'm saying by providing this is that a lot of reading and experimentation has led to a KISS approach to events in PHP.

Hope this is useful :)

@jkobus

Hello. I saw your discussion and wanted to share my thoughts. Take a look at https://github.com/phpextra/event-manager/blob/master/src/PHPExtra/EventManager/EventManagerInterface.php
It can be simplified to:


interface ListenerInterface {}

interface EventInterface{}

interface CancelableEventInterface extends EventInterface {
    public function isCancelled();
}

interface EventManagerInterface {

    public function addListener(ListenerInterface $listener, $priority = 0);

    public function trigger(EventInterface $event);
}
@designermonkey

Pennies number three and four here too :D

interface ControlledHandlerInterface
{
    /**
     * Register an event to be listened for
     * @param  string $event
     */
    public function register($event);

    /**
     * Check the handler has a registered event
     * @param  string  $event
     * @return boolean
     */
    public function has($event);

    /**
     * Control to a registered event with a class
     * @param  EventControllerInterface $callable
     */
    public function control(EventControllerInterface $controller);

    /**
     * Trigger a registered event
     * @param  string   $event
     * @param  mixed    $payload
     */
    public function trigger($event, &$payload);
}

Edited to remove event name in wrong place

and

interface EventControllerInterface
{
    /**
     * Run before any event handling method
     * @param  string $event
     * @return mixed
     */
    public function before($event);

    /**
     * Run after any event handler method
     * @param  string $event
     * @return mixed
     */
    public function after($event);
}

For a scenario where a class would be provided with methods corresponding to event names.

@designermonkey

@jkobus just as I was typing out a similar approach :)

@jkobus

@designermonkey :+1: yeah I started following this repo just like a day ago or so and I saw your message, so I'm here thanks to you :) The code I posted above is very simple. I'm testing it in few of my projects. I see few areas of possible improvement. Example implementation can be found at https://github.com/phpextra/proxy. Proxy is a for-fun project, a little messy, don't hate me on creating proxy in php :dart: but I think it's a good real-life example of a simple event driven application.

@jschreuder

@designermonkey I made the comment over 2 years ago, hardly remember it. But I agree the observer pattern would be wrong here.

Don't have the time to get involved with any further discussion regretably. But I do feel this guy got it right when making a minimal interface that is very interoperable: http://php-and-symfony.matthiasnoback.nl/2014/08/symfony2-decoupling-your-event-system/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.