Completed
Pull Request — master (#179)
by Greg
01:42
created

CommandProcessor::dataCanBeFormatted()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
namespace Consolidation\AnnotatedCommand;
3
4
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\ReplaceCommandHookDispatcher;
5
use Psr\Log\LoggerAwareInterface;
6
use Psr\Log\LoggerAwareTrait;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Output\OutputInterface;
9
use Symfony\Component\Console\Output\ConsoleOutputInterface;
10
11
use Consolidation\OutputFormatters\FormatterManager;
12
use Consolidation\OutputFormatters\Options\FormatterOptions;
13
use Consolidation\AnnotatedCommand\Hooks\HookManager;
14
use Consolidation\AnnotatedCommand\Options\PrepareFormatter;
15
16
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\InitializeHookDispatcher;
17
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\OptionsHookDispatcher;
18
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\InteractHookDispatcher;
19
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\ValidateHookDispatcher;
20
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\ProcessResultHookDispatcher;
21
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\StatusDeterminerHookDispatcher;
22
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\ExtracterHookDispatcher;
23
24
/**
25
 * Process a command, including hooks and other callbacks.
26
 * There should only be one command processor per application.
27
 * Provide your command processor to the AnnotatedCommandFactory
28
 * via AnnotatedCommandFactory::setCommandProcessor().
29
 */
30
class CommandProcessor implements LoggerAwareInterface
31
{
32
    use LoggerAwareTrait;
33
34
    /** var HookManager */
35
    protected $hookManager;
36
    /** var FormatterManager */
37
    protected $formatterManager;
38
    /** var callable */
39
    protected $displayErrorFunction;
40
    /** var PrepareFormatterOptions[] */
41
    protected $prepareOptionsList = [];
42
    /** var boolean */
43
    protected $passExceptions;
44
45
    public function __construct(HookManager $hookManager)
46
    {
47
        $this->hookManager = $hookManager;
48
    }
49
50
    /**
51
     * Return the hook manager
52
     * @return HookManager
53
     */
54
    public function hookManager()
55
    {
56
        return $this->hookManager;
57
    }
58
59
    public function addPrepareFormatter(PrepareFormatter $preparer)
60
    {
61
        $this->prepareOptionsList[] = $preparer;
62
    }
63
64
    public function setFormatterManager(FormatterManager $formatterManager)
65
    {
66
        $this->formatterManager = $formatterManager;
67
        return $this;
68
    }
69
70
    public function setDisplayErrorFunction(callable $fn)
71
    {
72
        $this->displayErrorFunction = $fn;
73
        return $this;
74
    }
75
76
    /**
77
     * Set a mode to make the annotated command library re-throw
78
     * any exception that it catches while processing a command.
79
     *
80
     * The default behavior in the current (2.x) branch is to catch
81
     * the exception and replace it with a CommandError object that
82
     * may be processed by the normal output processing passthrough.
83
     *
84
     * In the 3.x branch, exceptions will never be caught; they will
85
     * be passed through, as if setPassExceptions(true) were called.
86
     * This is the recommended behavior.
87
     */
88
    public function setPassExceptions($passExceptions)
89
    {
90
        $this->passExceptions = $passExceptions;
91
        return $this;
92
    }
93
94
    public function commandErrorForException(\Exception $e)
95
    {
96
        if ($this->passExceptions) {
97
            throw $e;
98
        }
99
        return new CommandError($e->getMessage(), $e->getCode());
100
    }
101
102
    /**
103
     * Return the formatter manager
104
     * @return FormatterManager
105
     */
106
    public function formatterManager()
107
    {
108
        return $this->formatterManager;
109
    }
110
111
    public function initializeHook(
112
        InputInterface $input,
113
        $names,
114
        AnnotationData $annotationData
115
    ) {
116
        $initializeDispatcher = new InitializeHookDispatcher($this->hookManager(), $names);
117
        return $initializeDispatcher->initialize($input, $annotationData);
118
    }
119
120
    public function optionsHook(
121
        AnnotatedCommand $command,
122
        $names,
123
        AnnotationData $annotationData
124
    ) {
125
        $optionsDispatcher = new OptionsHookDispatcher($this->hookManager(), $names);
126
        $optionsDispatcher->getOptions($command, $annotationData);
127
    }
128
129
    public function interact(
130
        InputInterface $input,
131
        OutputInterface $output,
132
        $names,
133
        AnnotationData $annotationData
134
    ) {
135
        $interactDispatcher = new InteractHookDispatcher($this->hookManager(), $names);
136
        return $interactDispatcher->interact($input, $output, $annotationData);
137
    }
138
139
    public function process(
140
        OutputInterface $output,
141
        $names,
142
        $commandCallback,
143
        CommandData $commandData
144
    ) {
145
        $result = [];
146
        try {
147
            $result = $this->validateRunAndAlter(
148
                $names,
149
                $commandCallback,
150
                $commandData
151
            );
152
            return $this->handleResults($output, $names, $result, $commandData);
153
        } catch (\Exception $e) {
154
            $result = $this->commandErrorForException($e);
155
            return $this->handleResults($output, $names, $result, $commandData);
156
        }
157
    }
158
159
    public function validateRunAndAlter(
160
        $names,
161
        $commandCallback,
162
        CommandData $commandData
163
    ) {
164
        // Validators return any object to signal a validation error;
165
        // if the return an array, it replaces the arguments.
166
        $validateDispatcher = new ValidateHookDispatcher($this->hookManager(), $names);
167
        $validated = $validateDispatcher->validate($commandData);
168
        if (is_object($validated)) {
169
            return $validated;
170
        }
171
172
        // Once we have validated the optins, create the formatter options.
173
        $this->createFormatterOptions($commandData);
174
175
        $replaceDispatcher = new ReplaceCommandHookDispatcher($this->hookManager(), $names);
176
        if ($this->logger) {
177
            $replaceDispatcher->setLogger($this->logger);
178
        }
179
        if ($replaceDispatcher->hasReplaceCommandHook()) {
180
            $commandCallback = $replaceDispatcher->getReplacementCommand($commandData);
181
        }
182
183
        // Run the command, alter the results, and then handle output and status
184
        $result = $this->runCommandCallback($commandCallback, $commandData);
185
        return $this->processResults($names, $result, $commandData);
186
    }
187
188
    public function processResults($names, $result, CommandData $commandData)
189
    {
190
        $processDispatcher = new ProcessResultHookDispatcher($this->hookManager(), $names);
191
        return $processDispatcher->process($result, $commandData);
192
    }
193
194
    /**
195
     * Create a FormatterOptions object for use in writing the formatted output.
196
     * @param CommandData $commandData
197
     * @return FormatterOptions
198
     */
199
    protected function createFormatterOptions($commandData)
200
    {
201
        $options = $commandData->input()->getOptions();
202
        $formatterOptions = new FormatterOptions($commandData->annotationData()->getArrayCopy(), $options);
203
        foreach ($this->prepareOptionsList as $preparer) {
204
            $preparer->prepare($commandData, $formatterOptions);
205
        }
206
        $commandData->setFormatterOptions($formatterOptions);
207
        return $formatterOptions;
208
    }
209
210
    /**
211
     * Handle the result output and status code calculation.
212
     */
213
    public function handleResults(OutputInterface $output, $names, $result, CommandData $commandData)
214
    {
215
        $statusCodeDispatcher = new StatusDeterminerHookDispatcher($this->hookManager(), $names);
216
        // A little messy, for backwards compatibility: if the result implements
217
        // ExitCodeInterface, then use that as the exit code. If a status code
218
        // dispatcher returns a non-zero result, then we will never print a
219
        // result.
220
        if ($result instanceof ExitCodeInterface) {
221
            $status = $result->getExitCode();
222
        } else {
223
            $status = $statusCodeDispatcher->determineStatusCode($result);
224
            if (isset($status) && ($status != 0)) {
225
                return $status;
226
            }
227
        }
228
        // If the result is an integer and no separate status code was provided, then use the result as the status and do no output.
229
        if (is_integer($result) && !isset($status)) {
230
            return $result;
231
        }
232
        $status = $this->interpretStatusCode($status);
233
234
        // Get the structured output, the output stream and the formatter
235
        $extractDispatcher = new ExtracterHookDispatcher($this->hookManager(), $names);
236
        $structuredOutput = $extractDispatcher->extractOutput($result);
237
        if (($status != 0) && is_string($structuredOutput)) {
238
            $output = $this->chooseOutputStream($output, $status);
239
            return $this->writeErrorMessage($output, $status, $structuredOutput, $result);
240
        }
241
        if ($this->dataCanBeFormatted($structuredOutput) && isset($this->formatterManager)) {
242
            return $this->writeUsingFormatter($output, $structuredOutput, $commandData, $status);
243
        }
244
        return $this->writeCommandOutput($output, $structuredOutput, $status);
245
    }
246
247
    protected function dataCanBeFormatted($structuredOutput)
248
    {
249
        if (!isset($this->formatterManager)) {
250
            return false;
251
        }
252
        return
253
            is_object($structuredOutput) ||
254
            is_array($structuredOutput);
255
    }
256
257
    /**
258
     * Run the main command callback
259
     */
260
    protected function runCommandCallback($commandCallback, CommandData $commandData)
261
    {
262
        $result = false;
263
        try {
264
            $args = array_merge(
265
                $commandData->injectedInstances(),
266
                $commandData->getArgsAndOptions()
267
            );
268
            $result = call_user_func_array($commandCallback, $args);
269
        } catch (\Exception $e) {
270
            $result = $this->commandErrorForException($e);
271
        }
272
        return $result;
273
    }
274
275
    public function injectIntoCommandData($commandData, $injectedClasses)
276
    {
277
        foreach ($injectedClasses as $injectedClass) {
278
            $injectedInstance = $this->getInstanceToInject($commandData, $injectedClass);
279
            $commandData->injectInstance($injectedInstance);
280
        }
281
    }
282
283
    protected function getInstanceToInject(CommandData $commandData, $injectedClass)
284
    {
285
        switch ($injectedClass) {
286
            case 'Symfony\Component\Console\Input\InputInterface':
287
                return $commandData->input();
288
            case 'Symfony\Component\Console\Output\OutputInterface':
289
                return $commandData->output();
290
        }
291
292
        return null;
293
    }
294
295
    /**
296
     * Determine the formatter that should be used to render
297
     * output.
298
     *
299
     * If the user specified a format via the --format option,
300
     * then always return that.  Otherwise, return the default
301
     * format, unless --pipe was specified, in which case
302
     * return the default pipe format, format-pipe.
303
     *
304
     * n.b. --pipe is a handy option introduced in Drush 2
305
     * (or perhaps even Drush 1) that indicates that the command
306
     * should select the output format that is most appropriate
307
     * for use in scripts (e.g. to pipe to another command).
308
     *
309
     * @return string
310
     */
311
    protected function getFormat(FormatterOptions $options)
312
    {
313
        // In Symfony Console, there is no way for us to differentiate
314
        // between the user specifying '--format=table', and the user
315
        // not specifying --format when the default value is 'table'.
316
        // Therefore, we must make --field always override --format; it
317
        // cannot become the default value for --format.
318
        if ($options->get('field')) {
319
            return 'string';
320
        }
321
        $defaults = [];
322
        if ($options->get('pipe')) {
323
            return $options->get('pipe-format', [], 'tsv');
324
        }
325
        return $options->getFormat($defaults);
326
    }
327
328
    /**
329
     * Determine whether we should use stdout or stderr.
330
     */
331
    protected function chooseOutputStream(OutputInterface $output, $status)
332
    {
333
        // If the status code indicates an error, then print the
334
        // result to stderr rather than stdout
335
        if ($status && ($output instanceof ConsoleOutputInterface)) {
336
            return $output->getErrorOutput();
337
        }
338
        return $output;
339
    }
340
341
    /**
342
     * Call the formatter to output the provided data.
343
     */
344
    protected function writeUsingFormatter(OutputInterface $output, $structuredOutput, CommandData $commandData, $status = 0)
345
    {
346
        $formatterOptions = $commandData->formatterOptions();
347
        $format = $this->getFormat($formatterOptions);
348
        $this->formatterManager->write(
349
            $output,
350
            $format,
351
            $structuredOutput,
352
            $formatterOptions
353
        );
354
        return $status;
355
    }
356
357
    /**
358
     * Description
359
     * @param OutputInterface $output
360
     * @param int $status
361
     * @param string $structuredOutput
362
     * @param mixed $originalResult
363
     * @return type
364
     */
365
    protected function writeErrorMessage($output, $status, $structuredOutput, $originalResult)
366
    {
367
        if (isset($this->displayErrorFunction)) {
368
            call_user_func($this->displayErrorFunction, $output, $structuredOutput, $status, $originalResult);
369
        } else {
370
            $this->writeCommandOutput($output, $structuredOutput);
371
        }
372
        return $status;
373
    }
374
375
    /**
376
     * If the result object is a string, then print it.
377
     */
378
    protected function writeCommandOutput(
379
        OutputInterface $output,
380
        $structuredOutput,
381
        $status = 0
382
    ) {
383
        // If there is no formatter, we will print strings,
384
        // but can do no more than that.
385
        if (is_string($structuredOutput)) {
386
            $output->writeln($structuredOutput);
387
        }
388
        return $status;
389
    }
390
391
    /**
392
     * If a status code was set, then return it; otherwise,
393
     * presume success.
394
     */
395
    protected function interpretStatusCode($status)
396
    {
397
        if (isset($status)) {
398
            return $status;
399
        }
400
        return 0;
401
    }
402
}
403