2 namespace Consolidation\AnnotatedCommand;
4 use Consolidation\AnnotatedCommand\Hooks\HookManager;
5 use Consolidation\AnnotatedCommand\Parser\CommandInfo;
6 use Consolidation\OutputFormatters\FormatterManager;
7 use Consolidation\OutputFormatters\Options\FormatterOptions;
8 use Consolidation\AnnotatedCommand\Help\HelpDocumentAlter;
9 use Symfony\Component\Console\Command\Command;
10 use Symfony\Component\Console\Input\InputArgument;
11 use Symfony\Component\Console\Input\InputInterface;
12 use Symfony\Component\Console\Input\InputOption;
13 use Symfony\Component\Console\Output\OutputInterface;
16 * AnnotatedCommands are created automatically by the
17 * AnnotatedCommandFactory. Each command method in a
18 * command file will produce one AnnotatedCommand. These
19 * are then added to your Symfony Console Application object;
20 * nothing else is needed.
22 * Optionally, though, you may extend AnnotatedCommand directly
23 * to make a single command. The usage pattern is the same
24 * as for any other Symfony Console command, except that you may
25 * omit the 'Confiure' method, and instead place your annotations
26 * on the execute() method.
28 * @package Consolidation\AnnotatedCommand
30 class AnnotatedCommand extends Command implements HelpDocumentAlter
32 protected $commandCallback;
33 protected $commandProcessor;
34 protected $annotationData;
35 protected $examples = [];
36 protected $topics = [];
37 protected $usesInputInterface;
38 protected $usesOutputInterface;
39 protected $returnType;
41 public function __construct($name = null)
45 // If this is a subclass of AnnotatedCommand, check to see
46 // if the 'execute' method is annotated. We could do this
47 // unconditionally; it is a performance optimization to skip
48 // checking the annotations if $this is an instance of
49 // AnnotatedCommand. Alternately, we break out a new subclass.
50 // The command factory instantiates the subclass.
51 if (get_class($this) != 'Consolidation\AnnotatedCommand\AnnotatedCommand') {
52 $commandInfo = CommandInfo::create($this, 'execute');
54 $name = $commandInfo->getName();
57 parent::__construct($name);
58 if ($commandInfo && $commandInfo->hasAnnotation('command')) {
59 $this->setCommandInfo($commandInfo);
60 $this->setCommandOptions($commandInfo);
64 public function setCommandCallback($commandCallback)
66 $this->commandCallback = $commandCallback;
70 public function setCommandProcessor($commandProcessor)
72 $this->commandProcessor = $commandProcessor;
76 public function commandProcessor()
78 // If someone is using an AnnotatedCommand, and is NOT getting
79 // it from an AnnotatedCommandFactory OR not correctly injecting
80 // a command processor via setCommandProcessor() (ideally via the
81 // DI container), then we'll just give each annotated command its
82 // own command processor. This is not ideal; preferably, there would
83 // only be one instance of the command processor in the application.
84 if (!isset($this->commandProcessor)) {
85 $this->commandProcessor = new CommandProcessor(new HookManager());
87 return $this->commandProcessor;
90 public function getReturnType()
92 return $this->returnType;
95 public function setReturnType($returnType)
97 $this->returnType = $returnType;
101 public function getAnnotationData()
103 return $this->annotationData;
106 public function setAnnotationData($annotationData)
108 $this->annotationData = $annotationData;
112 public function getTopics()
114 return $this->topics;
117 public function setTopics($topics)
119 $this->topics = $topics;
123 public function setCommandInfo($commandInfo)
125 $this->setDescription($commandInfo->getDescription());
126 $this->setHelp($commandInfo->getHelp());
127 $this->setAliases($commandInfo->getAliases());
128 $this->setAnnotationData($commandInfo->getAnnotations());
129 $this->setTopics($commandInfo->getTopics());
130 foreach ($commandInfo->getExampleUsages() as $usage => $description) {
131 $this->addUsageOrExample($usage, $description);
133 $this->setCommandArguments($commandInfo);
134 $this->setReturnType($commandInfo->getReturnType());
138 public function getExampleUsages()
140 return $this->examples;
143 protected function addUsageOrExample($usage, $description)
145 $this->addUsage($usage);
146 if (!empty($description)) {
147 $this->examples[$usage] = $description;
151 public function helpAlter(\DomDocument $originalDom)
153 $dom = new \DOMDocument('1.0', 'UTF-8');
154 $dom->appendChild($commandXML = $dom->createElement('command'));
155 $commandXML->setAttribute('id', $this->getName());
156 $commandXML->setAttribute('name', $this->getName());
158 // Get the original <command> element and its top-level elements.
159 $originalCommandXML = $this->getSingleElementByTagName($dom, $originalDom, 'command');
160 $originalUsagesXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'usages');
161 $originalDescriptionXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'description');
162 $originalHelpXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'help');
163 $originalArgumentsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'arguments');
164 $originalOptionsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'options');
166 // Keep only the first of the <usage> elements
167 $newUsagesXML = $dom->createElement('usages');
168 $firstUsageXML = $this->getSingleElementByTagName($dom, $originalUsagesXML, 'usage');
169 $newUsagesXML->appendChild($firstUsageXML);
171 // Create our own <example> elements
172 $newExamplesXML = $dom->createElement('examples');
173 foreach ($this->examples as $usage => $description) {
174 $newExamplesXML->appendChild($exampleXML = $dom->createElement('example'));
175 $exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage));
176 $exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description));
179 // Create our own <alias> elements
180 $newAliasesXML = $dom->createElement('aliases');
181 foreach ($this->getAliases() as $alias) {
182 $newAliasesXML->appendChild($dom->createElement('alias', $alias));
185 // Create our own <topic> elements
186 $newTopicsXML = $dom->createElement('topics');
187 foreach ($this->getTopics() as $topic) {
188 $newTopicsXML->appendChild($topicXML = $dom->createElement('topic', $topic));
191 // Place the different elements into the <command> element in the desired order
192 $commandXML->appendChild($newUsagesXML);
193 $commandXML->appendChild($newExamplesXML);
194 $commandXML->appendChild($originalDescriptionXML);
195 $commandXML->appendChild($originalArgumentsXML);
196 $commandXML->appendChild($originalOptionsXML);
197 $commandXML->appendChild($originalHelpXML);
198 $commandXML->appendChild($newAliasesXML);
199 $commandXML->appendChild($newTopicsXML);
204 protected function getSingleElementByTagName($dom, $parent, $tagName)
206 // There should always be exactly one '<command>' element.
207 $elements = $parent->getElementsByTagName($tagName);
208 $result = $elements->item(0);
210 $result = $dom->importNode($result, true);
215 protected function setCommandArguments($commandInfo)
217 $this->setUsesInputInterface($commandInfo);
218 $this->setUsesOutputInterface($commandInfo);
219 $this->setCommandArgumentsFromParameters($commandInfo);
224 * Check whether the first parameter is an InputInterface.
226 protected function checkUsesInputInterface($params)
228 $firstParam = reset($params);
229 return $firstParam instanceof InputInterface;
233 * Determine whether this command wants to get its inputs
234 * via an InputInterface or via its command parameters
236 protected function setUsesInputInterface($commandInfo)
238 $params = $commandInfo->getParameters();
239 $this->usesInputInterface = $this->checkUsesInputInterface($params);
244 * Determine whether this command wants to send its output directly
245 * to the provided OutputInterface, or whether it will returned
246 * structured output to be processed by the command processor.
248 protected function setUsesOutputInterface($commandInfo)
250 $params = $commandInfo->getParameters();
251 $index = $this->checkUsesInputInterface($params) ? 1 : 0;
252 $this->usesOutputInterface =
253 (count($params) > $index) &&
254 ($params[$index] instanceof OutputInterface);
258 protected function setCommandArgumentsFromParameters($commandInfo)
260 $args = $commandInfo->arguments()->getValues();
261 foreach ($args as $name => $defaultValue) {
262 $description = $commandInfo->arguments()->getDescription($name);
263 $hasDefault = $commandInfo->arguments()->hasDefault($name);
264 $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
265 $this->addArgument($name, $parameterMode, $description, $defaultValue);
270 protected function getCommandArgumentMode($hasDefault, $defaultValue)
273 return InputArgument::REQUIRED;
275 if (is_array($defaultValue)) {
276 return InputArgument::IS_ARRAY;
278 return InputArgument::OPTIONAL;
281 public function setCommandOptions($commandInfo, $automaticOptions = [])
283 $inputOptions = $commandInfo->inputOptions();
285 $this->addOptions($inputOptions + $automaticOptions, $automaticOptions);
289 public function addOptions($inputOptions, $automaticOptions = [])
291 foreach ($inputOptions as $name => $inputOption) {
292 $description = $inputOption->getDescription();
294 if (empty($description) && isset($automaticOptions[$name])) {
295 $description = $automaticOptions[$name]->getDescription();
296 $inputOption = static::inputOptionSetDescription($inputOption, $description);
298 $this->getDefinition()->addOption($inputOption);
302 protected static function inputOptionSetDescription($inputOption, $description)
304 // Recover the 'mode' value, because Symfony is stubborn
306 if ($inputOption->isValueRequired()) {
307 $mode |= InputOption::VALUE_REQUIRED;
309 if ($inputOption->isValueOptional()) {
310 $mode |= InputOption::VALUE_OPTIONAL;
312 if ($inputOption->isArray()) {
313 $mode |= InputOption::VALUE_IS_ARRAY;
316 $mode = InputOption::VALUE_NONE;
319 $inputOption = new InputOption(
320 $inputOption->getName(),
321 $inputOption->getShortcut(),
324 $inputOption->getDefault()
330 * Returns all of the hook names that may be called for this command.
334 public function getNames()
336 return HookManager::getNames($this, $this->commandCallback);
340 * Add any options to this command that are defined by hook implementations
342 public function optionsHook()
344 $this->commandProcessor()->optionsHook(
347 $this->annotationData
351 public function optionsHookForHookAnnotations($commandInfoList)
353 foreach ($commandInfoList as $commandInfo) {
354 $inputOptions = $commandInfo->inputOptions();
355 $this->addOptions($inputOptions);
356 foreach ($commandInfo->getExampleUsages() as $usage => $description) {
357 if (!in_array($usage, $this->getUsages())) {
358 $this->addUsageOrExample($usage, $description);
367 protected function interact(InputInterface $input, OutputInterface $output)
369 $this->commandProcessor()->interact(
373 $this->annotationData
377 protected function initialize(InputInterface $input, OutputInterface $output)
379 // Allow the hook manager a chance to provide configuration values,
380 // if there are any registered hooks to do that.
381 $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
387 protected function execute(InputInterface $input, OutputInterface $output)
389 // Validate, run, process, alter, handle results.
390 return $this->commandProcessor()->process(
393 $this->commandCallback,
394 $this->createCommandData($input, $output)
399 * This function is available for use by a class that may
400 * wish to extend this class rather than use annotations to
401 * define commands. Using this technique does allow for the
402 * use of annotations to define hooks.
404 public function processResults(InputInterface $input, OutputInterface $output, $results)
406 $commandData = $this->createCommandData($input, $output);
407 $commandProcessor = $this->commandProcessor();
408 $names = $this->getNames();
409 $results = $commandProcessor->processResults(
414 return $commandProcessor->handleResults(
422 protected function createCommandData(InputInterface $input, OutputInterface $output)
424 $commandData = new CommandData(
425 $this->annotationData,
430 $commandData->setUseIOInterfaces(
431 $this->usesOutputInterface,
432 $this->usesInputInterface