Completed
Push — master ( 8277a3...7a221a )
by Greg
10s
created

HookManager::addInteractor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
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_INTERACT = 'pre-interact';
24
    const INTERACT = 'interact';
25
    const POST_INTERACT = 'post-interact';
26
    const PRE_ARGUMENT_VALIDATOR = 'pre-validate';
27
    const ARGUMENT_VALIDATOR = 'validate';
28
    const POST_ARGUMENT_VALIDATOR = 'post-validate';
29
    const PRE_COMMAND_EVENT = 'pre-command';
30
    const COMMAND_EVENT = 'command';
31
    const POST_COMMAND_EVENT = 'post-command';
32
    const PRE_PROCESS_RESULT = 'pre-process';
33
    const PROCESS_RESULT = 'process';
34
    const POST_PROCESS_RESULT = 'post-process';
35
    const PRE_ALTER_RESULT = 'pre-alter';
36
    const ALTER_RESULT = 'alter';
37
    const POST_ALTER_RESULT = 'post-alter';
38
    const STATUS_DETERMINER = 'status';
39
    const EXTRACT_OUTPUT = 'extract';
40
41
    public function __construct()
42
    {
43
    }
44
45
    public function getAllHooks()
46
    {
47
        return $this->hooks;
48
    }
49
50
    /**
51
     * Add a hook
52
     *
53
     * @param mixed $callback The callback function to call
54
     * @param string   $hook     The name of the hook to add
55
     * @param string   $name     The name of the command to hook
56
     *   ('*' for all)
57
     */
58
    public function add(callable $callback, $hook, $name = '*')
59
    {
60
        if (empty($name)) {
61
            $name = static::getClassNameFromCallback($callback);
62
        }
63
        $this->hooks[$name][$hook][] = $callback;
64
    }
65
66
    /**
67
     * If a command hook does not specify any particular command
68
     * name that it should be attached to, then it will be applied
69
     * to every command that is defined in the same class as the hook.
70
     * This is controlled by using the namespace + class name of
71
     * the implementing class of the callback hook.
72
     */
73
    public static function getClassNameFromCallback($callback)
74
    {
75
        if (!is_array($callback)) {
76
            return '';
77
        }
78
        $reflectionClass = new \ReflectionClass($callback[0]);
79
        return $reflectionClass->getName();
80
    }
81
82
    /**
83
     * Add an interact hook
84
     *
85
     * @param type ValidatorInterface $validator
86
     * @param type $name The name of the command to hook
87
     *   ('*' for all)
88
     */
89
    public function addInteractor(InteractorInterface $interactor, $name = '*')
90
    {
91
        $this->hooks[$name][self::INTERACT][] = $interactor;
92
    }
93
94
    /**
95
     * Add a pre-validator hook
96
     *
97
     * @param type ValidatorInterface $validator
98
     * @param type $name The name of the command to hook
99
     *   ('*' for all)
100
     */
101
    public function addPreValidator(ValidatorInterface $validator, $name = '*')
102
    {
103
        $this->hooks[$name][self::PRE_ARGUMENT_VALIDATOR][] = $validator;
104
    }
105
106
    /**
107
     * Add a validator hook
108
     *
109
     * @param type ValidatorInterface $validator
110
     * @param type $name The name of the command to hook
111
     *   ('*' for all)
112
     */
113
    public function addValidator(ValidatorInterface $validator, $name = '*')
114
    {
115
        $this->hooks[$name][self::ARGUMENT_VALIDATOR][] = $validator;
116
    }
117
118
    /**
119
     * Add a result processor.
120
     *
121
     * @param type ProcessResultInterface $resultProcessor
122
     * @param type $name The name of the command to hook
123
     *   ('*' for all)
124
     */
125
    public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*')
126
    {
127
        $this->hooks[$name][self::PROCESS_RESULT][] = $resultProcessor;
128
    }
129
130
    /**
131
     * Add a result alterer. After a result is processed
132
     * by a result processor, an alter hook may be used
133
     * to convert the result from one form to another.
134
     *
135
     * @param type AlterResultInterface $resultAlterer
136
     * @param type $name The name of the command to hook
137
     *   ('*' for all)
138
     */
139
    public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*')
140
    {
141
        $this->hooks[$name][self::ALTER_RESULT][] = $resultAlterer;
142
    }
143
144
    /**
145
     * Add a status determiner. Usually, a command should return
146
     * an integer on error, or a result object on success (which
147
     * implies a status code of zero). If a result contains the
148
     * status code in some other field, then a status determiner
149
     * can be used to call the appropriate accessor method to
150
     * determine the status code.  This is usually not necessary,
151
     * though; a command that fails may return a CommandError
152
     * object, which contains a status code and a result message
153
     * to display.
154
     * @see CommandError::getExitCode()
155
     *
156
     * @param type StatusDeterminerInterface $statusDeterminer
157
     * @param type $name The name of the command to hook
158
     *   ('*' for all)
159
     */
