PluginCollection.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.6.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Core;
  16. use Cake\Core\Exception\MissingPluginException;
  17. use Countable;
  18. use Generator;
  19. use InvalidArgumentException;
  20. use Iterator;
  21. /**
  22. * Plugin Collection
  23. *
  24. * Holds onto plugin objects loaded into an application, and
  25. * provides methods for iterating, and finding plugins based
  26. * on criteria.
  27. *
  28. * This class implements the Iterator interface to allow plugins
  29. * to be iterated, handling the situation where a plugin's hook
  30. * method (usually bootstrap) loads another plugin during iteration.
  31. *
  32. * While its implementation supported nested iteration it does not
  33. * support using `continue` or `break` inside loops.
  34. */
  35. class PluginCollection implements Iterator, Countable
  36. {
  37. /**
  38. * Plugin list
  39. *
  40. * @var array<\Cake\Core\PluginInterface>
  41. */
  42. protected $plugins = [];
  43. /**
  44. * Names of plugins
  45. *
  46. * @var array<string>
  47. */
  48. protected $names = [];
  49. /**
  50. * Iterator position stack.
  51. *
  52. * @var array<int>
  53. */
  54. protected $positions = [];
  55. /**
  56. * Loop depth
  57. *
  58. * @var int
  59. */
  60. protected $loopDepth = -1;
  61. /**
  62. * Constructor
  63. *
  64. * @param array<\Cake\Core\PluginInterface> $plugins The map of plugins to add to the collection.
  65. */
  66. public function __construct(array $plugins = [])
  67. {
  68. foreach ($plugins as $plugin) {
  69. $this->add($plugin);
  70. }
  71. $this->loadConfig();
  72. }
  73. /**
  74. * Load the path information stored in vendor/cakephp-plugins.php
  75. *
  76. * This file is generated by the cakephp/plugin-installer package and used
  77. * to locate plugins on the filesystem as applications can use `extra.plugin-paths`
  78. * in their composer.json file to move plugin outside of vendor/
  79. *
  80. * @internal
  81. * @return void
  82. */
  83. protected function loadConfig(): void
  84. {
  85. if (Configure::check('plugins')) {
  86. return;
  87. }
  88. $vendorFile = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
  89. if (!is_file($vendorFile)) {
  90. $vendorFile = dirname(dirname(dirname(dirname(__DIR__)))) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
  91. if (!is_file($vendorFile)) {
  92. Configure::write(['plugins' => []]);
  93. return;
  94. }
  95. }
  96. $config = require $vendorFile;
  97. Configure::write($config);
  98. }
  99. /**
  100. * Locate a plugin path by looking at configuration data.
  101. *
  102. * This will use the `plugins` Configure key, and fallback to enumerating `App::path('plugins')`
  103. *
  104. * This method is not part of the official public API as plugins with
  105. * no plugin class are being phased out.
  106. *
  107. * @param string $name The plugin name to locate a path for.
  108. * @return string
  109. * @throws \Cake\Core\Exception\MissingPluginException when a plugin path cannot be resolved.
  110. * @internal
  111. */
  112. public function findPath(string $name): string
  113. {
  114. // Ensure plugin config is loaded each time. This is necessary primarily
  115. // for testing because the Configure::clear() call in TestCase::tearDown()
  116. // wipes out all configuration including plugin paths config.
  117. $this->loadConfig();
  118. $path = Configure::read('plugins.' . $name);
  119. if ($path) {
  120. return $path;
  121. }
  122. $pluginPath = str_replace('/', DIRECTORY_SEPARATOR, $name);
  123. $paths = App::path('plugins');
  124. foreach ($paths as $path) {
  125. if (is_dir($path . $pluginPath)) {
  126. return $path . $pluginPath . DIRECTORY_SEPARATOR;
  127. }
  128. }
  129. throw new MissingPluginException(['plugin' => $name]);
  130. }
  131. /**
  132. * Add a plugin to the collection
  133. *
  134. * Plugins will be keyed by their names.
  135. *
  136. * @param \Cake\Core\PluginInterface $plugin The plugin to load.
  137. * @return $this
  138. */
  139. public function add(PluginInterface $plugin)
  140. {
  141. $name = $plugin->getName();
  142. $this->plugins[$name] = $plugin;
  143. $this->names = array_keys($this->plugins);
  144. return $this;
  145. }
  146. /**
  147. * Remove a plugin from the collection if it exists.
  148. *
  149. * @param string $name The named plugin.
  150. * @return $this
  151. */
  152. public function remove(string $name)
  153. {
  154. unset($this->plugins[$name]);
  155. $this->names = array_keys($this->plugins);
  156. return $this;
  157. }
  158. /**
  159. * Remove all plugins from the collection
  160. *
  161. * @return $this
  162. */
  163. public function clear()
  164. {
  165. $this->plugins = [];
  166. $this->names = [];
  167. $this->positions = [];
  168. $this->loopDepth = -1;
  169. return $this;
  170. }
  171. /**
  172. * Check whether the named plugin exists in the collection.
  173. *
  174. * @param string $name The named plugin.
  175. * @return bool
  176. */
  177. public function has(string $name): bool
  178. {
  179. return isset($this->plugins[$name]);
  180. }
  181. /**
  182. * Get the a plugin by name.
  183. *
  184. * If a plugin isn't already loaded it will be autoloaded on first access
  185. * and that plugins loaded this way may miss some hook methods.
  186. *
  187. * @param string $name The plugin to get.
  188. * @return \Cake\Core\PluginInterface The plugin.
  189. * @throws \Cake\Core\Exception\MissingPluginException when unknown plugins are fetched.
  190. */
  191. public function get(string $name): PluginInterface
  192. {
  193. if ($this->has($name)) {
  194. return $this->plugins[$name];
  195. }
  196. $plugin = $this->create($name);
  197. $this->add($plugin);
  198. return $plugin;
  199. }
  200. /**
  201. * Create a plugin instance from a name/classname and configuration.
  202. *
  203. * @param string $name The plugin name or classname
  204. * @param array<string, mixed> $config Configuration options for the plugin.
  205. * @return \Cake\Core\PluginInterface
  206. * @throws \Cake\Core\Exception\MissingPluginException When plugin instance could not be created.
  207. */
  208. public function create(string $name, array $config = []): PluginInterface
  209. {
  210. if (strpos($name, '\\') !== false) {
  211. /** @var \Cake\Core\PluginInterface */
  212. return new $name($config);
  213. }
  214. $config += ['name' => $name];
  215. /** @var class-string<\Cake\Core\PluginInterface> $className */
  216. $className = str_replace('/', '\\', $name) . '\\' . 'Plugin';
  217. if (!class_exists($className)) {
  218. $className = BasePlugin::class;
  219. if (empty($config['path'])) {
  220. $config['path'] = $this->findPath($name);
  221. }
  222. }
  223. return new $className($config);
  224. }
  225. /**
  226. * Implementation of Countable.
  227. *
  228. * Get the number of plugins in the collection.
  229. *
  230. * @return int
  231. */
  232. public function count(): int
  233. {
  234. return count($this->plugins);
  235. }
  236. /**
  237. * Part of Iterator Interface
  238. *
  239. * @return void
  240. */
  241. public function next(): void
  242. {
  243. $this->positions[$this->loopDepth]++;
  244. }
  245. /**
  246. * Part of Iterator Interface
  247. *
  248. * @return string
  249. */
  250. public function key(): string
  251. {
  252. return $this->names[$this->positions[$this->loopDepth]];
  253. }
  254. /**
  255. * Part of Iterator Interface
  256. *
  257. * @return \Cake\Core\PluginInterface
  258. */
  259. public function current(): PluginInterface
  260. {
  261. $position = $this->positions[$this->loopDepth];
  262. $name = $this->names[$position];
  263. return $this->plugins[$name];
  264. }
  265. /**
  266. * Part of Iterator Interface
  267. *
  268. * @return void
  269. */
  270. public function rewind(): void
  271. {
  272. $this->positions[] = 0;
  273. $this->loopDepth += 1;
  274. }
  275. /**
  276. * Part of Iterator Interface
  277. *
  278. * @return bool
  279. */
  280. public function valid(): bool
  281. {
  282. $valid = isset($this->names[$this->positions[$this->loopDepth]]);
  283. if (!$valid) {
  284. array_pop($this->positions);
  285. $this->loopDepth -= 1;
  286. }
  287. return $valid;
  288. }
  289. /**
  290. * Filter the plugins to those with the named hook enabled.
  291. *
  292. * @param string $hook The hook to filter plugins by
  293. * @return \Generator<\Cake\Core\PluginInterface> A generator containing matching plugins.
  294. * @throws \InvalidArgumentException on invalid hooks
  295. */
  296. public function with(string $hook): Generator
  297. {
  298. if (!in_array($hook, PluginInterface::VALID_HOOKS, true)) {
  299. throw new InvalidArgumentException("The `{$hook}` hook is not a known plugin hook.");
  300. }
  301. foreach ($this as $plugin) {
  302. if ($plugin->isEnabled($hook)) {
  303. yield $plugin;
  304. }
  305. }
  306. }
  307. }