* @copyright 2008-2010 Phergie Development Team (http://phergie.org) * @license http://phergie.org/license New BSD License * @link http://pear.phergie.org/package/Phergie */ /** * Handles on-demand loading of, iteration over, and access to plugins. * * @category Phergie * @package Phergie * @author Phergie Development Team * @license http://phergie.org/license New BSD License * @link http://pear.phergie.org/package/Phergie */ class Phergie_Plugin_Handler implements IteratorAggregate, Countable { /** * Current list of plugin instances * * @var array */ protected $plugins; /** * Paths in which to search for plugin class files * * @var array */ protected $paths; /** * Flag indicating whether plugin classes should be instantiated on * demand if they are requested but no instance currently exists * * @var bool */ protected $autoload; /** * Phergie_Config instance that should be passed in to any plugin * instantiated within the handler * * @var Phergie_Config */ protected $config; /** * Phergie_Event_Handler instance that should be passed in to any plugin * instantiated within the handler * * @var Phergie_Event_Handler */ protected $events; /** * Name of the class to use for iterating over all currently loaded * plugins * * @var string */ protected $iteratorClass = 'Phergie_Plugin_Iterator'; /** * Constructor to initialize class properties and add the path for core * plugins. * * @param Phergie_Config $config configuration to pass to any * instantiated plugin * @param Phergie_Event_Handler $events event handler to pass to any * instantiated plugin * * @return void */ public function __construct( Phergie_Config $config, Phergie_Event_Handler $events ) { $this->config = $config; $this->events = $events; $this->plugins = array(); $this->paths = array(); $this->autoload = false; if (!empty($config['plugins.paths'])) { foreach ($config['plugins.paths'] as $dir => $prefix) { $this->addPath($dir, $prefix); } } $this->addPath(dirname(__FILE__), 'Phergie_Plugin_'); } /** * Adds a path to search for plugin class files. Paths are searched in * the reverse order in which they are added. * * @param string $path Filesystem directory path * @param string $prefix Optional class name prefix corresponding to the * path * * @return Phergie_Plugin_Handler Provides a fluent interface * @throws Phergie_Plugin_Exception */ public function addPath($path, $prefix = '') { if (!is_readable($path)) { throw new Phergie_Plugin_Exception( 'Path "' . $path . '" does not reference a readable directory', Phergie_Plugin_Exception::ERR_DIRECTORY_NOT_READABLE ); } $this->paths[] = array( 'path' => rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, 'prefix' => $prefix ); return $this; } /** * Returns metadata corresponding to a specified plugin. * * @param string $plugin Short name of the plugin class * * @throws Phergie_Plugin_Exception Class file can't be found * * @return array|boolean Associative array containing the path to the * class file and its containing directory as well as the full * class name */ public function getPluginInfo($plugin) { foreach (array_reverse($this->paths) as $path) { $file = $path['path'] . $plugin . '.php'; if (file_exists($file)) { $path = array( 'dir' => $path['path'], 'file' => $file, 'class' => $path['prefix'] . $plugin, ); return $path; } } // If the class can't be found, display an error throw new Phergie_Plugin_Exception( 'Class file for plugin "' . $plugin . '" cannot be found', Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND ); } /** * Adds a plugin instance to the handler. * * @param string|Phergie_Plugin_Abstract $plugin Short name of the * plugin class or a plugin object * @param array $args Optional array of * arguments to pass to the plugin constructor if a short name is * passed for $plugin * * @return Phergie_Plugin_Abstract New plugin instance */ public function addPlugin($plugin, array $args = null) { // If a short plugin name is specified... if (is_string($plugin)) { $index = strtolower($plugin); if (isset($this->plugins[$index])) { return $this->plugins[$index]; } // Attempt to locate and load the class $info = $this->getPluginInfo($plugin); $file = $info['file']; $class = $info['class']; include_once $file; if (!class_exists($class, false)) { throw new Phergie_Plugin_Exception( 'File "' . $file . '" does not contain class "' . $class . '"', Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND ); } // Check to ensure the class is a plugin class if (!is_subclass_of($class, 'Phergie_Plugin_Abstract')) { $msg = 'Class for plugin "' . $plugin . '" does not extend Phergie_Plugin_Abstract'; throw new Phergie_Plugin_Exception( $msg, Phergie_Plugin_Exception::ERR_INCORRECT_BASE_CLASS ); } // Check to ensure the class can be instantiated $reflection = new ReflectionClass($class); if (!$reflection->isInstantiable()) { throw new Phergie_Plugin_Exception( 'Class for plugin "' . $plugin . '" cannot be instantiated', Phergie_Plugin_Exception::ERR_CLASS_NOT_INSTANTIABLE ); } // If the class is found, instantiate it if (!empty($args)) { $instance = $reflection->newInstanceArgs($args); } else { $instance = new $class; } // Store the instance $this->plugins[$index] = $instance; $plugin = $instance; } elseif ($plugin instanceof Phergie_Plugin_Abstract) { // If a plugin instance is specified... // Add the plugin instance to the list of plugins $this->plugins[strtolower($plugin->getName())] = $plugin; } // Configure and initialize the instance $plugin->setPluginHandler($this); $plugin->setConfig($this->config); $plugin->setEventHandler($this->events); $plugin->onLoad(); return $plugin; } /** * Adds multiple plugin instances to the handler. * * @param array $plugins List of elements where each is of the form * 'ShortPluginName' or array('ShortPluginName', array($arg1, * ..., $argN)) * * @return Phergie_Plugin_Handler Provides a fluent interface */ public function addPlugins(array $plugins) { foreach ($plugins as $plugin) { if (is_array($plugin)) { $this->addPlugin($plugin[0], $plugin[1]); } else { $this->addPlugin($plugin); } } return $this; } /** * Removes a plugin instance from the handler. * * @param string|Phergie_Plugin_Abstract $plugin Short name of the * plugin class or a plugin object * * @return Phergie_Plugin_Handler Provides a fluent interface */ public function removePlugin($plugin) { if ($plugin instanceof Phergie_Plugin_Abstract) { $plugin = $plugin->getName(); } $plugin = strtolower($plugin); unset($this->plugins[$plugin]); return $this; } /** * Returns the corresponding instance for a specified plugin, loading it * if it is not already loaded and autoloading is enabled. * * @param string $name Short name of the plugin class * * @return Phergie_Plugin_Abstract Plugin instance */ public function getPlugin($name) { // If the plugin is loaded, return the instance $lower = strtolower($name); if (isset($this->plugins[$lower])) { return $this->plugins[$lower]; } // If autoloading is disabled, display an error if (!$this->autoload) { $msg = 'Plugin "' . $name . '" has been requested, ' . 'is not loaded, and autoload is disabled'; throw new Phergie_Plugin_Exception( $msg, Phergie_Plugin_Exception::ERR_PLUGIN_NOT_LOADED ); } // If autoloading is enabled, attempt to load the plugin $plugin = $this->addPlugin($name); // Return the added plugin return $plugin; } /** * Returns the corresponding instances for multiple specified plugins, * loading them if they are not already loaded and autoloading is * enabled. * * @param array $names Optional list of short names of the plugin * classes to which the returned plugin list will be limited, * defaults to all presently loaded plugins * * @return array Associative array mapping lowercased plugin class short * names to corresponding plugin instances */ public function getPlugins(array $names = array()) { if (empty($names)) { return $this->plugins; } $plugins = array(); foreach ($names as $name) { $plugins[strtolower($name)] = $this->getPlugin($name); } return $plugins; } /** * Returns whether or not at least one instance of a specified plugin * class is loaded. * * @param string $name Short name of the plugin class * * @return bool TRUE if an instance exists, FALSE otherwise */ public function hasPlugin($name) { return isset($this->plugins[strtolower($name)]); } /** * Sets a flag used to determine whether plugins should be loaded * automatically if they have not been explicitly loaded. * * @param bool $flag TRUE to have plugins autoload (default), FALSE * otherwise * * @return Phergie_Plugin_Handler Provides a fluent interface. */ public function setAutoload($flag = true) { $this->autoload = $flag; return $this; } /** * Returns the value of a flag used to determine whether plugins should * be loaded automatically if they have not been explicitly loaded. * * @return bool TRUE if autoloading is enabled, FALSE otherwise */ public function getAutoload() { return $this->autoload; } /** * Allows plugin instances to be accessed as properties of the handler. * * @param string $name Short name of the plugin * * @return Phergie_Plugin_Abstract Requested plugin instance */ public function __get($name) { return $this->getPlugin($name); } /** * Allows plugin instances to be detected as properties of the handler. * * @param string $name Short name of the plugin * * @return bool TRUE if the plugin is loaded, FALSE otherwise */ public function __isset($name) { return $this->hasPlugin($name); } /** * Allows plugin instances to be removed as properties of handler. * * @param string $name Short name of the plugin * * @return void */ public function __unset($name) { $this->removePlugin($name); } /** * Returns an iterator for all currently loaded plugin instances. * * @return ArrayIterator */ public function getIterator() { return new $this->iteratorClass( new ArrayIterator($this->plugins) ); } /** * Sets the iterator class used for all currently loaded plugin * instances. * * @param string $class Name of a class that extends FilterIterator * * @return Phergie_Plugin_Handler Provides a fluent API * @throws Phergie_Plugin_Exception Class cannot be found or is not an * FilterIterator-based class */ public function setIteratorClass($class) { $valid = true; try { $error_reporting = error_reporting(0); // ignore autoloader errors $r = new ReflectionClass($class); error_reporting($error_reporting); if (!$r->isSubclassOf('FilterIterator')) { $message = 'Class ' . $class . ' is not a subclass of FilterIterator'; $valid = false; } } catch (ReflectionException $e) { $message = $e->getMessage(); $valid = false; } if (!$valid) { throw new Phergie_Plugin_Exception( $message, Phergie_Plugin_Exception::ERR_INVALID_ITERATOR_CLASS ); } $this->iteratorClass = $class; } /** * Proxies method calls to all plugins containing the called method. * * @param string $name Name of the method called * @param array $args Arguments passed in the method call * * @return void */ public function __call($name, array $args) { foreach ($this->getIterator() as $plugin) { call_user_func_array(array($plugin, $name), $args); } return true; } /** * Returns the number of plugins contained within the handler. * * @return int Plugin count */ public function count() { return count($this->plugins); } }