160
    public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*')
161
    {
162
        $this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer;
163
    }
164
165
    /**
166
     * Add an output extractor. If a command returns an object
167
     * object, by default it is passed directly to the output
168
     * formatter (if in use) for rendering. If the result object
169
     * contains more information than just the data to render, though,
170
     * then an output extractor can be used to call the appopriate
171
     * accessor method of the result object to get the data to
172
     * rendered.  This is usually not necessary, though; it is preferable
173
     * to have complex result objects implement the OutputDataInterface.
174
     * @see OutputDataInterface::getOutputData()
175
     *
176
     * @param type ExtractOutputInterface $outputExtractor
177
     * @param type $name The name of the command to hook
178
     *   ('*' for all)
179
     */
180
    public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*')
181
    {
182
        $this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor;
183
    }
184
185
    public function interact(
186
        InputInterface $input,
187
        OutputInterface $output,
188
        $names,
189
        AnnotationData $annotationData
190
    ) {
191
        $interactors = $this->getInteractors($names, $annotationData);
192
        foreach ($interactors as $interactor) {
193
            $this->callInteractor($interactor, $input, $output, $annotationData);
194
        }
195
    }
196
197 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...
198
    {
199
        $validators = $this->getValidators($names, $annotationData);
200
        foreach ($validators as $validator) {
201
            $validated = $this->callValidator($validator, $args, $annotationData);
202
            if (is_object($validated)) {
203
                return $validated;
204
            }
205
            if (is_array($validated)) {
206
                $args = $validated;
207
            }
208
        }
209
        return $args;
210
    }
211
212
    /**
213
     * Process result and decide what to do with it.
214
     * Allow client to add transformation / interpretation
215
     * callbacks.
216
     */
217
    public function alterResult($names, $result, $args, AnnotationData $annotationData)
218
    {
219
        $processors = $this->getProcessResultHooks($names, $annotationData);
220
        foreach ($processors as $processor) {
221
            $result = $this->callProcessor($processor, $result, $args, $annotationData);
222
        }
223
        $alterers = $this->getAlterResultHooks($names, $annotationData);
224
        foreach ($alterers as $alterer) {
225
            $result = $this->callProcessor($alterer, $result, $args, $annotationData);
226
        }
227
228
        return $result;
229
    }
230
231
    /**
232
     * Call all status determiners, and see if any of them
233
     * know how to convert to a status code.
234
     */
235
    public function determineStatusCode($names, $result)
236
    {
237
        // If the result (post-processing) is an object that
238
        // implements ExitCodeInterface, then we will ask it
239
        // to give us the status code.
240
        if ($result instanceof ExitCodeInterface) {
241
            return $result->getExitCode();
242
        }
243
244
        // If the result does not implement ExitCodeInterface,
245
        // then we'll see if there is a determiner that can
246
        // extract a status code from the result.
247
        $determiners = $this->getStatusDeterminers($names);
248
        foreach ($determiners as $determiner) {
249
            $status = $this->callDeterminer($determiner, $result);
250
            if (isset($status)) {
251
                return $status;
252
            }
253
        }
254
    }
255
256
    /**
257
     * Convert the result object to printable output in
258
     * structured form.
259
     */
260
    public function extractOutput($names, $result)
261
    {
262
        if ($result instanceof OutputDataInterface) {
263
            return $result->getOutputData();
264
        }
265
266
        $extractors = $this->getOutputExtractors($names);
267
        foreach ($extractors as $extractor) {
268
            $structuredOutput = $this->callExtractor($extractor, $result);
269
            if (isset($structuredOutput)) {
270
                return $structuredOutput;
271
            }
272
        }
273
274
        return $result;
275
    }
276
277
    protected function getInteractors($names, AnnotationData $annotationData)
278
    {
279
        return $this->getHooks($names, self::INTERACT, $annotationData);
280
    }
281
282
    protected function getValidators($names, AnnotationData $annotationData)
283
    {
284
        return $this->getHooks($names, self::ARGUMENT_VALIDATOR, $annotationData);
285
    }
286
287
    protected function getStatusDeterminers($names)
288
    {
289
        return $this->getHooks($names, self::STATUS_DETERMINER);
290
    }
291
292
    protected function getProcessResultHooks($names, AnnotationData $annotationData)
293
    {
294
        return $this->getHooks($names, self::PROCESS_RESULT, $annotationData);
295
    }
296
297
    protected function getAlterResultHooks($names, AnnotationData $annotationData)
298
    {
299
        return $this->getHooks($names, self::ALTER_RESULT, $annotationData);
300
    }
301
302
    protected function getOutputExtractors($names)
