Skip to content

Instantly share code, notes, and snippets.

@joshuaadickerson
Created March 4, 2014 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshuaadickerson/9347177 to your computer and use it in GitHub Desktop.
Save joshuaadickerson/9347177 to your computer and use it in GitHub Desktop.
Hooker: The Plugin Pimp
<?php
class Event
{
protected $name;
protected $parameters = array();
/** @var EventDispatcher **/
protected $dispatcher;
/** @var Pimple dependency injection container */
protected $container;
/** @var bool if the event is stopped **/
protected $stop = false;
/** @var EventListener what listener stopped the event **/
protected $stopped_by;
/**
* @param string $name The name of the event
* @param array $parameters Parameters that the callbacks should have access to
* @throws \InvalidArgumentException If $name is not a valid name
*/
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];
}
}
/**
* Get all the parameters
*
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Get the name of this event
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Trigger this event
*
* @return EventDispatcher
* @throws \RuntimeException
*/
public function trigger()
{
if (empty($this->dispatcher))
{
throw new \RuntimeException('The Event Dispatcher has not been set.');
}
return $this->dispatcher->dispatch($this);
}
/**
* Stop the rest of the listeners
*
* @return \Event
*/
public function stopPropogation()
{
$this->stop = true;
return $this;
}
/**
* Check if the event is stopped
*
* @return bool
*/
public function isStopped()
{
return $this->stop;
}
/**
* Set who stopped the event
*
* @param EventListener $listener
* @return \Event
*/
public function setStoppedBy(EventListener $listener)
{
$this->stopped_by = $listener;
return $this;
}
/**
* Get the event that stopped this event
*
* @return EventListener
*/
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;
}
/**
* Set the dependency injection container
*
* @param Pimple $container
* @return \Event
*/
public function setContainer(Pimple $container)
{
$this->container = $container;
return $this;
}
public function getContainer()
{
return $this->container instanceof Pimple ?: $this->container;
}
}
<?php
class EventDispatcher
{
protected $events = array();
/**
* The replacements for file paths
* @var array
*/
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;
/**
*
* @param Pimple $container
* @param array $event_subscribers
*/
public function __construct(Pimple $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;
}
/**
* Call all of the listeners for a dispatched event
*
* @param Event $event
* @param array $listeners
* @return mixed[]
*/
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;
}
/**
*
* @param string $class
* @param string $method
* @param Event $event
* @return \class
*/
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);
}
/**
* Include a file for an 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);
}
/**
* @param string $file
* @return string
*/
protected function getFileName($file)
{
return strtr($file, $this->path_replacements);
}
/**
*
* @param EventListener $event
* @return \EventDispatcher
*/
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;
}
public function getListeners()
{
return $this->events;
}
/**
* Should only be used by the plugin manager or when absolutely necessary
*/
public function clearListeners()
{
$this->events = array();
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);
}
}
/**
* Get a new event object and dispatch it
*
* @param string $name the name of the event
* @param array $parameters passed to the event
* @return \Event
*/
public function getAndDispatch($name, array $parameters = array())
{
$event = new Event($name, $parameters);
$this->dispatch($event);
return $event;
}
}
<?php
class EventListener
{
protected $id_listener;
protected $id_plugin;
protected $event;
protected $class;
protected $method;
protected $include;
protected $priority;
/**
* {@inheritdoc}
*/
public function __set($property, $value)
{
return $this->setProperty($property, $value);
}
/**
* {@inheritdoc}
*/
public function __get($property)
{
return property_exists($this, $property) ? $this->$property : null;
}
/**
* Set the id of the event listener
*
* @param int $id
* @return \EventListener
*/
public function setIdEvent($id)
{
return $this->setProperty('id_listener', (int) $id);
}
/**
* Set the id of the parent plugin
*
* @param int $id
* @return \EventListener
*/
public function setIdPlugin($id)
{
return $this->setProperty('id_plugin', (int) $id);
}
/**
* Set the event trigger
*
* @param string $event
* @return \EventListener
*/
public function setEvent($event)
{
return $this->setProperty('event', (string) $event);
}
/**
* The class to instantiate
* Must not have any constructor
*
* @param string $class
* @return \EventListener
*/
public function setClass($class)
{
return $this->setProperty('class', (string) $class);
}
/**
* Set the method of the class to call.
*
* @param string $method
* @return \EventListener
*/
public function setMethod($method)
{
return $this->setProperty('method', (string) $method);
}
/**
* Set the file to include
*
* @param string $include
* @return \EventListener
*/
public function setInclude($include)
{
return $this->setProperty('include', (string) $include);
}
/**
* Set the priority of the event
*
* @param int $priority
* @return \EventListener
*/
public function setPriority($priority)
{
return $this->setProperty('priority', (int) $priority);
}
/**
*
* @param string $property
* @param mixed $value
* @return \EventListener
* @throws \BadFunctionCallException if the property doesn't exist
*/
protected function setProperty($property, $value)
{
if (!property_exists($this, $property))
{
throw new \BadFunctionCallException;
}
// All properties are immutable
if ($this->$property !== null)
{
return;
}
$this->$property = $value;
return $this;
}
}
<?php
class PluginEntity
{
protected $id_plugin;
protected $name;
protected $author;
protected $link;
protected $installed;
protected $path;
protected $events = array();
}
<?php
class PluginController
{
}
<?php
/*
CREATE TABLE {$db_prefix}events (
id_listener smallint(6) unsigned NOT NULL auto_increment,
id_plugin smallint(6) unsigned NOT NULL,
event varchar(255) NOT NULL,
class varchar(255) NOT NULL DEFAULT '',
method varchar(255) NOT NULL,
include_file varchar(255) NOT NULL DEFAULT '',
priority tinyint(4) NOT NULL DEFAULT 0,
PRIMARY KEY (id_hook)
) ENGINE=MyISAM;
CREATE TABLE {$db_prefix}plugins (
id_plugin smallint(6) unsigned NOT NULL auto_increment,
name varchar(255) NOT NULL,
description varchar(255) NOT NULL,
installed_on int(10) unsigned NOT NULL,
link varchar(255) NOT NULL,
path varchar(255) NOT NULL,
PRIMARY KEY (id_hook)
) ENGINE=MyISAM;
*/
use Plugin;
use Event;
class PluginManager extends BaseManager
{
public function __construct($database, $cache, EventDispatcher $dispatcher)
{
$this->database = $database;
$this->cache = $cache;
$this->dispatcher = $dispatcher;
}
/**
* Cache all of the listeners that the dispatcher has registered
* Note: this may cache listeners not found in the database
*
* @return \PluginManager
*/
public function cacheEvents()
{
$listeners = $this->dispatcher->getListeners();
// @todo find out the actual method
$this->cache->put('event_listeners', $listeners);
return $this;
}
/**
* Register all of the listeners in the database
*
* @return \PluginManager
*/
public function registerAllListeners()
{
$this->dispatcher->clearListeners();
foreach ($this->getListeners() as $listener)
{
$this->dispatcher->registerListener($listener);
}
return $this;
}
/**
*
* @param array $listeners = null
* @return \EventListener[]
*/
public function getListeners(array $listeners = null)
{
$request = $this->db->query('', '
SELECT id_event, id_plugin, event, class, method, include, priority
FROM {db_prefix}events' . ($listeners !== null ? '
WHERE id_event IN({array_int:events})' : ''),
array(
'events' => $listeners
)
);
$listeners = array();
while ($row = $this->db->fetch_assoc($request))
{
$listeners[(int) $row['id_event']] = new EventListener(array(
'id_event' => (int) $row['id_event'],
'id_plugin' => (int) $row['id_plugin'],
'event' => $row['event'],
'class' => $row['class'],
'method' => $row['method'],
'include' => $row['include'],
'priority' => (int) $row['priority'],
));
}
return $listeners;
}
public function removeListeners(array $listeners)
{
return $this->db->query('', '
DELETE FROM {db_prefix}events
WHERE id_listener IN({array_int:listeners})'
, array(
'listeners' => $listeners,
));
}
public function removeListenersByPluginId($plugin_id)
{
return $this->db->query('', '
DELETE FROM {db_prefix}events
WHERE id_plugin = {int:plugin_id})'
, array(
'plugin_id' => $plugin_id,
));
}
public function addListener(EventListener $listener)
{
if (empty($listener->id_plugin))
{
throw new \Exception('The event listener must have a plugin id set');
}
$columns = array(
'id_plugin' => 'int',
'event' => 'string',
'class' => 'string',
'method' => 'string',
'include' => 'string',
'priority' => 'int',
);
$values = array(
'id_plugin' => $listener->id_plugin,
'event' => $listener->event,
'class' => $listener->class,
'method' => $listener->method,
'include' => $listener->include,
'priority' => $listener->priority,
);
$this->db->insert('',
'{db_prefix}events',
$columns,
$values,
array('id_listener')
);
$listener->id_listener = $this->db->insert_id('{db_prefix}events', 'id_listener');
}
public function addPlugin(PluginInterface $plugin)
{
// Make sure it has a name
$name = $plugin->name;
if (empty($name) || !is_string($name))
{
throw new \InvalidArgumentException('Plugins must have a name');
}
// Get the date and set the $date_installed
// Insert it
$this->insertPlugin($plugin);
foreach ($plugin->listeners as $listener)
{
// Set the plugin id for the listeners
$listener->setPluginId($plugin->id);
// Insert the listeners
$this->addListener($listener);
}
// @todo destroy the settings/plugin cache
}
protected function insertPlugin(PluginInterface $plugin)
{
$columns = array(
'name' => 'string',
'description' => 'string',
'installed_on' => 'int',
'link' => 'string',
'path' => 'string',
);
$values = array(
'name' => $plugin->name,
'description' => $plugin->description,
'installed_on' => $plugin->installed_on,
'link' => $plugin->link,
'path' => $plugin->path,
);
$this->db->insert('',
'{db_prefix}plugins',
$columns,
$values,
array('id_plugin')
);
$plugin->id_plugin = $this->db->insert_id('{db_prefix}plugins', 'id_plugin');
}
public function removePlugin($plugin_id)
{
// First, get the plugin.
list($plugin) = $this->getPlugins(array((int) $plugin_id));
// Even if the plugin doesn't exist, keep trying.
$this->removeListenersByPluginId($plugin_id);
// Remove the plugin from the database
$this->db->query('', '
DELETE FROM {db_prefix}plugins
WHERE id_plugin = {int:plugin_id})'
, array(
'plugin_id' => $plugin_id,
));
// @todo destroy the settings/plugin cache
// Remove the plugin directory
if ($plugin->path)
{
$this->removePluginDirectory($plugin->path);
}
}
protected function removePluginDirectory($path)
{
}
/**
* Get all of the plugins and the matching events
* This should only be used for Admin and debugging.
* If you want to get all of the listeners, use registerAllListeners()
*
* @param int[] $plugins
* @return array[]
*/
public function getPlugins(array $plugins)
{
// @todo get from cache
$request = $this->db->query('', '
SELECT p.id_plugin, name, description, installed_on, link, path
id_event, event, class, method, include, priority
FROM {db_prefix}plugins AS p
LEFT JOIN {db_prefix}events e USING(id_plugin)
WHERE p.id_plugin IN({array_int:plugins})'
, array(
'plugins' => $plugins,
));
$result = array();
while ($row = $this->db->fetch_assoc($request))
{
if (!isset($result[(int) $row['id_plugin']]))
{
// @todo new Plugin instead of an array
$result[$row['id_plugin']] = array(
'id_plugin' => (int) $row['id_plugin'],
'name' => $row['name'],
'description' => $row['description'],
'installed_on' => new \DateTime($row['installed_on']),
'link' => $row['link'],
'path' => $row['path'],
'events' => array(),
);
}
// Add the events
if (!empty($result[$row['id_event']]))
{
$result[$row['id_plugin']][(int) $row['id_event']] = array(
'id_event' => (int) $row['id_event'],
'event' => $row['event'],
'class' => $row['class'],
'method' => $row['method'],
'include' => $row['include'],
'priority' => (int) $row['priority'],
);
}
}
return $result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment