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