Chris@0
|
1 <?php
|
Chris@0
|
2 namespace Consolidation\AnnotatedCommand;
|
Chris@0
|
3
|
Chris@0
|
4 use Consolidation\AnnotatedCommand\Hooks\HookManager;
|
Chris@0
|
5 use Consolidation\AnnotatedCommand\Parser\CommandInfo;
|
Chris@0
|
6 use Consolidation\AnnotatedCommand\Help\HelpDocumentAlter;
|
Chris@0
|
7 use Symfony\Component\Console\Command\Command;
|
Chris@0
|
8 use Symfony\Component\Console\Input\InputArgument;
|
Chris@0
|
9 use Symfony\Component\Console\Input\InputInterface;
|
Chris@0
|
10 use Symfony\Component\Console\Input\InputOption;
|
Chris@0
|
11 use Symfony\Component\Console\Output\OutputInterface;
|
Chris@17
|
12 use Consolidation\AnnotatedCommand\Help\HelpDocumentBuilder;
|
Chris@0
|
13
|
Chris@0
|
14 /**
|
Chris@0
|
15 * AnnotatedCommands are created automatically by the
|
Chris@0
|
16 * AnnotatedCommandFactory. Each command method in a
|
Chris@0
|
17 * command file will produce one AnnotatedCommand. These
|
Chris@0
|
18 * are then added to your Symfony Console Application object;
|
Chris@0
|
19 * nothing else is needed.
|
Chris@0
|
20 *
|
Chris@0
|
21 * Optionally, though, you may extend AnnotatedCommand directly
|
Chris@0
|
22 * to make a single command. The usage pattern is the same
|
Chris@0
|
23 * as for any other Symfony Console command, except that you may
|
Chris@0
|
24 * omit the 'Confiure' method, and instead place your annotations
|
Chris@0
|
25 * on the execute() method.
|
Chris@0
|
26 *
|
Chris@0
|
27 * @package Consolidation\AnnotatedCommand
|
Chris@0
|
28 */
|
Chris@0
|
29 class AnnotatedCommand extends Command implements HelpDocumentAlter
|
Chris@0
|
30 {
|
Chris@0
|
31 protected $commandCallback;
|
Chris@0
|
32 protected $commandProcessor;
|
Chris@0
|
33 protected $annotationData;
|
Chris@0
|
34 protected $examples = [];
|
Chris@0
|
35 protected $topics = [];
|
Chris@0
|
36 protected $returnType;
|
Chris@17
|
37 protected $injectedClasses = [];
|
Chris@0
|
38
|
Chris@0
|
39 public function __construct($name = null)
|
Chris@0
|
40 {
|
Chris@0
|
41 $commandInfo = false;
|
Chris@0
|
42
|
Chris@0
|
43 // If this is a subclass of AnnotatedCommand, check to see
|
Chris@0
|
44 // if the 'execute' method is annotated. We could do this
|
Chris@0
|
45 // unconditionally; it is a performance optimization to skip
|
Chris@0
|
46 // checking the annotations if $this is an instance of
|
Chris@0
|
47 // AnnotatedCommand. Alternately, we break out a new subclass.
|
Chris@0
|
48 // The command factory instantiates the subclass.
|
Chris@0
|
49 if (get_class($this) != 'Consolidation\AnnotatedCommand\AnnotatedCommand') {
|
Chris@0
|
50 $commandInfo = CommandInfo::create($this, 'execute');
|
Chris@0
|
51 if (!isset($name)) {
|
Chris@0
|
52 $name = $commandInfo->getName();
|
Chris@0
|
53 }
|
Chris@0
|
54 }
|
Chris@0
|
55 parent::__construct($name);
|
Chris@0
|
56 if ($commandInfo && $commandInfo->hasAnnotation('command')) {
|
Chris@0
|
57 $this->setCommandInfo($commandInfo);
|
Chris@0
|
58 $this->setCommandOptions($commandInfo);
|
Chris@0
|
59 }
|
Chris@0
|
60 }
|
Chris@0
|
61
|
Chris@0
|
62 public function setCommandCallback($commandCallback)
|
Chris@0
|
63 {
|
Chris@0
|
64 $this->commandCallback = $commandCallback;
|
Chris@0
|
65 return $this;
|
Chris@0
|
66 }
|
Chris@0
|
67
|
Chris@0
|
68 public function setCommandProcessor($commandProcessor)
|
Chris@0
|
69 {
|
Chris@0
|
70 $this->commandProcessor = $commandProcessor;
|
Chris@0
|
71 return $this;
|
Chris@0
|
72 }
|
Chris@0
|
73
|
Chris@0
|
74 public function commandProcessor()
|
Chris@0
|
75 {
|
Chris@0
|
76 // If someone is using an AnnotatedCommand, and is NOT getting
|
Chris@0
|
77 // it from an AnnotatedCommandFactory OR not correctly injecting
|
Chris@0
|
78 // a command processor via setCommandProcessor() (ideally via the
|
Chris@0
|
79 // DI container), then we'll just give each annotated command its
|
Chris@0
|
80 // own command processor. This is not ideal; preferably, there would
|
Chris@0
|
81 // only be one instance of the command processor in the application.
|
Chris@0
|
82 if (!isset($this->commandProcessor)) {
|
Chris@0
|
83 $this->commandProcessor = new CommandProcessor(new HookManager());
|
Chris@0
|
84 }
|
Chris@0
|
85 return $this->commandProcessor;
|
Chris@0
|
86 }
|
Chris@0
|
87
|
Chris@0
|
88 public function getReturnType()
|
Chris@0
|
89 {
|
Chris@0
|
90 return $this->returnType;
|
Chris@0
|
91 }
|
Chris@0
|
92
|
Chris@0
|
93 public function setReturnType($returnType)
|
Chris@0
|
94 {
|
Chris@0
|
95 $this->returnType = $returnType;
|
Chris@0
|
96 return $this;
|
Chris@0
|
97 }
|
Chris@0
|
98
|
Chris@0
|
99 public function getAnnotationData()
|
Chris@0
|
100 {
|
Chris@0
|
101 return $this->annotationData;
|
Chris@0
|
102 }
|
Chris@0
|
103
|
Chris@0
|
104 public function setAnnotationData($annotationData)
|
Chris@0
|
105 {
|
Chris@0
|
106 $this->annotationData = $annotationData;
|
Chris@0
|
107 return $this;
|
Chris@0
|
108 }
|
Chris@0
|
109
|
Chris@0
|
110 public function getTopics()
|
Chris@0
|
111 {
|
Chris@0
|
112 return $this->topics;
|
Chris@0
|
113 }
|
Chris@0
|
114
|
Chris@0
|
115 public function setTopics($topics)
|
Chris@0
|
116 {
|
Chris@0
|
117 $this->topics = $topics;
|
Chris@0
|
118 return $this;
|
Chris@0
|
119 }
|
Chris@0
|
120
|
Chris@0
|
121 public function setCommandInfo($commandInfo)
|
Chris@0
|
122 {
|
Chris@0
|
123 $this->setDescription($commandInfo->getDescription());
|
Chris@0
|
124 $this->setHelp($commandInfo->getHelp());
|
Chris@0
|
125 $this->setAliases($commandInfo->getAliases());
|
Chris@0
|
126 $this->setAnnotationData($commandInfo->getAnnotations());
|
Chris@0
|
127 $this->setTopics($commandInfo->getTopics());
|
Chris@0
|
128 foreach ($commandInfo->getExampleUsages() as $usage => $description) {
|
Chris@0
|
129 $this->addUsageOrExample($usage, $description);
|
Chris@0
|
130 }
|
Chris@0
|
131 $this->setCommandArguments($commandInfo);
|
Chris@0
|
132 $this->setReturnType($commandInfo->getReturnType());
|
Chris@0
|
133 // Hidden commands available since Symfony 3.2
|
Chris@0
|
134 // http://symfony.com/doc/current/console/hide_commands.html
|
Chris@0
|
135 if (method_exists($this, 'setHidden')) {
|
Chris@0
|
136 $this->setHidden($commandInfo->getHidden());
|
Chris@0
|
137 }
|
Chris@0
|
138 return $this;
|
Chris@0
|
139 }
|
Chris@0
|
140
|
Chris@0
|
141 public function getExampleUsages()
|
Chris@0
|
142 {
|
Chris@0
|
143 return $this->examples;
|
Chris@0
|
144 }
|
Chris@0
|
145
|
Chris@0
|
146 protected function addUsageOrExample($usage, $description)
|
Chris@0
|
147 {
|
Chris@0
|
148 $this->addUsage($usage);
|
Chris@0
|
149 if (!empty($description)) {
|
Chris@0
|
150 $this->examples[$usage] = $description;
|
Chris@0
|
151 }
|
Chris@0
|
152 }
|
Chris@0
|
153
|
Chris@0
|
154 public function helpAlter(\DomDocument $originalDom)
|
Chris@0
|
155 {
|
Chris@17
|
156 return HelpDocumentBuilder::alter($originalDom, $this);
|
Chris@0
|
157 }
|
Chris@0
|
158
|
Chris@0
|
159 protected function setCommandArguments($commandInfo)
|
Chris@0
|
160 {
|
Chris@17
|
161 $this->injectedClasses = $commandInfo->getInjectedClasses();
|
Chris@0
|
162 $this->setCommandArgumentsFromParameters($commandInfo);
|
Chris@0
|
163 return $this;
|
Chris@0
|
164 }
|
Chris@0
|
165
|
Chris@0
|
166 protected function setCommandArgumentsFromParameters($commandInfo)
|
Chris@0
|
167 {
|
Chris@0
|
168 $args = $commandInfo->arguments()->getValues();
|
Chris@0
|
169 foreach ($args as $name => $defaultValue) {
|
Chris@0
|
170 $description = $commandInfo->arguments()->getDescription($name);
|
Chris@0
|
171 $hasDefault = $commandInfo->arguments()->hasDefault($name);
|
Chris@0
|
172 $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
|
Chris@0
|
173 $this->addArgument($name, $parameterMode, $description, $defaultValue);
|
Chris@0
|
174 }
|
Chris@0
|
175 return $this;
|
Chris@0
|
176 }
|
Chris@0
|
177
|
Chris@0
|
178 protected function getCommandArgumentMode($hasDefault, $defaultValue)
|
Chris@0
|
179 {
|
Chris@0
|
180 if (!$hasDefault) {
|
Chris@0
|
181 return InputArgument::REQUIRED;
|
Chris@0
|
182 }
|
Chris@0
|
183 if (is_array($defaultValue)) {
|
Chris@0
|
184 return InputArgument::IS_ARRAY;
|
Chris@0
|
185 }
|
Chris@0
|
186 return InputArgument::OPTIONAL;
|
Chris@0
|
187 }
|
Chris@0
|
188
|
Chris@0
|
189 public function setCommandOptions($commandInfo, $automaticOptions = [])
|
Chris@0
|
190 {
|
Chris@0
|
191 $inputOptions = $commandInfo->inputOptions();
|
Chris@0
|
192
|
Chris@0
|
193 $this->addOptions($inputOptions + $automaticOptions, $automaticOptions);
|
Chris@0
|
194 return $this;
|
Chris@0
|
195 }
|
Chris@0
|
196
|
Chris@0
|
197 public function addOptions($inputOptions, $automaticOptions = [])
|
Chris@0
|
198 {
|
Chris@0
|
199 foreach ($inputOptions as $name => $inputOption) {
|
Chris@0
|
200 $description = $inputOption->getDescription();
|
Chris@0
|
201
|
Chris@0
|
202 if (empty($description) && isset($automaticOptions[$name])) {
|
Chris@0
|
203 $description = $automaticOptions[$name]->getDescription();
|
Chris@0
|
204 $inputOption = static::inputOptionSetDescription($inputOption, $description);
|
Chris@0
|
205 }
|
Chris@0
|
206 $this->getDefinition()->addOption($inputOption);
|
Chris@0
|
207 }
|
Chris@0
|
208 }
|
Chris@0
|
209
|
Chris@0
|
210 protected static function inputOptionSetDescription($inputOption, $description)
|
Chris@0
|
211 {
|
Chris@0
|
212 // Recover the 'mode' value, because Symfony is stubborn
|
Chris@0
|
213 $mode = 0;
|
Chris@0
|
214 if ($inputOption->isValueRequired()) {
|
Chris@0
|
215 $mode |= InputOption::VALUE_REQUIRED;
|
Chris@0
|
216 }
|
Chris@0
|
217 if ($inputOption->isValueOptional()) {
|
Chris@0
|
218 $mode |= InputOption::VALUE_OPTIONAL;
|
Chris@0
|
219 }
|
Chris@0
|
220 if ($inputOption->isArray()) {
|
Chris@0
|
221 $mode |= InputOption::VALUE_IS_ARRAY;
|
Chris@0
|
222 }
|
Chris@0
|
223 if (!$mode) {
|
Chris@0
|
224 $mode = InputOption::VALUE_NONE;
|
Chris@0
|
225 }
|
Chris@0
|
226
|
Chris@0
|
227 $inputOption = new InputOption(
|
Chris@0
|
228 $inputOption->getName(),
|
Chris@0
|
229 $inputOption->getShortcut(),
|
Chris@0
|
230 $mode,
|
Chris@0
|
231 $description,
|
Chris@0
|
232 $inputOption->getDefault()
|
Chris@0
|
233 );
|
Chris@0
|
234 return $inputOption;
|
Chris@0
|
235 }
|
Chris@0
|
236
|
Chris@0
|
237 /**
|
Chris@0
|
238 * Returns all of the hook names that may be called for this command.
|
Chris@0
|
239 *
|
Chris@0
|
240 * @return array
|
Chris@0
|
241 */
|
Chris@0
|
242 public function getNames()
|
Chris@0
|
243 {
|
Chris@0
|
244 return HookManager::getNames($this, $this->commandCallback);
|
Chris@0
|
245 }
|
Chris@0
|
246
|
Chris@0
|
247 /**
|
Chris@0
|
248 * Add any options to this command that are defined by hook implementations
|
Chris@0
|
249 */
|
Chris@0
|
250 public function optionsHook()
|
Chris@0
|
251 {
|
Chris@0
|
252 $this->commandProcessor()->optionsHook(
|
Chris@0
|
253 $this,
|
Chris@0
|
254 $this->getNames(),
|
Chris@0
|
255 $this->annotationData
|
Chris@0
|
256 );
|
Chris@0
|
257 }
|
Chris@0
|
258
|
Chris@0
|
259 public function optionsHookForHookAnnotations($commandInfoList)
|
Chris@0
|
260 {
|
Chris@0
|
261 foreach ($commandInfoList as $commandInfo) {
|
Chris@0
|
262 $inputOptions = $commandInfo->inputOptions();
|
Chris@0
|
263 $this->addOptions($inputOptions);
|
Chris@0
|
264 foreach ($commandInfo->getExampleUsages() as $usage => $description) {
|
Chris@0
|
265 if (!in_array($usage, $this->getUsages())) {
|
Chris@0
|
266 $this->addUsageOrExample($usage, $description);
|
Chris@0
|
267 }
|
Chris@0
|
268 }
|
Chris@0
|
269 }
|
Chris@0
|
270 }
|
Chris@0
|
271
|
Chris@0
|
272 /**
|
Chris@0
|
273 * {@inheritdoc}
|
Chris@0
|
274 */
|
Chris@0
|
275 protected function interact(InputInterface $input, OutputInterface $output)
|
Chris@0
|
276 {
|
Chris@0
|
277 $this->commandProcessor()->interact(
|
Chris@0
|
278 $input,
|
Chris@0
|
279 $output,
|
Chris@0
|
280 $this->getNames(),
|
Chris@0
|
281 $this->annotationData
|
Chris@0
|
282 );
|
Chris@0
|
283 }
|
Chris@0
|
284
|
Chris@0
|
285 protected function initialize(InputInterface $input, OutputInterface $output)
|
Chris@0
|
286 {
|
Chris@0
|
287 // Allow the hook manager a chance to provide configuration values,
|
Chris@0
|
288 // if there are any registered hooks to do that.
|
Chris@0
|
289 $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
|
Chris@0
|
290 }
|
Chris@0
|
291
|
Chris@0
|
292 /**
|
Chris@0
|
293 * {@inheritdoc}
|
Chris@0
|
294 */
|
Chris@0
|
295 protected function execute(InputInterface $input, OutputInterface $output)
|
Chris@0
|
296 {
|
Chris@0
|
297 // Validate, run, process, alter, handle results.
|
Chris@0
|
298 return $this->commandProcessor()->process(
|
Chris@0
|
299 $output,
|
Chris@0
|
300 $this->getNames(),
|
Chris@0
|
301 $this->commandCallback,
|
Chris@0
|
302 $this->createCommandData($input, $output)
|
Chris@0
|
303 );
|
Chris@0
|
304 }
|
Chris@0
|
305
|
Chris@0
|
306 /**
|
Chris@0
|
307 * This function is available for use by a class that may
|
Chris@0
|
308 * wish to extend this class rather than use annotations to
|
Chris@0
|
309 * define commands. Using this technique does allow for the
|
Chris@0
|
310 * use of annotations to define hooks.
|
Chris@0
|
311 */
|
Chris@0
|
312 public function processResults(InputInterface $input, OutputInterface $output, $results)
|
Chris@0
|
313 {
|
Chris@0
|
314 $commandData = $this->createCommandData($input, $output);
|
Chris@0
|
315 $commandProcessor = $this->commandProcessor();
|
Chris@0
|
316 $names = $this->getNames();
|
Chris@0
|
317 $results = $commandProcessor->processResults(
|
Chris@0
|
318 $names,
|
Chris@0
|
319 $results,
|
Chris@0
|
320 $commandData
|
Chris@0
|
321 );
|
Chris@0
|
322 return $commandProcessor->handleResults(
|
Chris@0
|
323 $output,
|
Chris@0
|
324 $names,
|
Chris@0
|
325 $results,
|
Chris@0
|
326 $commandData
|
Chris@0
|
327 );
|
Chris@0
|
328 }
|
Chris@0
|
329
|
Chris@0
|
330 protected function createCommandData(InputInterface $input, OutputInterface $output)
|
Chris@0
|
331 {
|
Chris@0
|
332 $commandData = new CommandData(
|
Chris@0
|
333 $this->annotationData,
|
Chris@0
|
334 $input,
|
Chris@0
|
335 $output
|
Chris@0
|
336 );
|
Chris@0
|
337
|
Chris@17
|
338 // Fetch any classes (e.g. InputInterface / OutputInterface) that
|
Chris@17
|
339 // this command's callback wants passed as a parameter and inject
|
Chris@17
|
340 // it into the command data.
|
Chris@17
|
341 $this->commandProcessor()->injectIntoCommandData($commandData, $this->injectedClasses);
|
Chris@0
|
342
|
Chris@0
|
343 // Allow the commandData to cache the list of options with
|
Chris@0
|
344 // special default values ('null' and 'true'), as these will
|
Chris@0
|
345 // need special handling. @see CommandData::options().
|
Chris@0
|
346 $commandData->cacheSpecialDefaults($this->getDefinition());
|
Chris@0
|
347
|
Chris@0
|
348 return $commandData;
|
Chris@0
|
349 }
|
Chris@0
|
350 }
|