PageRenderTime 62ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/Phergie/Plugin/Handler.php

https://github.com/jfolkins/phergie
PHP | 501 lines | 221 code | 51 blank | 229 comment | 18 complexity | c37f03506f9c6857c5eab42ded23f2e6 MD5 | raw file
  1. <?php
  2. /**
  3. * Phergie
  4. *
  5. * PHP version 5
  6. *
  7. * LICENSE
  8. *
  9. * This source file is subject to the new BSD license that is bundled
  10. * with this package in the file LICENSE.
  11. * It is also available through the world-wide-web at this URL:
  12. * http://phergie.org/license
  13. *
  14. * @category Phergie
  15. * @package Phergie
  16. * @author Phergie Development Team <team@phergie.org>
  17. * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
  18. * @license http://phergie.org/license New BSD License
  19. * @link http://pear.phergie.org/package/Phergie
  20. */
  21. /**
  22. * Handles on-demand loading of, iteration over, and access to plugins.
  23. *
  24. * @category Phergie
  25. * @package Phergie
  26. * @author Phergie Development Team <team@phergie.org>
  27. * @license http://phergie.org/license New BSD License
  28. * @link http://pear.phergie.org/package/Phergie
  29. */
  30. class Phergie_Plugin_Handler implements IteratorAggregate, Countable
  31. {
  32. /**
  33. * Current list of plugin instances
  34. *
  35. * @var array
  36. */
  37. protected $plugins;
  38. /**
  39. * Paths in which to search for plugin class files
  40. *
  41. * @var array
  42. */
  43. protected $paths;
  44. /**
  45. * Flag indicating whether plugin classes should be instantiated on
  46. * demand if they are requested but no instance currently exists
  47. *
  48. * @var bool
  49. */
  50. protected $autoload;
  51. /**
  52. * Phergie_Config instance that should be passed in to any plugin
  53. * instantiated within the handler
  54. *
  55. * @var Phergie_Config
  56. */
  57. protected $config;
  58. /**
  59. * Phergie_Event_Handler instance that should be passed in to any plugin
  60. * instantiated within the handler
  61. *
  62. * @var Phergie_Event_Handler
  63. */
  64. protected $events;
  65. /**
  66. * Name of the class to use for iterating over all currently loaded
  67. * plugins
  68. *
  69. * @var string
  70. */
  71. protected $iteratorClass = 'Phergie_Plugin_Iterator';
  72. /**
  73. * Constructor to initialize class properties and add the path for core
  74. * plugins.
  75. *
  76. * @param Phergie_Config $config configuration to pass to any
  77. * instantiated plugin
  78. * @param Phergie_Event_Handler $events event handler to pass to any
  79. * instantiated plugin
  80. *
  81. * @return void
  82. */
  83. public function __construct(
  84. Phergie_Config $config,
  85. Phergie_Event_Handler $events
  86. ) {
  87. $this->config = $config;
  88. $this->events = $events;
  89. $this->plugins = array();
  90. $this->paths = array();
  91. $this->autoload = false;
  92. if (!empty($config['plugins.paths'])) {
  93. foreach ($config['plugins.paths'] as $dir => $prefix) {
  94. $this->addPath($dir, $prefix);
  95. }
  96. }
  97. $this->addPath(dirname(__FILE__), 'Phergie_Plugin_');
  98. }
  99. /**
  100. * Adds a path to search for plugin class files. Paths are searched in
  101. * the reverse order in which they are added.
  102. *
  103. * @param string $path Filesystem directory path
  104. * @param string $prefix Optional class name prefix corresponding to the
  105. * path
  106. *
  107. * @return Phergie_Plugin_Handler Provides a fluent interface
  108. * @throws Phergie_Plugin_Exception
  109. */
  110. public function addPath($path, $prefix = '')
  111. {
  112. if (!is_readable($path)) {
  113. throw new Phergie_Plugin_Exception(
  114. 'Path "' . $path . '" does not reference a readable directory',
  115. Phergie_Plugin_Exception::ERR_DIRECTORY_NOT_READABLE
  116. );
  117. }
  118. $this->paths[] = array(
  119. 'path' => rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR,
  120. 'prefix' => $prefix
  121. );
  122. return $this;
  123. }
  124. /**
  125. * Returns metadata corresponding to a specified plugin.
  126. *
  127. * @param string $plugin Short name of the plugin class
  128. *
  129. * @throws Phergie_Plugin_Exception Class file can't be found
  130. *
  131. * @return array|boolean Associative array containing the path to the
  132. * class file and its containing directory as well as the full
  133. * class name
  134. */
  135. public function getPluginInfo($plugin)
  136. {
  137. foreach (array_reverse($this->paths) as $path) {
  138. $file = $path['path'] . $plugin . '.php';
  139. if (file_exists($file)) {
  140. $path = array(
  141. 'dir' => $path['path'],
  142. 'file' => $file,
  143. 'class' => $path['prefix'] . $plugin,
  144. );
  145. return $path;
  146. }
  147. }
  148. // If the class can't be found, display an error
  149. throw new Phergie_Plugin_Exception(
  150. 'Class file for plugin "' . $plugin . '" cannot be found',
  151. Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND
  152. );
  153. }
  154. /**
  155. * Adds a plugin instance to the handler.
  156. *
  157. * @param string|Phergie_Plugin_Abstract $plugin Short name of the
  158. * plugin class or a plugin object
  159. * @param array $args Optional array of
  160. * arguments to pass to the plugin constructor if a short name is
  161. * passed for $plugin
  162. *
  163. * @return Phergie_Plugin_Abstract New plugin instance
  164. */
  165. public function addPlugin($plugin, array $args = null)
  166. {
  167. // If a short plugin name is specified...
  168. if (is_string($plugin)) {
  169. $index = strtolower($plugin);
  170. if (isset($this->plugins[$index])) {
  171. return $this->plugins[$index];
  172. }
  173. // Attempt to locate and load the class
  174. $info = $this->getPluginInfo($plugin);
  175. $file = $info['file'];
  176. $class = $info['class'];
  177. include_once $file;
  178. if (!class_exists($class, false)) {
  179. throw new Phergie_Plugin_Exception(
  180. 'File "' . $file . '" does not contain class "' . $class . '"',
  181. Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND
  182. );
  183. }
  184. // Check to ensure the class is a plugin class
  185. if (!is_subclass_of($class, 'Phergie_Plugin_Abstract')) {
  186. $msg
  187. = 'Class for plugin "' . $plugin .
  188. '" does not extend Phergie_Plugin_Abstract';
  189. throw new Phergie_Plugin_Exception(
  190. $msg,
  191. Phergie_Plugin_Exception::ERR_INCORRECT_BASE_CLASS
  192. );
  193. }
  194. // Check to ensure the class can be instantiated
  195. $reflection = new ReflectionClass($class);
  196. if (!$reflection->isInstantiable()) {
  197. throw new Phergie_Plugin_Exception(
  198. 'Class for plugin "' . $plugin . '" cannot be instantiated',
  199. Phergie_Plugin_Exception::ERR_CLASS_NOT_INSTANTIABLE
  200. );
  201. }
  202. // If the class is found, instantiate it
  203. if (!empty($args)) {
  204. $instance = $reflection->newInstanceArgs($args);
  205. } else {
  206. $instance = new $class;
  207. }
  208. } elseif ($plugin instanceof Phergie_Plugin_Abstract) {
  209. // If a plugin instance is specified...
  210. // Add the plugin instance to the list of plugins
  211. $index = strtolower($plugin->getName());
  212. $instance = $plugin;
  213. }
  214. // Configure and initialize the instance
  215. $instance->setPluginHandler($this);
  216. $instance->setConfig($this->config);
  217. $instance->setEventHandler($this->events);
  218. $instance->onLoad();
  219. // Store the instance
  220. $this->plugins[$index] = $instance;
  221. return $instance;
  222. }
  223. /**
  224. * Adds multiple plugin instances to the handler.
  225. *
  226. * @param array $plugins List of elements where each is of the form
  227. * 'ShortPluginName' or array('ShortPluginName', array($arg1,
  228. * ..., $argN))
  229. *
  230. * @return Phergie_Plugin_Handler Provides a fluent interface
  231. */
  232. public function addPlugins(array $plugins)
  233. {
  234. foreach ($plugins as $plugin) {
  235. if (is_array($plugin)) {
  236. $this->addPlugin($plugin[0], $plugin[1]);
  237. } else {
  238. $this->addPlugin($plugin);
  239. }
  240. }
  241. return $this;
  242. }
  243. /**
  244. * Removes a plugin instance from the handler.
  245. *
  246. * @param string|Phergie_Plugin_Abstract $plugin Short name of the
  247. * plugin class or a plugin object
  248. *
  249. * @return Phergie_Plugin_Handler Provides a fluent interface
  250. */
  251. public function removePlugin($plugin)
  252. {
  253. if ($plugin instanceof Phergie_Plugin_Abstract) {
  254. $plugin = $plugin->getName();
  255. }
  256. $plugin = strtolower($plugin);
  257. unset($this->plugins[$plugin]);
  258. return $this;
  259. }
  260. /**
  261. * Returns the corresponding instance for a specified plugin, loading it
  262. * if it is not already loaded and autoloading is enabled.
  263. *
  264. * @param string $name Short name of the plugin class
  265. *
  266. * @return Phergie_Plugin_Abstract Plugin instance
  267. */
  268. public function getPlugin($name)
  269. {
  270. // If the plugin is loaded, return the instance
  271. $lower = strtolower($name);
  272. if (isset($this->plugins[$lower])) {
  273. return $this->plugins[$lower];
  274. }
  275. // If autoloading is disabled, display an error
  276. if (!$this->autoload) {
  277. $msg
  278. = 'Plugin "' . $name . '" has been requested, ' .
  279. 'is not loaded, and autoload is disabled';
  280. throw new Phergie_Plugin_Exception(
  281. $msg,
  282. Phergie_Plugin_Exception::ERR_PLUGIN_NOT_LOADED
  283. );
  284. }
  285. // If autoloading is enabled, attempt to load the plugin
  286. $plugin = $this->addPlugin($name);
  287. // Return the added plugin
  288. return $plugin;
  289. }
  290. /**
  291. * Returns the corresponding instances for multiple specified plugins,
  292. * loading them if they are not already loaded and autoloading is
  293. * enabled.
  294. *
  295. * @param array $names Optional list of short names of the plugin
  296. * classes to which the returned plugin list will be limited,
  297. * defaults to all presently loaded plugins
  298. *
  299. * @return array Associative array mapping lowercased plugin class short
  300. * names to corresponding plugin instances
  301. */
  302. public function getPlugins(array $names = array())
  303. {
  304. if (empty($names)) {
  305. return $this->plugins;
  306. }
  307. $plugins = array();
  308. foreach ($names as $name) {
  309. $plugins[strtolower($name)] = $this->getPlugin($name);
  310. }
  311. return $plugins;
  312. }
  313. /**
  314. * Returns whether or not at least one instance of a specified plugin
  315. * class is loaded.
  316. *
  317. * @param string $name Short name of the plugin class
  318. *
  319. * @return bool TRUE if an instance exists, FALSE otherwise
  320. */
  321. public function hasPlugin($name)
  322. {
  323. return isset($this->plugins[strtolower($name)]);
  324. }
  325. /**
  326. * Sets a flag used to determine whether plugins should be loaded
  327. * automatically if they have not been explicitly loaded.
  328. *
  329. * @param bool $flag TRUE to have plugins autoload (default), FALSE
  330. * otherwise
  331. *
  332. * @return Phergie_Plugin_Handler Provides a fluent interface.
  333. */
  334. public function setAutoload($flag = true)
  335. {
  336. $this->autoload = $flag;
  337. return $this;
  338. }
  339. /**
  340. * Returns the value of a flag used to determine whether plugins should
  341. * be loaded automatically if they have not been explicitly loaded.
  342. *
  343. * @return bool TRUE if autoloading is enabled, FALSE otherwise
  344. */
  345. public function getAutoload()
  346. {
  347. return $this->autoload;
  348. }
  349. /**
  350. * Allows plugin instances to be accessed as properties of the handler.
  351. *
  352. * @param string $name Short name of the plugin
  353. *
  354. * @return Phergie_Plugin_Abstract Requested plugin instance
  355. */
  356. public function __get($name)
  357. {
  358. return $this->getPlugin($name);
  359. }
  360. /**
  361. * Allows plugin instances to be detected as properties of the handler.
  362. *
  363. * @param string $name Short name of the plugin
  364. *
  365. * @return bool TRUE if the plugin is loaded, FALSE otherwise
  366. */
  367. public function __isset($name)
  368. {
  369. return $this->hasPlugin($name);
  370. }
  371. /**
  372. * Allows plugin instances to be removed as properties of handler.
  373. *
  374. * @param string $name Short name of the plugin
  375. *
  376. * @return void
  377. */
  378. public function __unset($name)
  379. {
  380. $this->removePlugin($name);
  381. }
  382. /**
  383. * Returns an iterator for all currently loaded plugin instances.
  384. *
  385. * @return ArrayIterator
  386. */
  387. public function getIterator()
  388. {
  389. return new $this->iteratorClass(
  390. new ArrayIterator($this->plugins)
  391. );
  392. }
  393. /**
  394. * Sets the iterator class used for all currently loaded plugin
  395. * instances.
  396. *
  397. * @param string $class Name of a class that extends FilterIterator
  398. *
  399. * @return Phergie_Plugin_Handler Provides a fluent API
  400. * @throws Phergie_Plugin_Exception Class cannot be found or is not an
  401. * FilterIterator-based class
  402. */
  403. public function setIteratorClass($class)
  404. {
  405. $valid = true;
  406. try {
  407. $error_reporting = error_reporting(0); // ignore autoloader errors
  408. $r = new ReflectionClass($class);
  409. error_reporting($error_reporting);
  410. if (!$r->isSubclassOf('FilterIterator')) {
  411. $message = 'Class ' . $class . ' is not a subclass of FilterIterator';
  412. $valid = false;
  413. }
  414. } catch (ReflectionException $e) {
  415. $message = $e->getMessage();
  416. $valid = false;
  417. }
  418. if (!$valid) {
  419. throw new Phergie_Plugin_Exception(
  420. $message,
  421. Phergie_Plugin_Exception::ERR_INVALID_ITERATOR_CLASS
  422. );
  423. }
  424. $this->iteratorClass = $class;
  425. }
  426. /**
  427. * Proxies method calls to all plugins containing the called method.
  428. *
  429. * @param string $name Name of the method called
  430. * @param array $args Arguments passed in the method call
  431. *
  432. * @return void
  433. */
  434. public function __call($name, array $args)
  435. {
  436. foreach ($this->getIterator() as $plugin) {
  437. call_user_func_array(array($plugin, $name), $args);
  438. }
  439. return true;
  440. }
  441. /**
  442. * Returns the number of plugins contained within the handler.
  443. *
  444. * @return int Plugin count
  445. */
  446. public function count()
  447. {
  448. return count($this->plugins);
  449. }
  450. }