Skip to content

Instantly share code, notes, and snippets.

@joshuaadickerson
Last active August 29, 2015 13:56
Show Gist options
  • Save joshuaadickerson/9045673 to your computer and use it in GitHub Desktop.
Save joshuaadickerson/9045673 to your computer and use it in GitHub Desktop.
<?php
/*
CREATE TABLE {$db_prefix}plugin_hooks (
id_hook smallint(6) unsigned NOT NULL auto_increment,
id_plugin smallint(6) unsigned NOT NULL,
event varchar(100) NOT NULL,
class varchar(100) NOT NULL DEFAULT '',
method varchar(255) NOT NULL,
include_file varchar(100) NOT NULL DEFAULT '',
priority tinyint(4) NOT NULL DEFAULT 0,
PRIMARY KEY (id_hook)
) ENGINE=MyISAM;
*/
// @todo store the results in the event.
// Initially I was thinking that $dispathcer->dispatch() should return the results,
// but then we need to allow an event to not be stopped. We shouldn't allow that
// If an event is stopped by a listener, it should be required to be recreated
// to run again. So, if we are doing that, just store the results in it as well.
class Event
{
protected $name;
protected $parameters = array();
protected $dispatcher;
/**
* The DIC
* @var Pimple
*/
protected $container;
protected $stop = false;
protected $stopped_by;
public function __construct($name, array $parameters = array())
{
if (!is_string($name) || trim($name) === '')
{
throw new \InvalidArgumentException('The name of the event must not be empty');
}
$this->name = $name;
if (!empty($parameters))
{
$this->setParameters = $parameters;
}
}
public function setParameters(array $parameters)
{
foreach ($parameters as $param => $value)
{
$this->parameters[$param] = $value;
}
return $this;
}
public function getParameter($param)
{
if (isset($this->parameters[$param]))
{
return $this->parameters[$param];
}
}
public function getParameters()
{
return $this->parameters;
}
public function getName()
{
return $this->name;
}
public function trigger()
{
if (empty($this->dispatcher))
{
throw new \RuntimeException('The Event Dispatcher has not been set.');
}
return $this->dispatcher->dispatch($this);
}
public function stopPropogation()
{
$this->stop = true;
return $this;
}
public function isStopped()
{
return $this->stop;
}
public function setStoppedBy(EventListener $listener)
{
$this->stopped_by = $listener;
return $this;
}
public function getStoppedBy()
{
return $this->stopped_by;
}
/**
* Makes it so that the events can be self-triggering
* @param $dispatcher
*/
public function setEventDispatcher($dispatcher)
{
$this->dispatcher = $dispatcher;
return $this;
}
public function setContainer($container)
{
$this->container = $container;
return $this;
}
}
class EventListener extends BaseEntity
{
protected $id_hook;
protected $id_plugin;
protected $event;
protected $class;
protected $method;
protected $include;
protected $priority;
}
class EventDispatcher
{
protected $_events = array();
// Get the replacements for the paths
protected $_path_replacements = array(
'BOARDDIR' => BOARDDIR,
'SOURCEDIR' => SOURCEDIR,
'EXTDIR' => EXTDIR,
'LANGUAGEDIR' => LANGUAGEDIR,
'ADMINDIR' => ADMINDIR,
'CONTROLLERDIR' => CONTROLLERDIR,
'SUBSDIR' => SUBSDIR,
);
protected $_debug = false;
protected $_included_files = array();
protected $container;
public function __construct($container, array $event_subscribers)
{
$this->container = $container;
foreach ($event_subscribers as $event)
{
$this->registerListener($event);
}
// @todo I think this is legacy
if (!empty($GLOBALS['settings']['theme_dir']))
$this->path_replacements['$themedir'] = $GLOBALS['settings']['theme_dir'];
}
public function setDebug($debug)
{
$this->debug = (bool) $debug;
}
public function dispatch(Event $event)
{
$name = $event->getName();
// Track that it was called
if ($this->debug)
{
// @todo this should have the debugger injected instead
$GLOBALS['context']['debug']['hooks'][] = $name;
}
if (isset($this->_events[$name]))
{
// Add the container to the event
$event->setContainer($this->container)
->setEventDispatcher($this);
$results = $this->callListeners($this->_events[$name]);
}
return $results;
}
protected function callListeners(Event $event, array $listeners)
{
$return = array();
foreach ($listeners as $priority_level)
{
foreach ($priority_level as $listener)
{
$this->includeFile($listener->include);
$return[] = $this->call($listener->class, $listener->method, $event);
// Someone wants us to stop propogating this event
if ($event->isStopped())
{
$event->setStoppedBy($listener);
break;
}
}
if ($event->isStopped())
{
break;
}
}
return $return;
}
protected function call($class, $method, Event $event)
{
// No class, just a function
if (empty($class))
{
if (is_callable($method))
return call_user_func(array($class, $method), $event);
// @todo throw exception?
return;
}
// No method, but a class - call the constructor
if (empty($method))
{
if (is_class($class))
return new $class($event);
// @todo throw exception?
return;
}
// An instantiated class
if (is_callable(array($class, $method)))
return call_user_func(array($class, $method), $event);
// A static call
if (is_callable($class . '::' . $method))
return call_user_func($class . '::' . $method, $event);
}
/**
*
* @param string $file
*/
protected function includeFile($file)
{
if (empty($file))
{
return;
}
$filename = $this->getFileName($file);
if (isset($this->_included_files[$filename]))
{
return;
}
$this->_included_files[$filename] = true;
// Don't check if it exists because we want it to break if it doesn't
// @todo maybe not?
require_once($filename);
}
protected function getFileName($file)
{
return strtr($file, $this->_path_replacements);
}
public function registerListener(EventListener $event)
{
$name = $event->getName();
$priority = $event->getPriority();
if (!isset($this->_events[$name]))
{
$this->_events[$name] = array();
}
if (!isset($this->_events[$name][$priority]))
{
$this->_events[$name][$priority] = array();
// Sort by priority
usort($this->_events[$name], array($this, 'priority_sort'));
}
$this->_events[$name][$priority][] = $event;
return $this;
}
/**
* Sort callback for the priority
*
* @param array $a
* @param array $b
* @return int
*/
protected function priority_sort($a, $b)
{
$a_priority = (int) $a['priority'];
$b_priority = (int) $b['priority'];
if ($a_priority === $b_priority)
{
return 0;
}
return ($a_priority < $b_priority) ? -1 : 1;
}
/**
* Checks that an event will work
* Checks that the file (if it is set) exists
* Checks if the callable is callable (uses autoloader)
*
* @param EventListener $listener
*/
public function checkEventListener(EventListener $listener)
{
if ($listener->include !== null)
{
if (!file_exists($listener->include))
{
throw new \Exception('The include file "' . $listener->include . '" does not exist');
}
$this->includeFile($listener->file);
}
}
public function getAndDispatch($name, array $parameters = array())
{
$event = new Event($name, $parameters);
$this->dispatch($event);
return $event;
}
}
class PluginManager extends BaseManager
{
public function __construct($database, $cache, EventDispatcher $dispatcher)
{
$this->database = $database;
$this->cache = $cache;
$this->dispatcher = $dispatcher;
}
public function removeListener($event_id)
{
}
public function addListener(EventListener $event)
{
}
public function addPlugin(PluginInterface $plugin)
{
// Make sure it has a name
// Get the date and set the $date_installed
// Insert it
// Get the event listeners
// Set the plugin id for the listeners
// Insert the listeners
}
public function removePlugin($plugin_id)
{
// Iterate all of the listeners
// Remove them by plugin_id
// Remove the plugin from the database
// Remove the plugin file
}
}
/**
* This is just an example
*/
class FooPlugin extends Entity
{
protected $name;
protected $installed;
protected $disabled;
protected $date_installed;
public function getEventListeners()
{
$listeners = array();
$event = new EventListener;
$event->setEvent('topic.get')
->setClass('Foo')
->setMethod('setBar')
->setPriority(33);
$listeners[] = $event;
return $listeners;
}
}
// Usage //
// We used to do:
call_integration_hook('integrate_prepare_display_context', array(&$output, &$message));
// Now we do
$event = new Event('display.prepare_context', array('output' => $output, 'message' => $message));
$container['events']->dispatch($event);
// You can also do
$event->trigger();
// You can even do
$results = $event->trigger()->getResults();
// Call the helper method
$results = $app['events']->getAndDispatch('display.prepare_context', array('output' => $output, 'message' => $message))->getResults();
// @todo this needs to use __get() and the ArrayObject
$event->output;
// The name is changed so that you can better find it.
// You know it is in the display controller
// It starts with a noun and ends with a verb
// Removed the "integrate_" stuff
// I'd like to make certain methods in the container global so you don't first use
// the key. So it would be $container->dispatch() or $container->event($name, $parameters)->trigger()->getResults()
// You should still be able to use references
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment