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