Completed
Pull Request — master (#42)
by Greg
02:53
created

HookManager::interact()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
441
        }
442
        if (is_callable($provider)) {
443
            return $provider($input, $annotationData);
444
        }
445
    }
446
447
    protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData)
448
    {
449
        if ($interactor instanceof InteractorInterface) {
450
            return $interactor->interact($input, $output, $annotationData);
451
        }
452
        if (is_callable($interactor)) {
453
            return $interactor($input, $output, $annotationData);
454
        }
455
    }
456
457
    protected function callValidator($validator, $args, AnnotationData $annotationData)
458
    {
459
        // TODO: Adding AnnotationData to ValidatorInterface would be
460
        // a breaking change. Either hold off until 2.x, or make
461
        // a new interface containing a method that takes the extra parameter.
462
        if ($validator instanceof ValidatorInterface) {
463
            return $validator->validate($args, $annotationData);
464
        }
465
        if (is_callable($validator)) {
466
            return $validator($args, $annotationData);
467
        }
468
    }
469
470
    protected function callProcessor($processor, $result, $args, AnnotationData $annotationData)
471
    {
472
        $processed = null;
473
        // TODO: Adding AnnotationData to ProcessResultInterface would be
474
        // a breaking change. Either hold off until 2.x, or make
475
        // a new interface containing a method that takes the extra parameter.
476
        if ($processor instanceof ProcessResultInterface) {
477
            $processed = $processor->process($result, $args, $annotationData);
478
        }
479
        if (is_callable($processor)) {
480
            $processed = $processor($result, $args, $annotationData);
481
        }
482
        if (isset($processed)) {
483
            return $processed;
484
        }
485
        return $result;
486
    }
487
488
    protected function callDeterminer($determiner, $result)
489
    {
490
        if ($determiner instanceof StatusDeterminerInterface) {
491
            return $determiner->determineStatusCode($result);
492
        }
493
        if (is_callable($determiner)) {
494
            return $determiner($result);
495
        }
496
    }
497
498
    protected function callExtractor($extractor, $result)
499
    {
500
        if ($extractor instanceof ExtractOutputInterface) {
501
            return $extractor->extractOutput($result);
502
        }
503
        if (is_callable($extractor)) {
504
            return $extractor($result);
505
        }
506
    }
507
508
    /**
509
     * @param ConsoleCommandEvent $event
510
     */
511
    public function callCommandEventHooks(ConsoleCommandEvent $event)
512
    {
513
        /* @var Command $command */
514
        $command = $event->getCommand();
515
        $names = [$command->getName()];
516
        $commandEventHooks = $this->getCommandEventHooks($names);
517
        foreach ($commandEventHooks as $commandEvent) {
518
            if (is_callable($commandEvent)) {
519
                $commandEvent($event);
520
            }
521
        }
522
    }
523
524
    /**
525
     * @{@inheritdoc}
526
     */
527
    public static function getSubscribedEvents()
528
    {
529
        return [ConsoleEvents::COMMAND => 'callCommandEventHooks'];
530
    }
531
}
532