Completed
Pull Request — master (#103)
by Greg
01:49
created

CommandProcessor::commandErrorForException()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 4
nc 2
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
    }
92
93
    public function commandErrorForException(\Exception $e)
94
    {
95
        if ($this->passExceptions) {
96
            throw $e;
97
        }
98
        return new CommandError($e->getMessage(), $e->getCode());
99
    }
100
101
    /**
102
     * Return the formatter manager
103
     * @return FormatterManager
104
     */
105
    public function formatterManager()
106
    {
107
        return $this->formatterManager;
108
    }
109
110
    public function initializeHook(
111
        InputInterface $input,
112
        $names,
113
        AnnotationData $annotationData
114
    ) {
115
        $initializeDispatcher = new InitializeHookDispatcher($this->hookManager(), $names);
116
        return $initializeDispatcher->initialize($input, $annotationData);
117
    }
118
119
    public function optionsHook(
120
        AnnotatedCommand $command,
121
        $names,
122
        AnnotationData $annotationData
123
    ) {
124
        $optionsDispatcher = new OptionsHookDispatcher($this->hookManager(), $names);
125
        $optionsDispatcher->getOptions($command, $annotationData);
126
    }
127
128
    public function interact(
129
        InputInterface $input,
130
        OutputInterface $output,
131
        $names,
132
        AnnotationData $annotationData
133
    ) {
134
        $interactDispatcher = new InteractHookDispatcher($this->hookManager(), $names);
135
        return $interactDispatcher->interact($input, $output, $annotationData);
136
    }
137
138
    public function process(
139
        OutputInterface $output,
140
        $names,
141
        $commandCallback,
142
        CommandData $commandData
143
    ) {
144
        $result = [];
145
        try {
146
            $result = $this->validateRunAndAlter(
147
                $names,
148
                $commandCallback,
149
                $commandData
150
            );
151
            return $this->handleResults($output, $names, $result, $commandData);
152
        } catch (\Exception $e) {
153
            $result = $this->commandErrorForException($e);
154
            return $this->handleResults($output, $names, $result, $commandData);
155
        }
156
    }
157
158
    public function validateRunAndAlter(
159
        $names,
160
        $commandCallback,
161
        CommandData $commandData
162
    ) {
163
        // Validators return any object to signal a validation error;
164
        // if the return an array, it replaces the arguments.
165
        $validateDispatcher = new ValidateHookDispatcher($this->hookManager(), $names);
166
        $validated = $validateDispatcher->validate($commandData);
167
        if (is_object($validated)) {
168
            return $validated;
169
        }
170
171
        $replaceDispatcher = new ReplaceCommandHookDispatcher($this->hookManager(), $names);
172
        if ($this->logger) {
173
            $replaceDispatcher->setLogger($this->logger);
174
        }
175
        if ($replaceDispatcher->hasReplaceCommandHook()) {
176
            $commandCallback = $replaceDispatcher->getReplacementCommand($commandData);
177
        }
178
179
        // Run the command, alter the results, and then handle output and status
180
        $result = $this->runCommandCallback($commandCallback, $commandData);
181
        return $this->processResults($names, $result, $commandData);
182
    }
183
184
    public function processResults($names, $result, CommandData $commandData)
185
    {
186
        $processDispatcher = new ProcessResultHookDispatcher($this->hookManager(), $names);
187
        return $processDispatcher->process($result, $commandData);
188
    }
189
190
    /**
191
     * Handle the result output and status code calculation.
192
     */
193
    public function handleResults(OutputInterface $output, $names, $result, CommandData $commandData)
194
    {
195
        $statusCodeDispatcher = new StatusDeterminerHookDispatcher($this->hookManager(), $names);
196
        $status = $statusCodeDispatcher->determineStatusCode($result);
197
        // If the result is an integer and no separate status code was provided, then use the result as the status and do no output.
198
        if (is_integer($result) && !isset($status)) {
199
            return $result;
200
        }
201
        $status = $this->interpretStatusCode($status);
202
203
        // Get the structured output, the output stream and the formatter
204
        $extractDispatcher = new ExtracterHookDispatcher($this->hookManager(), $names);
205
        $structuredOutput = $extractDispatcher->extractOutput($result);
206
        $output = $this->chooseOutputStream($output, $status);
207
        if ($status != 0) {
208
            return $this->writeErrorMessage($output, $status, $structuredOutput, $result);
209
        }
210
        if ($this->dataCanBeFormatted($structuredOutput) && isset($this->formatterManager)) {
211
            return $this->writeUsingFormatter($output, $structuredOutput, $commandData);
212
        }
213
        return $this->writeCommandOutput($output, $structuredOutput);
214
    }