303
    {
304
        return $this->getHooks($names, self::EXTRACT_OUTPUT);
305
    }
306
307
    protected function getCommandEvents($names)
308
    {
309
        return $this->getHooks($names, self::COMMAND_EVENT);
310
    }
311
312
    /**
313
     * Get a set of hooks with the provided name(s). Include the
314
     * pre- and post- hooks, and also include the global hooks ('*')
315
     * in addition to the named hooks provided.
316
     *
317
     * @param string|array $names The name of the function being hooked.
318
     * @param string $hook The specific hook name (e.g. alter)
319
     *
320
     * @return callable[]
321
     */
322
    protected function getHooks($names, $hook, $annotationData = null)
323
    {
324
        $names = array_merge(
325
            (array)$names,
326
            ($annotationData == null) ? [] : array_map(function ($item) {
327
                return "@$item";
328
            }, $annotationData->keys())
329
        );
330
        $names[] = '*';
331
        return array_merge(
332
            $this->get($names, "pre-$hook"),
333
            $this->get($names, $hook),
334
            $this->get($names, "post-$hook")
335
        );
336
    }
337
338
    /**
339
     * Get a set of hooks with the provided name(s).
340
     *
341
     * @param string|array $names The name of the function being hooked.
342
     * @param string $hook The specific hook name (e.g. alter)
343
     *
344
     * @return callable[]
345
     */
346
    public function get($names, $hook)
347
    {
348
        $hooks = [];
349
        foreach ((array)$names as $name) {
350
            $hooks = array_merge($hooks, $this->getHook($name, $hook));
351
        }
352
        return $hooks;
353
    }
354
355
    /**
356
     * Get a single named hook.
357
     *
358
     * @param string $name The name of the hooked method
359
     * @param string $hook The specific hook name (e.g. alter)
360
     *
361
     * @return callable[]
362
     */
363
    protected function getHook($name, $hook)
364
    {
365
        if (isset($this->hooks[$name][$hook])) {
366
            return $this->hooks[$name][$hook];
367
        }
368
        return [];
369
    }
370
371
    protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData)
372
    {
373
        if ($interactor instanceof InteractorInterface) {
374
            return $interactor->interact($input, $output, $annotationData);
375
        }
376
        if (is_callable($interactor)) {
377
            return $interactor($input, $output, $annotationData);
378
        }
379
    }
380
381
    protected function callValidator($validator, $args, AnnotationData $annotationData)
382
    {
383
        // TODO: Adding AnnotationData to ValidatorInterface would be
384
        // a breaking change. Either hold off until 2.x, or make
385
        // a new interface containing a method that takes the extra parameter.
386
        if ($validator instanceof ValidatorInterface) {
387
            return $validator->validate($args);
388
        }
389
        if (is_callable($validator)) {
390
            return $validator($args, $annotationData);
391
        }
392
    }
393
394
    protected function callProcessor($processor, $result, $args, AnnotationData $annotationData)
395
    {
396
        $processed = null;
397
        // TODO: Adding AnnotationData to ProcessResultInterface would be
398
        // a breaking change. Either hold off until 2.x, or make
399
        // a new interface containing a method that takes the extra parameter.
400
        if ($processor instanceof ProcessResultInterface) {
401
            $processed = $processor->process($result, $args);
402
        }
403
        if (is_callable($processor)) {
404
            $processed = $processor($result, $args, $annotationData);
405
        }
406
        if (isset($processed)) {
407
            return $processed;
408
        }
409
        return $result;
410
    }
411
412
    protected function callDeterminer($determiner, $result)
413
    {
414
        if ($determiner instanceof StatusDeterminerInterface) {
415
            return $determiner->determineStatusCode($result);
416
        }
417
        if (is_callable($determiner)) {
418
            return $determiner($result);
419
        }
420
    }
421
422
    protected function callExtractor($extractor, $result)
423
    {
424
        if ($extractor instanceof ExtractOutputInterface) {
425
            return $extractor->extractOutput($result);
426
        }
427
        if (is_callable($extractor)) {
428
            return $extractor($result);
429
        }
430
    }
431
432
    /**
433
     * @param ConsoleCommandEvent $event
434
     */
435
    public function callCommandEventHooks(ConsoleCommandEvent $event)
436
    {
437
        /* @var Command $command */
438
        $command = $event->getCommand();
439
        $names = [$command->getName()];
440
        $commandEventHooks = $this->getCommandEvents($names);
441
        foreach ($commandEventHooks as $commandEvent) {
442
            if (is_callable($commandEvent)) {
443
                $commandEvent($event);
444
            }
445
        }
446
    }
447
448
    /**
449
     * @{@inheritdoc}
450
     */
451
    public static function getSubscribedEvents()
452
    {
453
        return [ConsoleEvents::COMMAND => 'callCommandEventHooks'];
454
    }
455
}
456