Completed
Pull Request — master (#34)
by Greg
02:46
created

HookManager::callProcessor()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 8
nop 4
1
<?php
2
namespace Consolidation\AnnotatedCommand\Hooks;
3
4
use Symfony\Component\Console\Command\Command;
5
use Symfony\Component\Console\Input\InputInterface;
6
use Symfony\Component\Console\Output\OutputInterface;
7
8
use Symfony\Component\Console\ConsoleEvents;
9
use Symfony\Component\Console\Event\ConsoleCommandEvent;
10
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11
12
use Consolidation\AnnotatedCommand\ExitCodeInterface;
13
use Consolidation\AnnotatedCommand\OutputDataInterface;
14
use Consolidation\AnnotatedCommand\AnnotationData;
15
16
/**
17
 * Manage named callback hooks
18
 */
19
class HookManager implements EventSubscriberInterface
20
{
21
    protected $hooks = [];
22
23
    const PRE_COMMAND_EVENT = 'pre-command';
24
    const COMMAND_EVENT = 'command';
25
    const POST_COMMAND_EVENT = 'post-command';
26
    const PRE_ARGUMENT_VALIDATOR = 'pre-validate';
27
    const ARGUMENT_VALIDATOR = 'validate';
28
    const POST_ARGUMENT_VALIDATOR = 'post-validate';
29
    const PRE_PROCESS_RESULT = 'pre-process';
30
    const PROCESS_RESULT = 'process';
31
    const POST_PROCESS_RESULT = 'post-process';
32
    const PRE_ALTER_RESULT = 'pre-alter';
33
    const ALTER_RESULT = 'alter';
34
    const POST_ALTER_RESULT = 'post-alter';
35
    const STATUS_DETERMINER = 'status';
36
    const EXTRACT_OUTPUT = 'extract';
37
38
    public function __construct()
39
    {
40
    }
41
42
    /**
43
     * Add a hook
44
     *
45
     * @param mixed $callback The callback function to call
46
     * @param string   $hook     The name of the hook to add
47
     * @param string   $name     The name of the command to hook
48
     *   ('*' for all)
49
     */
50
    public function add(callable $callback, $hook, $name = '*')
51
    {
52
        $this->hooks[$name][$hook][] = $callback;
53
    }
54
55
    /**
56
     * Add a pre-validator hook
57
     *
58
     * @param type ValidatorInterface $validator
59
     * @param type $name The name of the command to hook
60
     *   ('*' for all)
61
     */
62
    public function addPreValidator(ValidatorInterface $validator, $name = '*')
63
    {
64
        $this->hooks[$name][self::PRE_ARGUMENT_VALIDATOR][] = $validator;
65
    }
66
67
    /**
68
     * Add a validator hook
69
     *
70
     * @param type ValidatorInterface $validator
71
     * @param type $name The name of the command to hook
72
     *   ('*' for all)
73
     */
74
    public function addValidator(ValidatorInterface $validator, $name = '*')
75
    {
76
        $this->hooks[$name][self::ARGUMENT_VALIDATOR][] = $validator;
77
    }
78
79
    /**
80
     * Add a result processor.
81
     *
82
     * @param type ProcessResultInterface $resultProcessor
83
     * @param type $name The name of the command to hook
84
     *   ('*' for all)
85
     */
86
    public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*')
87
    {
88
        $this->hooks[$name][self::PROCESS_RESULT][] = $resultProcessor;
89
    }
90
91
    /**
92
     * Add a result alterer. After a result is processed
93
     * by a result processor, an alter hook may be used
94
     * to convert the result from one form to another.
95
     *
96
     * @param type AlterResultInterface $resultAlterer
97
     * @param type $name The name of the command to hook
98
     *   ('*' for all)
99
     */
100
    public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*')
101
    {
102
        $this->hooks[$name][self::ALTER_RESULT][] = $resultAlterer;
103
    }
104
105
    /**
106
     * Add a status determiner. Usually, a command should return
107
     * an integer on error, or a result object on success (which
108
     * implies a status code of zero). If a result contains the
109
     * status code in some other field, then a status determiner
110
     * can be used to call the appropriate accessor method to
111
     * determine the status code.  This is usually not necessary,
112
     * though; a command that fails may return a CommandError
113
     * object, which contains a status code and a result message
114
     * to display.
115
     * @see CommandError::getExitCode()
116
     *
117
     * @param type StatusDeterminerInterface $statusDeterminer
118
     * @param type $name The name of the command to hook
119
     *   ('*' for all)
120
     */
121
    public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*')
122
    {
123
        $this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer;
124
    }
125
126
    /**
127
     * Add an output extractor. If a command returns an object
128
     * object, by default it is passed directly to the output
129
     * formatter (if in use) for rendering. If the result object
130
     * contains more information than just the data to render, though,
131
     * then an output extractor can be used to call the appopriate
132
     * accessor method of the result object to get the data to
133
     * rendered.  This is usually not necessary, though; it is preferable
134
     * to have complex result objects implement the OutputDataInterface.
135
     * @see OutputDataInterface::getOutputData()
136
     *
137
     * @param type ExtractOutputInterface $outputExtractor
138
     * @param type $name The name of the command to hook
139
     *   ('*' for all)
140
     */
141
    public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*')
142
    {
143
        $this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor;
144
    }
145
146 View Code Duplication
    public function validateArguments($names, $args, AnnotationData $annotationData)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
147
    {
148
        $validators = $this->getValidators($names, $annotationData);
149
        foreach ($validators as $validator) {
150
            $validated = $this->callValidator($validator, $args, $annotationData);
151
            if (is_object($validated)) {
152
                return $validated;
153
            }
154
            if (is_array($validated)) {
155
                $args = $validated;
156
            }
157
        }
158
        return $args;
159
    }
160
161
    /**
162
     * Process result and decide what to do with it.
163
     * Allow client to add transformation / interpretation
164
     * callbacks.
165
     */
166
    public function alterResult($names, $result, $args, AnnotationData $annotationData)
167
    {
168
        $processors = $this->getProcessResultHooks($names, $annotationData);
169
        foreach ($processors as $processor) {
170
            $result = $this->callProcessor($processor, $result, $args, $annotationData);
171
        }
172
        $alterers = $this->getAlterResultHooks($names, $annotationData);
173
        foreach ($alterers as $alterer) {
174
            $result = $this->callProcessor($alterer, $result, $args, $annotationData);
175
        }
176
177
        return $result;
178
    }
179
180
    /**
181
     * Call all status determiners, and see if any of them
182
     * know how to convert to a status code.
183
     */
184
    public function determineStatusCode($names, $result)
185
    {
186
        // If the result (post-processing) is an object that
187
        // implements ExitCodeInterface, then we will ask it
188
        // to give us the status code.
189
        if ($result instanceof ExitCodeInterface) {
190
            return $result->getExitCode();
191
        }
192
193
        // If the result does not implement ExitCodeInterface,
194
        // then we'll see if there is a determiner that can
195
        // extract a status code from the result.
196
        $determiners = $this->getStatusDeterminers($names);
197
        foreach ($determiners as $determiner) {
198
            $status = $this->callDeterminer($determiner, $result);
199
            if (isset($status)) {
200
                return $status;
201
            }
202
        }
203
    }
204
205
    /**
206
     * Convert the result object to printable output in
207
     * structured form.
208
     */
209
    public function extractOutput($names, $result)
210
    {
211
        if ($result instanceof OutputDataInterface) {
212
            return $result->getOutputData();
213
        }
214
215
        $extractors = $this->getOutputExtractors($names);
216
        foreach ($extractors as $extractor) {
217
            $structuredOutput = $this->callExtractor($extractor, $result);
218
            if (isset($structuredOutput)) {
219
                return $structuredOutput;
220
            }
221
        }
222
223
        return $result;
224
    }
225
226
    protected function getValidators($names, AnnotationData $annotationData)
227
    {
228
        return $this->getHooks($names, self::ARGUMENT_VALIDATOR, $annotationData);
229
    }
230
231
    protected function getStatusDeterminers($names)
232
    {
233
        return $this->getHooks($names, self::STATUS_DETERMINER);
234
    }
235
236
    protected function getProcessResultHooks($names, AnnotationData $annotationData)
237
    {
238
        return $this->getHooks($names, self::PROCESS_RESULT, $annotationData);
239
    }
240
241
    protected function getAlterResultHooks($names, AnnotationData $annotationData)
242
    {
243
        return $this->getHooks($names, self::ALTER_RESULT, $annotationData);
244
    }
245
246
    protected function getOutputExtractors($names)
247
    {
248
        return $this->getHooks($names, self::EXTRACT_OUTPUT);
249
    }
250
251
    protected function getCommandEvents($names)
252
    {
253
        return $this->getHooks($names, self::COMMAND_EVENT);
254
    }
255
256
    /**
257
     * Get a set of hooks with the provided name(s). Include the
258
     * pre- and post- hooks, and also include the global hooks ('*')
259
     * in addition to the named hooks provided.
260
     *
261
     * @param string|array $names The name of the function being hooked.
262
     * @param string $hook The specific hook name (e.g. alter)
263
     *
264
     * @return callable[]
265
     */
266
    protected function getHooks($names, $hook, $annotationData = null)
267
    {
268
        $names = array_merge(
269
            (array)$names,
270
            ($annotationData == null) ? [] : array_map(function ($item) {
271
                return "@$item";
272
            }, $annotationData->keys())
273
        );
274
        $names[] = '*';
275
        return array_merge(
276
            $this->get($names, "pre-$hook"),
277
            $this->get($names, $hook),
278
            $this->get($names, "post-$hook")
279
        );
280
    }
281
282
    /**
283
     * Get a set of hooks with the provided name(s).
284
     *
285
     * @param string|array $names The name of the function being hooked.
286
     * @param string $hook The specific hook name (e.g. alter)
287
     *
288
     * @return callable[]
289
     */
290
    public function get($names, $hook)
291
    {
292
        $hooks = [];
293
        foreach ((array)$names as $name) {
294
            $hooks = array_merge($hooks, $this->getHook($name, $hook));
295
        }
296
        return $hooks;
297
    }
298
299
    /**
300
     * Get a single named hook.
301
     *
302
     * @param string $name The name of the hooked method
303
     * @param string $hook The specific hook name (e.g. alter)
304
     *
305
     * @return callable[]
306
     */
307
    protected function getHook($name, $hook)
308
    {
309
        if (isset($this->hooks[$name][$hook])) {
310
            return $this->hooks[$name][$hook];
311
        }
312
        return [];
313
    }
314
315
    protected function callValidator($validator, $args, AnnotationData $annotationData)
316
    {
317
        // TODO: Adding AnnotationData to ValidatorInterface would be
318
        // a breaking change. Either hold off until 2.x, or make
319
        // a new interface containing a method that takes the extra parameter.
320
        if ($validator instanceof ValidatorInterface) {
321
            return $validator->validate($args);
322
        }
323
        if (is_callable($validator)) {
324
            return $validator($args, $annotationData);
325
        }
326
    }
327
328
    protected function callProcessor($processor, $result, $args, AnnotationData $annotationData)
329
    {
330
        $processed = null;
331
        // TODO: Adding AnnotationData to ProcessResultInterface would be
332
        // a breaking change. Either hold off until 2.x, or make
333
        // a new interface containing a method that takes the extra parameter.
334
        if ($processor instanceof ProcessResultInterface) {
335
            $processed = $processor->process($result, $args);
336
        }
337
        if (is_callable($processor)) {
338
            $processed = $processor($result, $args, $annotationData);
339
        }
340
        if (isset($processed)) {
341
            return $processed;
342
        }
343
        return $result;
344
    }
345
346
    protected function callDeterminer($determiner, $result)
347
    {
348
        if ($determiner instanceof StatusDeterminerInterface) {
349
            return $determiner->determineStatusCode($result);
350
        }
351
        if (is_callable($determiner)) {
352
            return $determiner($result);
353
        }
354
    }
355
356
    protected function callExtractor($extractor, $result)
357
    {
358
        if ($extractor instanceof ExtractOutputInterface) {
359
            return $extractor->extractOutput($result);
360
        }
361
        if (is_callable($extractor)) {
362
            return $extractor($result);
363
        }
364
    }
365
366
    /**
367
     * @param ConsoleCommandEvent $event
368
     */
369
    public function callCommandEventHooks(ConsoleCommandEvent $event)
370
    {
371
        /* @var Command $command */
372
        $command = $event->getCommand();
373
        $names = [$command->getName()];
374
        $commandEventHooks = $this->getCommandEvents($names);
375
        foreach ($commandEventHooks as $commandEvent) {
376
            if (is_callable($commandEvent)) {
377
                $commandEvent($event);
378
            }
379
        }
380
    }
381
382
    /**
383
     * @{@inheritdoc}
384
     */
385
    public static function getSubscribedEvents()
386
    {
387
        return [ConsoleEvents::COMMAND => 'callCommandEventHooks'];
388
    }
389
}
390