annotate vendor/consolidation/annotated-command/src/AnnotatedCommand.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 129ea1e6d783
rev   line source
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@0 12
Chris@0 13 /**
Chris@0 14 * AnnotatedCommands are created automatically by the
Chris@0 15 * AnnotatedCommandFactory. Each command method in a
Chris@0 16 * command file will produce one AnnotatedCommand. These
Chris@0 17 * are then added to your Symfony Console Application object;
Chris@0 18 * nothing else is needed.
Chris@0 19 *
Chris@0 20 * Optionally, though, you may extend AnnotatedCommand directly
Chris@0 21 * to make a single command. The usage pattern is the same
Chris@0 22 * as for any other Symfony Console command, except that you may
Chris@0 23 * omit the 'Confiure' method, and instead place your annotations
Chris@0 24 * on the execute() method.
Chris@0 25 *
Chris@0 26 * @package Consolidation\AnnotatedCommand
Chris@0 27 */
Chris@0 28 class AnnotatedCommand extends Command implements HelpDocumentAlter
Chris@0 29 {
Chris@0 30 protected $commandCallback;
Chris@0 31 protected $commandProcessor;
Chris@0 32 protected $annotationData;
Chris@0 33 protected $examples = [];
Chris@0 34 protected $topics = [];
Chris@0 35 protected $usesInputInterface;
Chris@0 36 protected $usesOutputInterface;
Chris@0 37 protected $returnType;
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@0 156 $dom = new \DOMDocument('1.0', 'UTF-8');
Chris@0 157 $dom->appendChild($commandXML = $dom->createElement('command'));
Chris@0 158 $commandXML->setAttribute('id', $this->getName());
Chris@0 159 $commandXML->setAttribute('name', $this->getName());
Chris@0 160
Chris@0 161 // Get the original <command> element and its top-level elements.
Chris@0 162 $originalCommandXML = $this->getSingleElementByTagName($dom, $originalDom, 'command');
Chris@0 163 $originalUsagesXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'usages');
Chris@0 164 $originalDescriptionXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'description');
Chris@0 165 $originalHelpXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'help');
Chris@0 166 $originalArgumentsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'arguments');
Chris@0 167 $originalOptionsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'options');
Chris@0 168
Chris@0 169 // Keep only the first of the <usage> elements
Chris@0 170 $newUsagesXML = $dom->createElement('usages');
Chris@0 171 $firstUsageXML = $this->getSingleElementByTagName($dom, $originalUsagesXML, 'usage');
Chris@0 172 $newUsagesXML->appendChild($firstUsageXML);
Chris@0 173
Chris@0 174 // Create our own <example> elements
Chris@0 175 $newExamplesXML = $dom->createElement('examples');
Chris@0 176 foreach ($this->examples as $usage => $description) {
Chris@0 177 $newExamplesXML->appendChild($exampleXML = $dom->createElement('example'));
Chris@0 178 $exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage));
Chris@0 179 $exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description));
Chris@0 180 }
Chris@0 181
Chris@0 182 // Create our own <alias> elements
Chris@0 183 $newAliasesXML = $dom->createElement('aliases');
Chris@0 184 foreach ($this->getAliases() as $alias) {
Chris@0 185 $newAliasesXML->appendChild($dom->createElement('alias', $alias));
Chris@0 186 }
Chris@0 187
Chris@0 188 // Create our own <topic> elements
Chris@0 189 $newTopicsXML = $dom->createElement('topics');
Chris@0 190 foreach ($this->getTopics() as $topic) {
Chris@0 191 $newTopicsXML->appendChild($topicXML = $dom->createElement('topic', $topic));
Chris@0 192 }
Chris@0 193
Chris@0 194 // Place the different elements into the <command> element in the desired order
Chris@0 195 $commandXML->appendChild($newUsagesXML);
Chris@0 196 $commandXML->appendChild($newExamplesXML);
Chris@0 197 $commandXML->appendChild($originalDescriptionXML);
Chris@0 198 $commandXML->appendChild($originalArgumentsXML);
Chris@0 199 $commandXML->appendChild($originalOptionsXML);
Chris@0 200 $commandXML->appendChild($originalHelpXML);
Chris@0 201 $commandXML->appendChild($newAliasesXML);
Chris@0 202 $commandXML->appendChild($newTopicsXML);
Chris@0 203
Chris@0 204 return $dom;
Chris@0 205 }
Chris@0 206
Chris@0 207 protected function getSingleElementByTagName($dom, $parent, $tagName)
Chris@0 208 {
Chris@0 209 // There should always be exactly one '<command>' element.
Chris@0 210 $elements = $parent->getElementsByTagName($tagName);
Chris@0 211 $result = $elements->item(0);
Chris@0 212
Chris@0 213 $result = $dom->importNode($result, true);
Chris@0 214
Chris@0 215 return $result;
Chris@0 216 }
Chris@0 217
Chris@0 218 protected function setCommandArguments($commandInfo)
Chris@0 219 {
Chris@0 220 $this->setUsesInputInterface($commandInfo);
Chris@0 221 $this->setUsesOutputInterface($commandInfo);
Chris@0 222 $this->setCommandArgumentsFromParameters($commandInfo);
Chris@0 223 return $this;
Chris@0 224 }
Chris@0 225
Chris@0 226 /**
Chris@0 227 * Check whether the first parameter is an InputInterface.
Chris@0 228 */
Chris@0 229 protected function checkUsesInputInterface($params)
Chris@0 230 {
Chris@0 231 /** @var \ReflectionParameter $firstParam */
Chris@0 232 $firstParam = reset($params);
Chris@0 233 return $firstParam && $firstParam->getClass() && $firstParam->getClass()->implementsInterface(
Chris@0 234 '\\Symfony\\Component\\Console\\Input\\InputInterface'
Chris@0 235 );
Chris@0 236 }
Chris@0 237
Chris@0 238 /**
Chris@0 239 * Determine whether this command wants to get its inputs
Chris@0 240 * via an InputInterface or via its command parameters
Chris@0 241 */
Chris@0 242 protected function setUsesInputInterface($commandInfo)
Chris@0 243 {
Chris@0 244 $params = $commandInfo->getParameters();
Chris@0 245 $this->usesInputInterface = $this->checkUsesInputInterface($params);
Chris@0 246 return $this;
Chris@0 247 }
Chris@0 248
Chris@0 249 /**
Chris@0 250 * Determine whether this command wants to send its output directly
Chris@0 251 * to the provided OutputInterface, or whether it will returned
Chris@0 252 * structured output to be processed by the command processor.
Chris@0 253 */
Chris@0 254 protected function setUsesOutputInterface($commandInfo)
Chris@0 255 {
Chris@0 256 $params = $commandInfo->getParameters();
Chris@0 257 $index = $this->checkUsesInputInterface($params) ? 1 : 0;
Chris@0 258 $this->usesOutputInterface =
Chris@0 259 (count($params) > $index) &&
Chris@0 260 $params[$index]->getClass() &&
Chris@0 261 $params[$index]->getClass()->implementsInterface(
Chris@0 262 '\\Symfony\\Component\\Console\\Output\\OutputInterface'
Chris@0 263 )
Chris@0 264 ;
Chris@0 265 return $this;
Chris@0 266 }
Chris@0 267
Chris@0 268 protected function setCommandArgumentsFromParameters($commandInfo)
Chris@0 269 {
Chris@0 270 $args = $commandInfo->arguments()->getValues();
Chris@0 271 foreach ($args as $name => $defaultValue) {
Chris@0 272 $description = $commandInfo->arguments()->getDescription($name);
Chris@0 273 $hasDefault = $commandInfo->arguments()->hasDefault($name);
Chris@0 274 $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
Chris@0 275 $this->addArgument($name, $parameterMode, $description, $defaultValue);
Chris@0 276 }
Chris@0 277 return $this;
Chris@0 278 }
Chris@0 279
Chris@0 280 protected function getCommandArgumentMode($hasDefault, $defaultValue)
Chris@0 281 {
Chris@0 282 if (!$hasDefault) {
Chris@0 283 return InputArgument::REQUIRED;
Chris@0 284 }
Chris@0 285 if (is_array($defaultValue)) {
Chris@0 286 return InputArgument::IS_ARRAY;
Chris@0 287 }
Chris@0 288 return InputArgument::OPTIONAL;
Chris@0 289 }
Chris@0 290
Chris@0 291 public function setCommandOptions($commandInfo, $automaticOptions = [])
Chris@0 292 {
Chris@0 293 $inputOptions = $commandInfo->inputOptions();
Chris@0 294
Chris@0 295 $this->addOptions($inputOptions + $automaticOptions, $automaticOptions);
Chris@0 296 return $this;
Chris@0 297 }
Chris@0 298
Chris@0 299 public function addOptions($inputOptions, $automaticOptions = [])
Chris@0 300 {
Chris@0 301 foreach ($inputOptions as $name => $inputOption) {
Chris@0 302 $description = $inputOption->getDescription();
Chris@0 303
Chris@0 304 if (empty($description) && isset($automaticOptions[$name])) {
Chris@0 305 $description = $automaticOptions[$name]->getDescription();
Chris@0 306 $inputOption = static::inputOptionSetDescription($inputOption, $description);
Chris@0 307 }
Chris@0 308 $this->getDefinition()->addOption($inputOption);
Chris@0 309 }
Chris@0 310 }
Chris@0 311
Chris@0 312 protected static function inputOptionSetDescription($inputOption, $description)
Chris@0 313 {
Chris@0 314 // Recover the 'mode' value, because Symfony is stubborn
Chris@0 315 $mode = 0;
Chris@0 316 if ($inputOption->isValueRequired()) {
Chris@0 317 $mode |= InputOption::VALUE_REQUIRED;
Chris@0 318 }
Chris@0 319 if ($inputOption->isValueOptional()) {
Chris@0 320 $mode |= InputOption::VALUE_OPTIONAL;
Chris@0 321 }
Chris@0 322 if ($inputOption->isArray()) {
Chris@0 323 $mode |= InputOption::VALUE_IS_ARRAY;
Chris@0 324 }
Chris@0 325 if (!$mode) {
Chris@0 326 $mode = InputOption::VALUE_NONE;
Chris@0 327 }
Chris@0 328
Chris@0 329 $inputOption = new InputOption(
Chris@0 330 $inputOption->getName(),
Chris@0 331 $inputOption->getShortcut(),
Chris@0 332 $mode,
Chris@0 333 $description,
Chris@0 334 $inputOption->getDefault()
Chris@0 335 );
Chris@0 336 return $inputOption;
Chris@0 337 }
Chris@0 338
Chris@0 339 /**
Chris@0 340 * Returns all of the hook names that may be called for this command.
Chris@0 341 *
Chris@0 342 * @return array
Chris@0 343 */
Chris@0 344 public function getNames()
Chris@0 345 {
Chris@0 346 return HookManager::getNames($this, $this->commandCallback);
Chris@0 347 }
Chris@0 348
Chris@0 349 /**
Chris@0 350 * Add any options to this command that are defined by hook implementations
Chris@0 351 */
Chris@0 352 public function optionsHook()
Chris@0 353 {
Chris@0 354 $this->commandProcessor()->optionsHook(
Chris@0 355 $this,
Chris@0 356 $this->getNames(),
Chris@0 357 $this->annotationData
Chris@0 358 );
Chris@0 359 }
Chris@0 360
Chris@0 361 public function optionsHookForHookAnnotations($commandInfoList)
Chris@0 362 {
Chris@0 363 foreach ($commandInfoList as $commandInfo) {
Chris@0 364 $inputOptions = $commandInfo->inputOptions();
Chris@0 365 $this->addOptions($inputOptions);
Chris@0 366 foreach ($commandInfo->getExampleUsages() as $usage => $description) {
Chris@0 367 if (!in_array($usage, $this->getUsages())) {
Chris@0 368 $this->addUsageOrExample($usage, $description);
Chris@0 369 }
Chris@0 370 }
Chris@0 371 }
Chris@0 372 }
Chris@0 373
Chris@0 374 /**
Chris@0 375 * {@inheritdoc}
Chris@0 376 */
Chris@0 377 protected function interact(InputInterface $input, OutputInterface $output)
Chris@0 378 {
Chris@0 379 $this->commandProcessor()->interact(
Chris@0 380 $input,
Chris@0 381 $output,
Chris@0 382 $this->getNames(),
Chris@0 383 $this->annotationData
Chris@0 384 );
Chris@0 385 }
Chris@0 386
Chris@0 387 protected function initialize(InputInterface $input, OutputInterface $output)
Chris@0 388 {
Chris@0 389 // Allow the hook manager a chance to provide configuration values,
Chris@0 390 // if there are any registered hooks to do that.
Chris@0 391 $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
Chris@0 392 }
Chris@0 393
Chris@0 394 /**
Chris@0 395 * {@inheritdoc}
Chris@0 396 */
Chris@0 397 protected function execute(InputInterface $input, OutputInterface $output)
Chris@0 398 {
Chris@0 399 // Validate, run, process, alter, handle results.
Chris@0 400 return $this->commandProcessor()->process(
Chris@0 401 $output,
Chris@0 402 $this->getNames(),
Chris@0 403 $this->commandCallback,
Chris@0 404 $this->createCommandData($input, $output)
Chris@0 405 );
Chris@0 406 }
Chris@0 407
Chris@0 408 /**
Chris@0 409 * This function is available for use by a class that may
Chris@0 410 * wish to extend this class rather than use annotations to
Chris@0 411 * define commands. Using this technique does allow for the
Chris@0 412 * use of annotations to define hooks.
Chris@0 413 */
Chris@0 414 public function processResults(InputInterface $input, OutputInterface $output, $results)
Chris@0 415 {
Chris@0 416 $commandData = $this->createCommandData($input, $output);
Chris@0 417 $commandProcessor = $this->commandProcessor();
Chris@0 418 $names = $this->getNames();
Chris@0 419 $results = $commandProcessor->processResults(
Chris@0 420 $names,
Chris@0 421 $results,
Chris@0 422 $commandData
Chris@0 423 );
Chris@0 424 return $commandProcessor->handleResults(
Chris@0 425 $output,
Chris@0 426 $names,
Chris@0 427 $results,
Chris@0 428 $commandData
Chris@0 429 );
Chris@0 430 }
Chris@0 431
Chris@0 432 protected function createCommandData(InputInterface $input, OutputInterface $output)
Chris@0 433 {
Chris@0 434 $commandData = new CommandData(
Chris@0 435 $this->annotationData,
Chris@0 436 $input,
Chris@0 437 $output
Chris@0 438 );
Chris@0 439
Chris@0 440 $commandData->setUseIOInterfaces(
Chris@0 441 $this->usesInputInterface,
Chris@0 442 $this->usesOutputInterface
Chris@0 443 );
Chris@0 444
Chris@0 445 // Allow the commandData to cache the list of options with
Chris@0 446 // special default values ('null' and 'true'), as these will
Chris@0 447 // need special handling. @see CommandData::options().
Chris@0 448 $commandData->cacheSpecialDefaults($this->getDefinition());
Chris@0 449
Chris@0 450 return $commandData;
Chris@0 451 }
Chris@0 452 }