215
216
    protected function dataCanBeFormatted($structuredOutput)
217
    {
218
        if (!isset($this->formatterManager)) {
219
            return false;
220
        }
221
        return
222
            is_object($structuredOutput) ||
223
            is_array($structuredOutput);
224
    }
225
226
    /**
227
     * Run the main command callback
228
     */
229
    protected function runCommandCallback($commandCallback, CommandData $commandData)
230
    {
231
        $result = false;
232
        try {
233
            $args = $commandData->getArgsAndOptions();
234
            $result = call_user_func_array($commandCallback, $args);
235
        } catch (\Exception $e) {
236
            $result = $this->commandErrorForException($e);
237
        }
238
        return $result;
239
    }
240
241
    /**
242
     * Determine the formatter that should be used to render
243
     * output.
244
     *
245
     * If the user specified a format via the --format option,
246
     * then always return that.  Otherwise, return the default
247
     * format, unless --pipe was specified, in which case
248
     * return the default pipe format, format-pipe.
249
     *
250
     * n.b. --pipe is a handy option introduced in Drush 2
251
     * (or perhaps even Drush 1) that indicates that the command
252
     * should select the output format that is most appropriate
253
     * for use in scripts (e.g. to pipe to another command).
254
     *
255
     * @return string
256
     */
257
    protected function getFormat(FormatterOptions $options)
258
    {
259
        // In Symfony Console, there is no way for us to differentiate
260
        // between the user specifying '--format=table', and the user
261
        // not specifying --format when the default value is 'table'.
262
        // Therefore, we must make --field always override --format; it
263
        // cannot become the default value for --format.
264
        if ($options->get('field')) {
265
            return 'string';
266
        }
267
        $defaults = [];
268
        if ($options->get('pipe')) {
269
            return $options->get('pipe-format', [], 'tsv');
270
        }
271
        return $options->getFormat($defaults);
272
    }
273
274
    /**
275
     * Determine whether we should use stdout or stderr.
276
     */
277
    protected function chooseOutputStream(OutputInterface $output, $status)
278
    {
279
        // If the status code indicates an error, then print the
280
        // result to stderr rather than stdout
281
        if ($status && ($output instanceof ConsoleOutputInterface)) {
282
            return $output->getErrorOutput();
283
        }
284
        return $output;
285
    }
286
287
    /**
288
     * Call the formatter to output the provided data.
289
     */
290
    protected function writeUsingFormatter(OutputInterface $output, $structuredOutput, CommandData $commandData)
291
    {
292
        $formatterOptions = $this->createFormatterOptions($commandData);
293
        $format = $this->getFormat($formatterOptions);
294
        $this->formatterManager->write(
295
            $output,
296
            $format,
297
            $structuredOutput,
298
            $formatterOptions
299
        );
300
        return 0;
301
    }
302
303
    /**
304
     * Create a FormatterOptions object for use in writing the formatted output.
305
     * @param CommandData $commandData
306
     * @return FormatterOptions
307
     */
308
    protected function createFormatterOptions($commandData)
309
    {
310
        $options = $commandData->input()->getOptions();
311
        $formatterOptions = new FormatterOptions($commandData->annotationData()->getArrayCopy(), $options);
312
        foreach ($this->prepareOptionsList as $preparer) {
313
            $preparer->prepare($commandData, $formatterOptions);
314
        }
315
        return $formatterOptions;
316
    }
317
318
    /**
319
     * Description
320
     * @param OutputInterface $output
321
     * @param int $status
322
     * @param string $structuredOutput
323
     * @param mixed $originalResult
324
     * @return type
325
     */
326
    protected function writeErrorMessage($output, $status, $structuredOutput, $originalResult)
327
    {
328
        if (isset($this->displayErrorFunction)) {
329
            call_user_func($this->displayErrorFunction, $output, $structuredOutput, $status, $originalResult);
330
        } else {
331
            $this->writeCommandOutput($output, $structuredOutput);
332
        }
333
        return $status;
334
    }
335
336
    /**
337
     * If the result object is a string, then print it.
338
     */
339
    protected function writeCommandOutput(
340
        OutputInterface $output,
341
        $structuredOutput
342
    ) {
343
        // If there is no formatter, we will print strings,
344
        // but can do no more than that.
345
        if (is_string($structuredOutput)) {
346
            $output->writeln($structuredOutput);
347
        }
348
        return 0;
349
    }
350
351
    /**
352
     * If a status code was set, then return it; otherwise,
353
     * presume success.
354
     */
355
    protected function interpretStatusCode($status)
356
    {
357
        if (isset($status)) {
358
            return $status;
359
        }
360
        return 0;
361
    }
362
}
363