CommandCollection.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  12. * @link http://cakephp.org CakePHP(tm) Project
  13. * @since 3.5.0
  14. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Console;
  17. use ArrayIterator;
  18. use Countable;
  19. use InvalidArgumentException;
  20. use IteratorAggregate;
  21. /**
  22. * Collection for Commands.
  23. *
  24. * Used by Applications to whitelist their console commands.
  25. * CakePHP will use the mapped commands to construct and dispatch
  26. * shell commands.
  27. */
  28. class CommandCollection implements IteratorAggregate, Countable
  29. {
  30. /**
  31. * Command list
  32. *
  33. * @var array
  34. */
  35. protected $commands = [];
  36. /**
  37. * Constructor
  38. *
  39. * @param array $commands The map of commands to add to the collection.
  40. */
  41. public function __construct(array $commands = [])
  42. {
  43. foreach ($commands as $name => $command) {
  44. $this->add($name, $command);
  45. }
  46. }
  47. /**
  48. * Add a command to the collection
  49. *
  50. * @param string $name The name of the command you want to map.
  51. * @param string|\Cake\Console\Shell|\Cake\Console\Command $command The command to map.
  52. * @return $this
  53. */
  54. public function add(string $name, $command): self
  55. {
  56. // Once we have a new Command class this should check
  57. // against that interface.
  58. if (!is_subclass_of($command, Shell::class) && !is_subclass_of($command, Command::class)) {
  59. $class = is_string($command) ? $command : get_class($command);
  60. throw new InvalidArgumentException(sprintf(
  61. "Cannot use '%s' for command '%s' it is not a subclass of Cake\Console\Shell or Cake\Console\Command.",
  62. $class,
  63. $name
  64. ));
  65. }
  66. if (!preg_match('/^[^\s]+(?:(?: [^\s]+){1,2})?$/ui', $name)) {
  67. throw new InvalidArgumentException(
  68. "The command name `{$name}` is invalid. Names can only be a maximum of three words."
  69. );
  70. }
  71. $this->commands[$name] = $command;
  72. return $this;
  73. }
  74. /**
  75. * Add multiple commands at once.
  76. *
  77. * @param array $commands A map of command names => command classes/instances.
  78. * @return $this
  79. * @see \Cake\Console\CommandCollection::add()
  80. */
  81. public function addMany(array $commands): self
  82. {
  83. foreach ($commands as $name => $class) {
  84. $this->add($name, $class);
  85. }
  86. return $this;
  87. }
  88. /**
  89. * Remove a command from the collection if it exists.
  90. *
  91. * @param string $name The named shell.
  92. * @return $this
  93. */
  94. public function remove(string $name): self
  95. {
  96. unset($this->commands[$name]);
  97. return $this;
  98. }
  99. /**
  100. * Check whether the named shell exists in the collection.
  101. *
  102. * @param string $name The named shell.
  103. * @return bool
  104. */
  105. public function has(string $name): bool
  106. {
  107. return isset($this->commands[$name]);
  108. }
  109. /**
  110. * Get the target for a command.
  111. *
  112. * @param string $name The named shell.
  113. * @return string|\Cake\Console\Command Either the command class or an instance.
  114. * @throws \InvalidArgumentException when unknown commands are fetched.
  115. */
  116. public function get(string $name)
  117. {
  118. if (!$this->has($name)) {
  119. throw new InvalidArgumentException("The $name is not a known command name.");
  120. }
  121. return $this->commands[$name];
  122. }
  123. /**
  124. * Implementation of IteratorAggregate.
  125. *
  126. * @return \ArrayIterator
  127. */
  128. public function getIterator()
  129. {
  130. return new ArrayIterator($this->commands);
  131. }
  132. /**
  133. * Implementation of Countable.
  134. *
  135. * Get the number of commands in the collection.
  136. *
  137. * @return int
  138. */
  139. public function count(): int
  140. {
  141. return count($this->commands);
  142. }
  143. /**
  144. * Auto-discover shell & commands from the named plugin.
  145. *
  146. * Discovered commands will have their names de-duplicated with
  147. * existing commands in the collection. If a command is already
  148. * defined in the collection and discovered in a plugin, only
  149. * the long name (`plugin.command`) will be returned.
  150. *
  151. * @param string $plugin The plugin to scan.
  152. * @return array Discovered plugin commands.
  153. */
  154. public function discoverPlugin($plugin): array
  155. {
  156. $scanner = new CommandScanner();
  157. $shells = $scanner->scanPlugin($plugin);
  158. return $this->resolveNames($shells);
  159. }
  160. /**
  161. * Resolve names based on existing commands
  162. *
  163. * @param array $input The results of a CommandScanner operation.
  164. * @return array A flat map of command names => class names.
  165. */
  166. protected function resolveNames(array $input): array
  167. {
  168. $out = [];
  169. foreach ($input as $info) {
  170. $name = $info['name'];
  171. $addLong = $name !== $info['fullName'];
  172. // If the short name has been used, use the full name.
  173. // This allows app shells to have name preference.
  174. // and app shells to overwrite core shells.
  175. if ($this->has($name) && $addLong) {
  176. $name = $info['fullName'];
  177. }
  178. $out[$name] = $info['class'];
  179. if ($addLong) {
  180. $out[$info['fullName']] = $info['class'];
  181. }
  182. }
  183. return $out;
  184. }
  185. /**
  186. * Automatically discover shell commands in CakePHP, the application and all plugins.
  187. *
  188. * Commands will be located using filesystem conventions. Commands are
  189. * discovered in the following order:
  190. *
  191. * - CakePHP provided commands
  192. * - Application commands
  193. *
  194. * Commands defined in the application will ovewrite commands with
  195. * the same name provided by CakePHP.
  196. *
  197. * @return array An array of command names and their classes.
  198. */
  199. public function autoDiscover(): array
  200. {
  201. $scanner = new CommandScanner();
  202. $core = $this->resolveNames($scanner->scanCore());
  203. $app = $this->resolveNames($scanner->scanApp());
  204. return array_merge($core, $app);
  205. }
  206. }