comparison vendor/consolidation/annotated-command/src/AnnotatedCommand.php @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children a9cd425dd02b
comparison
equal deleted inserted replaced
-1:000000000000 0:c75dbcec494b
1 <?php
2 namespace Consolidation\AnnotatedCommand;
3
4 use Consolidation\AnnotatedCommand\Hooks\HookManager;
5 use Consolidation\AnnotatedCommand\Parser\CommandInfo;
6 use Consolidation\AnnotatedCommand\Help\HelpDocumentAlter;
7 use Symfony\Component\Console\Command\Command;
8 use Symfony\Component\Console\Input\InputArgument;
9 use Symfony\Component\Console\Input\InputInterface;
10 use Symfony\Component\Console\Input\InputOption;
11 use Symfony\Component\Console\Output\OutputInterface;
12
13 /**
14 * AnnotatedCommands are created automatically by the
15 * AnnotatedCommandFactory. Each command method in a
16 * command file will produce one AnnotatedCommand. These
17 * are then added to your Symfony Console Application object;
18 * nothing else is needed.
19 *
20 * Optionally, though, you may extend AnnotatedCommand directly
21 * to make a single command. The usage pattern is the same
22 * as for any other Symfony Console command, except that you may
23 * omit the 'Confiure' method, and instead place your annotations
24 * on the execute() method.
25 *
26 * @package Consolidation\AnnotatedCommand
27 */
28 class AnnotatedCommand extends Command implements HelpDocumentAlter
29 {
30 protected $commandCallback;
31 protected $commandProcessor;
32 protected $annotationData;
33 protected $examples = [];
34 protected $topics = [];
35 protected $usesInputInterface;
36 protected $usesOutputInterface;
37 protected $returnType;
38
39 public function __construct($name = null)
40 {
41 $commandInfo = false;
42
43 // If this is a subclass of AnnotatedCommand, check to see
44 // if the 'execute' method is annotated. We could do this
45 // unconditionally; it is a performance optimization to skip
46 // checking the annotations if $this is an instance of
47 // AnnotatedCommand. Alternately, we break out a new subclass.
48 // The command factory instantiates the subclass.
49 if (get_class($this) != 'Consolidation\AnnotatedCommand\AnnotatedCommand') {
50 $commandInfo = CommandInfo::create($this, 'execute');
51 if (!isset($name)) {
52 $name = $commandInfo->getName();
53 }
54 }
55 parent::__construct($name);
56 if ($commandInfo && $commandInfo->hasAnnotation('command')) {
57 $this->setCommandInfo($commandInfo);
58 $this->setCommandOptions($commandInfo);
59 }
60 }
61
62 public function setCommandCallback($commandCallback)
63 {
64 $this->commandCallback = $commandCallback;
65 return $this;
66 }
67
68 public function setCommandProcessor($commandProcessor)
69 {
70 $this->commandProcessor = $commandProcessor;
71 return $this;
72 }
73
74 public function commandProcessor()
75 {
76 // If someone is using an AnnotatedCommand, and is NOT getting
77 // it from an AnnotatedCommandFactory OR not correctly injecting
78 // a command processor via setCommandProcessor() (ideally via the
79 // DI container), then we'll just give each annotated command its
80 // own command processor. This is not ideal; preferably, there would
81 // only be one instance of the command processor in the application.
82 if (!isset($this->commandProcessor)) {
83 $this->commandProcessor = new CommandProcessor(new HookManager());
84 }
85 return $this->commandProcessor;
86 }
87
88 public function getReturnType()
89 {
90 return $this->returnType;
91 }
92
93 public function setReturnType($returnType)
94 {
95 $this->returnType = $returnType;
96 return $this;
97 }
98
99 public function getAnnotationData()
100 {
101 return $this->annotationData;
102 }
103
104 public function setAnnotationData($annotationData)
105 {
106 $this->annotationData = $annotationData;
107 return $this;
108 }
109
110 public function getTopics()
111 {
112 return $this->topics;
113 }
114
115 public function setTopics($topics)
116 {
117 $this->topics = $topics;
118 return $this;
119 }
120
121 public function setCommandInfo($commandInfo)
122 {
123 $this->setDescription($commandInfo->getDescription());
124 $this->setHelp($commandInfo->getHelp());
125 $this->setAliases($commandInfo->getAliases());
126 $this->setAnnotationData($commandInfo->getAnnotations());
127 $this->setTopics($commandInfo->getTopics());
128 foreach ($commandInfo->getExampleUsages() as $usage => $description) {
129 $this->addUsageOrExample($usage, $description);
130 }
131 $this->setCommandArguments($commandInfo);
132 $this->setReturnType($commandInfo->getReturnType());
133 // Hidden commands available since Symfony 3.2
134 // http://symfony.com/doc/current/console/hide_commands.html
135 if (method_exists($this, 'setHidden')) {
136 $this->setHidden($commandInfo->getHidden());
137 }
138 return $this;
139 }
140
141 public function getExampleUsages()
142 {
143 return $this->examples;
144 }
145
146 protected function addUsageOrExample($usage, $description)
147 {
148 $this->addUsage($usage);
149 if (!empty($description)) {
150 $this->examples[$usage] = $description;
151 }
152 }
153
154 public function helpAlter(\DomDocument $originalDom)
155 {
156 $dom = new \DOMDocument('1.0', 'UTF-8');
157 $dom->appendChild($commandXML = $dom->createElement('command'));
158 $commandXML->setAttribute('id', $this->getName());
159 $commandXML->setAttribute('name', $this->getName());
160
161 // Get the original <command> element and its top-level elements.
162 $originalCommandXML = $this->getSingleElementByTagName($dom, $originalDom, 'command');
163 $originalUsagesXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'usages');
164 $originalDescriptionXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'description');
165 $originalHelpXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'help');
166 $originalArgumentsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'arguments');
167 $originalOptionsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'options');
168
169 // Keep only the first of the <usage> elements
170 $newUsagesXML = $dom->createElement('usages');
171 $firstUsageXML = $this->getSingleElementByTagName($dom, $originalUsagesXML, 'usage');
172 $newUsagesXML->appendChild($firstUsageXML);
173
174 // Create our own <example> elements
175 $newExamplesXML = $dom->createElement('examples');
176 foreach ($this->examples as $usage => $description) {
177 $newExamplesXML->appendChild($exampleXML = $dom->createElement('example'));
178 $exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage));
179 $exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description));
180 }
181
182 // Create our own <alias> elements
183 $newAliasesXML = $dom->createElement('aliases');
184 foreach ($this->getAliases() as $alias) {
185 $newAliasesXML->appendChild($dom->createElement('alias', $alias));
186 }
187
188 // Create our own <topic> elements
189 $newTopicsXML = $dom->createElement('topics');
190 foreach ($this->getTopics() as $topic) {
191 $newTopicsXML->appendChild($topicXML = $dom->createElement('topic', $topic));
192 }
193
194 // Place the different elements into the <command> element in the desired order
195 $commandXML->appendChild($newUsagesXML);
196 $commandXML->appendChild($newExamplesXML);
197 $commandXML->appendChild($originalDescriptionXML);
198 $commandXML->appendChild($originalArgumentsXML);
199 $commandXML->appendChild($originalOptionsXML);
200 $commandXML->appendChild($originalHelpXML);
201 $commandXML->appendChild($newAliasesXML);
202 $commandXML->appendChild($newTopicsXML);
203
204 return $dom;
205 }
206
207 protected function getSingleElementByTagName($dom, $parent, $tagName)
208 {
209 // There should always be exactly one '<command>' element.
210 $elements = $parent->getElementsByTagName($tagName);
211 $result = $elements->item(0);
212
213 $result = $dom->importNode($result, true);
214
215 return $result;
216 }
217
218 protected function setCommandArguments($commandInfo)
219 {
220 $this->setUsesInputInterface($commandInfo);
221 $this->setUsesOutputInterface($commandInfo);
222 $this->setCommandArgumentsFromParameters($commandInfo);
223 return $this;
224 }
225
226 /**
227 * Check whether the first parameter is an InputInterface.
228 */
229 protected function checkUsesInputInterface($params)
230 {
231 /** @var \ReflectionParameter $firstParam */
232 $firstParam = reset($params);
233 return $firstParam && $firstParam->getClass() && $firstParam->getClass()->implementsInterface(
234 '\\Symfony\\Component\\Console\\Input\\InputInterface'
235 );
236 }
237
238 /**
239 * Determine whether this command wants to get its inputs
240 * via an InputInterface or via its command parameters
241 */
242 protected function setUsesInputInterface($commandInfo)
243 {
244 $params = $commandInfo->getParameters();
245 $this->usesInputInterface = $this->checkUsesInputInterface($params);
246 return $this;
247 }
248
249 /**
250 * Determine whether this command wants to send its output directly
251 * to the provided OutputInterface, or whether it will returned
252 * structured output to be processed by the command processor.
253 */
254 protected function setUsesOutputInterface($commandInfo)
255 {
256 $params = $commandInfo->getParameters();
257 $index = $this->checkUsesInputInterface($params) ? 1 : 0;
258 $this->usesOutputInterface =
259 (count($params) > $index) &&
260 $params[$index]->getClass() &&
261 $params[$index]->getClass()->implementsInterface(
262 '\\Symfony\\Component\\Console\\Output\\OutputInterface'
263 )
264 ;
265 return $this;
266 }
267
268 protected function setCommandArgumentsFromParameters($commandInfo)
269 {
270 $args = $commandInfo->arguments()->getValues();
271 foreach ($args as $name => $defaultValue) {
272 $description = $commandInfo->arguments()->getDescription($name);
273 $hasDefault = $commandInfo->arguments()->hasDefault($name);
274 $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
275 $this->addArgument($name, $parameterMode, $description, $defaultValue);
276 }
277 return $this;
278 }
279
280 protected function getCommandArgumentMode($hasDefault, $defaultValue)
281 {
282 if (!$hasDefault) {
283 return InputArgument::REQUIRED;
284 }
285 if (is_array($defaultValue)) {
286 return InputArgument::IS_ARRAY;
287 }
288 return InputArgument::OPTIONAL;
289 }
290
291 public function setCommandOptions($commandInfo, $automaticOptions = [])
292 {
293 $inputOptions = $commandInfo->inputOptions();
294
295 $this->addOptions($inputOptions + $automaticOptions, $automaticOptions);
296 return $this;
297 }
298
299 public function addOptions($inputOptions, $automaticOptions = [])
300 {
301 foreach ($inputOptions as $name => $inputOption) {
302 $description = $inputOption->getDescription();
303
304 if (empty($description) && isset($automaticOptions[$name])) {
305 $description = $automaticOptions[$name]->getDescription();
306 $inputOption = static::inputOptionSetDescription($inputOption, $description);
307 }
308 $this->getDefinition()->addOption($inputOption);
309 }
310 }
311
312 protected static function inputOptionSetDescription($inputOption, $description)
313 {
314 // Recover the 'mode' value, because Symfony is stubborn
315 $mode = 0;
316 if ($inputOption->isValueRequired()) {
317 $mode |= InputOption::VALUE_REQUIRED;
318 }
319 if ($inputOption->isValueOptional()) {
320 $mode |= InputOption::VALUE_OPTIONAL;
321 }
322 if ($inputOption->isArray()) {
323 $mode |= InputOption::VALUE_IS_ARRAY;
324 }
325 if (!$mode) {
326 $mode = InputOption::VALUE_NONE;
327 }
328
329 $inputOption = new InputOption(
330 $inputOption->getName(),
331 $inputOption->getShortcut(),
332 $mode,
333 $description,
334 $inputOption->getDefault()
335 );
336 return $inputOption;
337 }
338
339 /**
340 * Returns all of the hook names that may be called for this command.
341 *
342 * @return array
343 */
344 public function getNames()
345 {
346 return HookManager::getNames($this, $this->commandCallback);
347 }
348
349 /**
350 * Add any options to this command that are defined by hook implementations
351 */
352 public function optionsHook()
353 {
354 $this->commandProcessor()->optionsHook(
355 $this,
356 $this->getNames(),
357 $this->annotationData
358 );
359 }
360
361 public function optionsHookForHookAnnotations($commandInfoList)
362 {
363 foreach ($commandInfoList as $commandInfo) {
364 $inputOptions = $commandInfo->inputOptions();
365 $this->addOptions($inputOptions);
366 foreach ($commandInfo->getExampleUsages() as $usage => $description) {
367 if (!in_array($usage, $this->getUsages())) {
368 $this->addUsageOrExample($usage, $description);
369 }
370 }
371 }
372 }
373
374 /**
375 * {@inheritdoc}
376 */
377 protected function interact(InputInterface $input, OutputInterface $output)
378 {
379 $this->commandProcessor()->interact(
380 $input,
381 $output,
382 $this->getNames(),
383 $this->annotationData
384 );
385 }
386
387 protected function initialize(InputInterface $input, OutputInterface $output)
388 {
389 // Allow the hook manager a chance to provide configuration values,
390 // if there are any registered hooks to do that.
391 $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
392 }
393
394 /**
395 * {@inheritdoc}
396 */
397 protected function execute(InputInterface $input, OutputInterface $output)
398 {
399 // Validate, run, process, alter, handle results.
400 return $this->commandProcessor()->process(
401 $output,
402 $this->getNames(),
403 $this->commandCallback,
404 $this->createCommandData($input, $output)
405 );
406 }
407
408 /**
409 * This function is available for use by a class that may
410 * wish to extend this class rather than use annotations to
411 * define commands. Using this technique does allow for the
412 * use of annotations to define hooks.
413 */
414 public function processResults(InputInterface $input, OutputInterface $output, $results)
415 {
416 $commandData = $this->createCommandData($input, $output);
417 $commandProcessor = $this->commandProcessor();
418 $names = $this->getNames();
419 $results = $commandProcessor->processResults(
420 $names,
421 $results,
422 $commandData
423 );
424 return $commandProcessor->handleResults(
425 $output,
426 $names,
427 $results,
428 $commandData
429 );
430 }
431
432 protected function createCommandData(InputInterface $input, OutputInterface $output)
433 {
434 $commandData = new CommandData(
435 $this->annotationData,
436 $input,
437 $output
438 );
439
440 $commandData->setUseIOInterfaces(
441 $this->usesInputInterface,
442 $this->usesOutputInterface
443 );
444
445 // Allow the commandData to cache the list of options with
446 // special default values ('null' and 'true'), as these will
447 // need special handling. @see CommandData::options().
448 $commandData->cacheSpecialDefaults($this->getDefinition());
449
450 return $commandData;
451 }
452 }