CommandCollection.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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)
  55. {
  56. // Once we have a new Command class this should check
  57. // against that interface.
  58. /** @psalm-suppress DeprecatedClass */
  59. if (!is_subclass_of($command, Shell::class) && !is_subclass_of($command, Command::class)) {
  60. $class = is_string($command) ? $command : get_class($command);
  61. throw new InvalidArgumentException(sprintf(
  62. "Cannot use '%s' for command '%s' it is not a subclass of Cake\Console\Shell or Cake\Console\Command.",
  63. $class,
  64. $name
  65. ));
  66. }
  67. if (!preg_match('/^[^\s]+(?:(?: [^\s]+){1,2})?$/ui', $name)) {
  68. throw new InvalidArgumentException(
  69. "The command name `{$name}` is invalid. Names can only be a maximum of three words."
  70. );
  71. }
  72. $this->commands[$name] = $command;
  73. return $this;
  74. }
  75. /**
  76. * Add multiple commands at once.
  77. *
  78. * @param array $commands A map of command names => command classes/instances.
  79. * @return $this
  80. * @see \Cake\Console\CommandCollection::add()
  81. */
  82. public function addMany(array $commands)
  83. {
  84. foreach ($commands as $name => $class) {
  85. $this->add($name, $class);
  86. }
  87. return $this;
  88. }
  89. /**
  90. * Remove a command from the collection if it exists.
  91. *
  92. * @param string $name The named shell.
  93. * @return $this
  94. */
  95. public function remove(string $name)
  96. {
  97. unset($this->commands[$name]);
  98. return $this;
  99. }
  100. /**
  101. * Check whether the named shell exists in the collection.
  102. *
  103. * @param string $name The named shell.
  104. * @return bool
  105. */
  106. public function has(string $name): bool
  107. {
  108. return isset($this->commands[$name]);
  109. }
  110. /**
  111. * Get the target for a command.
  112. *
  113. * @param string $name The named shell.
  114. * @return string|\Cake\Console\Command Either the command class or an instance.
  115. * @throws \InvalidArgumentException when unknown commands are fetched.
  116. */
  117. public function get(string $name)
  118. {
  119. if (!$this->has($name)) {
  120. throw new InvalidArgumentException("The $name is not a known command name.");
  121. }
  122. return $this->commands[$name];
  123. }
  124. /**
  125. * Implementation of IteratorAggregate.
  126. *
  127. * @return \ArrayIterator
  128. */
  129. public function getIterator(): ArrayIterator
  130. {
  131. return new ArrayIterator($this->commands);
  132. }
  133. /**
  134. * Implementation of Countable.
  135. *
  136. * Get the number of commands in the collection.
  137. *
  138. * @return int
  139. */
  140. public function count(): int
  141. {
  142. return count($this->commands);
  143. }
  144. /**
  145. * Auto-discover shell & commands from the named plugin.
  146. *
  147. * Discovered commands will have their names de-duplicated with
  148. * existing commands in the collection. If a command is already
  149. * defined in the collection and discovered in a plugin, only
  150. * the long name (`plugin.command`) will be returned.
  151. *
  152. * @param string $plugin The plugin to scan.
  153. * @return string[] Discovered plugin commands.
  154. */
  155. public function discoverPlugin(string $plugin): array
  156. {
  157. $scanner = new CommandScanner();
  158. $shells = $scanner->scanPlugin($plugin);
  159. return $this->resolveNames($shells);
  160. }
  161. /**
  162. * Resolve names based on existing commands
  163. *
  164. * @param array $input The results of a CommandScanner operation.
  165. * @return string[] A flat map of command names => class names.
  166. */
  167. protected function resolveNames(array $input): array
  168. {
  169. $out = [];
  170. foreach ($input as $info) {
  171. $name = $info['name'];
  172. $addLong = $name !== $info['fullName'];
  173. // If the short name has been used, use the full name.
  174. // This allows app shells to have name preference.
  175. // and app shells to overwrite core shells.
  176. if ($this->has($name) && $addLong) {
  177. $name = $info['fullName'];
  178. }
  179. $out[$name] = $info['class'];
  180. if ($addLong) {
  181. $out[$info['fullName']] = $info['class'];
  182. }
  183. }
  184. return $out;
  185. }
  186. /**
  187. * Automatically discover shell commands in CakePHP, the application and all plugins.
  188. *
  189. * Commands will be located using filesystem conventions. Commands are
  190. * discovered in the following order:
  191. *
  192. * - CakePHP provided commands
  193. * - Application commands
  194. *
  195. * Commands defined in the application will ovewrite commands with
  196. * the same name provided by CakePHP.
  197. *
  198. * @return string[] An array of command names and their classes.
  199. */
  200. public function autoDiscover(): array
  201. {
  202. $scanner = new CommandScanner();
  203. $core = $this->resolveNames($scanner->scanCore());
  204. $app = $this->resolveNames($scanner->scanApp());
  205. return array_merge($core, $app);
  206. }
  207. }