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