Created
March 4, 2014 14:13
-
-
Save joshuaadickerson/9347177 to your computer and use it in GitHub Desktop.
Hooker: The Plugin Pimp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class PluginEntity | |
{ | |
protected $id_plugin; | |
protected $name; | |
protected $author; | |
protected $link; | |
protected $installed; | |
protected $path; | |
protected $events = array(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class PluginController | |
{ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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