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

HookManager::addPostCommandHook()   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
use Consolidation\AnnotatedCommand\CommandData;
16
17
/**
18
 * Manage named callback hooks
19
 */
20
class HookManager implements EventSubscriberInterface
21
{
22
    protected $hooks = [];
23
    /** var CommandInfo[] */
24
    protected $hookOptions = [];
25
26
    const PRE_COMMAND_EVENT = 'pre-command-event';
27
    const COMMAND_EVENT = 'command-event';
28
    const POST_COMMAND_EVENT = 'post-command-event';
29
    const PRE_OPTION_HOOK = 'pre-option';
30
    const OPTION_HOOK = 'option';
31
    const POST_OPTION_HOOK = 'post-option';
32
    const PRE_INITIALIZE = 'pre-init';
33
    const INITIALIZE = 'init';
34
    const POST_INITIALIZE = 'post-init';
35
    const PRE_INTERACT = 'pre-interact';
36
    const INTERACT = 'interact';
37
    const POST_INTERACT = 'post-interact';
38
    const PRE_ARGUMENT_VALIDATOR = 'pre-validate';
39
    const ARGUMENT_VALIDATOR = 'validate';
40
    const POST_ARGUMENT_VALIDATOR = 'post-validate';
41
    const PRE_COMMAND_HOOK = 'pre-command';
42
    const COMMAND_HOOK = 'command';
43
    const POST_COMMAND_HOOK = 'post-command';
44
    const PRE_PROCESS_RESULT = 'pre-process';
45
    const PROCESS_RESULT = 'process';
46
    const POST_PROCESS_RESULT = 'post-process';
47
    const PRE_ALTER_RESULT = 'pre-alter';
48
    const ALTER_RESULT = 'alter';
49
    const POST_ALTER_RESULT = 'post-alter';
50
    const STATUS_DETERMINER = 'status';
51
    const EXTRACT_OUTPUT = 'extract';
52
53
    public function __construct()
54
    {
55
    }
56
57
    public function getAllHooks()
58
    {
59
        return $this->hooks;
60
    }
61
62
    /**
63
     * Add a hook
64
     *
65
     * @param mixed $callback The callback function to call
66
     * @param string   $hook     The name of the hook to add
67
     * @param string   $name     The name of the command to hook
68
     *   ('*' for all)
69
     */
70
    public function add(callable $callback, $hook, $name = '*')
71
    {
72
        if (empty($name)) {
73
            $name = static::getClassNameFromCallback($callback);
74
        }
75
        $this->hooks[$name][$hook][] = $callback;
76
    }
77
78
    public function recordHookOptions($commandInfo, $name)
79
    {
80
        $this->hookOptions[$name][] = $commandInfo;
81
    }
82
83
    public static function getNames($command, $callback)
84
    {
85
        return array_filter(
86
            array_merge(
87
                static::getNamesUsingCommands($command),
88
                [static::getClassNameFromCallback($callback)]
89
            )
90
        );
91
    }
92
93
    protected static function getNamesUsingCommands($command)
94
    {
95
        return array_merge(
96
            [$command->getName()],
97
            $command->getAliases()
98
        );
99
    }
100
101
    /**
102
     * If a command hook does not specify any particular command
103
     * name that it should be attached to, then it will be applied
104
     * to every command that is defined in the same class as the hook.
105
     * This is controlled by using the namespace + class name of
106
     * the implementing class of the callback hook.
107
     */
108
    protected static function getClassNameFromCallback($callback)
109
    {
110
        if (!is_array($callback)) {
111
            return '';
112
        }
113
        $reflectionClass = new \ReflectionClass($callback[0]);
114
        return $reflectionClass->getName();
115
    }
116
117
    /**
118
     * Add an configuration provider hook
119
     *
120
     * @param type InitializeHookInterface $provider
121
     * @param type $name The name of the command to hook
122
     *   ('*' for all)
123
     */
124
    public function addInitializeHook(InitializeHookInterface $initializeHook, $name = '*')
125
    {
126
        $this->hooks[$name][self::INITIALIZE][] = $initializeHook;
127
    }
128
129
    /**
130
     * Add an option hook
131
     *
132
     * @param type ValidatorInterface $validator
133
     * @param type $name The name of the command to hook
134
     *   ('*' for all)
135
     */
136
    public function addOptionHook(OptionHookInterface $interactor, $name = '*')
137
    {
138
        $this->hooks[$name][self::INTERACT][] = $interactor;
139
    }
140
141
    /**
142
     * Add an interact hook
143
     *
144
     * @param type ValidatorInterface $validator
145
     * @param type $name The name of the command to hook
146
     *   ('*' for all)
147
     */
148
    public function addInteractor(InteractorInterface $interactor, $name = '*')
149
    {
150
        $this->hooks[$name][self::INTERACT][] = $interactor;
151
    }
152
153
    /**
154
     * Add a pre-validator hook
155
     *
156
     * @param type ValidatorInterface $validator
157
     * @param type $name The name of the command to hook
158
     *   ('*' for all)
159
     */
160
    public function addPreValidator(ValidatorInterface $validator, $name = '*')
161
    {
162
        $this->hooks[$name][self::PRE_ARGUMENT_VALIDATOR][] = $validator;
163
    }
164
165
    /**
166
     * Add a validator hook
167
     *
168
     * @param type ValidatorInterface $validator
169
     * @param type $name The name of the command to hook
170
     *   ('*' for all)
171
     */
172
    public function addValidator(ValidatorInterface $validator, $name = '*')
173
    {
174
        $this->hooks[$name][self::ARGUMENT_VALIDATOR][] = $validator;
175
    }
176
177
    /**
178
     * Add a pre-command hook.  This is the same as a validator hook, except
179
     * that it will run after all of the post-validator hooks.
180
     *
181
     * @param type ValidatorInterface $preCommand
182
     * @param type $name The name of the command to hook
183
     *   ('*' for all)
184
     */
185
    public function addPreCommandHook(ValidatorInterface $preCommand, $name = '*')
186
    {
187
        $this->hooks[$name][self::PRE_COMMAND_HOOK][] = $preCommand;
188
    }
189
190
    /**
191
     * Add a post-command hook.  This is the same as a pre-process hook,
192
     * except that it will run before the first pre-process hook.
193
     *
194
     * @param type ProcessResultInterface $postCommand
195
     * @param type $name The name of the command to hook
196
     *   ('*' for all)
197
     */
198
    public function addPostCommandHook(ProcessResultInterface $postCommand, $name = '*')
199
    {
200
        $this->hooks[$name][self::POST_COMMAND_HOOK][] = $postCommand;
201
    }
202
203
    /**
204
     * Add a result processor.
205
     *
206
     * @param type ProcessResultInterface $resultProcessor
207
     * @param type $name The name of the command to hook
208
     *   ('*' for all)
209
     */
210
    public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*')
211
    {
212
        $this->hooks[$name][self::PROCESS_RESULT][] = $resultProcessor;
213
    }
214
215
    /**
216
     * Add a result alterer. After a result is processed
217
     * by a result processor, an alter hook may be used
218
     * to convert the result from one form to another.
219
     *
220
     * @param type AlterResultInterface $resultAlterer
221
     * @param type $name The name of the command to hook
222
     *   ('*' for all)
223
     */
224
    public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*')
225
    {
226
        $this->hooks[$name][self::ALTER_RESULT][] = $resultAlterer;
227
    }
228
229
    /**
230
     * Add a status determiner. Usually, a command should return
231
     * an integer on error, or a result object on success (which
232
     * implies a status code of zero). If a result contains the
233
     * status code in some other field, then a status determiner
234
     * can be used to call the appropriate accessor method to
235
     * determine the status code.  This is usually not necessary,
236
     * though; a command that fails may return a CommandError
237
     * object, which contains a status code and a result message
238
     * to display.
239
     * @see CommandError::getExitCode()
240
     *
241
     * @param type StatusDeterminerInterface $statusDeterminer
242
     * @param type $name The name of the command to hook
243
     *   ('*' for all)
244
     */
245
    public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*')
246
    {
247
        $this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer;
248
    }
249
250
    /**
251
     * Add an output extractor. If a command returns an object
252
     * object, by default it is passed directly to the output
253
     * formatter (if in use) for rendering. If the result object
254
     * contains more information than just the data to render, though,
255
     * then an output extractor can be used to call the appopriate
256
     * accessor method of the result object to get the data to
257
     * rendered.  This is usually not necessary, though; it is preferable
258
     * to have complex result objects implement the OutputDataInterface.
259
     * @see OutputDataInterface::getOutputData()
260
     *
261
     * @param type ExtractOutputInterface $outputExtractor
262
     * @param type $name The name of the command to hook
263
     *   ('*' for all)
264
     */
265
    public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*')
266
    {
267
        $this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor;
268
    }
269
270
    public function initializeHook(
271
        InputInterface $input,
272
        $names,
273
        AnnotationData $annotationData
274
    ) {
275
        $providers = $this->getInitializeHooks($names, $annotationData);
276
        foreach ($providers as $provider) {
277
            $this->callInjectConfigurationHook($provider, $input, $annotationData);
278
        }
279
    }
280
281
    public function optionsHook(
282
        \Consolidation\AnnotatedCommand\AnnotatedCommand $command,
283
        $names,
284
        AnnotationData $annotationData
285
    ) {
286
        $optionHooks = $this->getOptionHooks($names, $annotationData);
287
        foreach ($optionHooks as $optionHook) {
288
            $this->callOptionHook($optionHook, $command, $annotationData);
289
        }
290
        $commandInfoList = $this->getHookOptionsForCommand($command);
291
        $command->optionsHookForHookAnnotations($commandInfoList);
292
    }
293
294
    public function getHookOptionsForCommand($command)
295
    {
296
        $names = $this->addWildcardHooksToNames($command->getNames(), $command->getAnnotationData());
297
        return $this->getHookOptions($names);
298
    }
299
300
    /**
301
     * @return CommandInfo[]
302
     */
303
    public function getHookOptions($names)
304
    {
305
        $result = [];
306
        foreach ($names as $name) {
307
            if (isset($this->hookOptions[$name])) {
308
                $result = array_merge($result, $this->hookOptions[$name]);
309
            }
310
        }
311
        return $result;
312
    }
313
314
    public function interact(
315
        InputInterface $input,
316
        OutputInterface $output,
317
        $names,
318
        AnnotationData $annotationData
319
    ) {
320
        $interactors = $this->getInteractors($names, $annotationData);
321
        foreach ($interactors as $interactor) {
322
            $this->callInteractor($interactor, $input, $output, $annotationData);
323
        }
324
    }
325
326
    public function validateArguments($names, CommandData $commandData)
327
    {
328
        $validators = $this->getValidators($names, $commandData->annotationData());
329
        foreach ($validators as $validator) {
330
            $validated = $this->callValidator($validator, $commandData);
331
            if (is_object($validated)) {
332
                return $validated;
333
            }
334
        }
335
    }
336
337
    /**
338
     * Process result and decide what to do with it.
339
     * Allow client to add transformation / interpretation
340
     * callbacks.
341
     */
342
    public function alterResult($names, $result, CommandData $commandData)
343
    {
344
        $processors = $this->getProcessResultHooks($names, $commandData->annotationData());
345
        foreach ($processors as $processor) {
346
            $result = $this->callProcessor($processor, $result, $commandData);
347
        }
348
        $alterers = $this->getAlterResultHooks($names, $commandData->annotationData());
349
        foreach ($alterers as $alterer) {
350
            $result = $this->callProcessor($alterer, $result, $commandData);
351
        }
352
353
        return $result;
354
    }
355
356
    /**
357
     * Call all status determiners, and see if any of them
358
     * know how to convert to a status code.
359
     */
360
    public function determineStatusCode($names, $result)
361
    {
362
        // If the result (post-processing) is an object that
363
        // implements ExitCodeInterface, then we will ask it
364
        // to give us the status code.
365
        if ($result instanceof ExitCodeInterface) {
366
            return $result->getExitCode();
367
        }
368
369
        // If the result does not implement ExitCodeInterface,
370
        // then we'll see if there is a determiner that can
371
        // extract a status code from the result.
372
        $determiners = $this->getStatusDeterminers($names);
373
        foreach ($determiners as $determiner) {
374
            $status = $this->callDeterminer($determiner, $result);
375
            if (isset($status)) {
376
                return $status;
377
            }
378
        }
379
    }
380
381
    /**
382
     * Convert the result object to printable output in
383
     * structured form.
384
     */
385
    public function extractOutput($names, $result)
386
    {
387
        if ($result instanceof OutputDataInterface) {
388
            return $result->getOutputData();
389
        }
390
391
        $extractors = $this->getOutputExtractors($names);
392
        foreach ($extractors as $extractor) {
393
            $structuredOutput = $this->callExtractor($extractor, $result);
394
            if (isset($structuredOutput)) {
395
                return $structuredOutput;
396
            }
397
        }
398
399
        return $result;
400
    }
401
402
    protected function getCommandEventHooks($names)
403
    {
404
        return $this->getHooks(
405
            $names,
406
            [
407
                self::PRE_COMMAND_EVENT,
408
                self::COMMAND_EVENT,
409
                self::POST_COMMAND_EVENT
410
            ]
411
        );
412
    }
413
414
    protected function getInitializeHooks($names, AnnotationData $annotationData)
415
    {
416
        return $this->getHooks(
417
            $names,
418
            [
419
                self::PRE_INITIALIZE,
420
                self::INITIALIZE,
421
                self::POST_INITIALIZE
422
            ],
423
            $annotationData
424
        );
425
    }
426
427
    protected function getOptionHooks($names, AnnotationData $annotationData)
428
    {
429
        return $this->getHooks(
430
            $names,
431
            [
432
                self::PRE_OPTION_HOOK,
433
                self::OPTION_HOOK,
434
                self::POST_OPTION_HOOK
435
            ],
436
            $annotationData
437
        );
438
    }
439
440
    protected function getInteractors($names, AnnotationData $annotationData)
441
    {
442
        return $this->getHooks(
443
            $names,
444
            [
445
                self::PRE_INTERACT,
446
                self::INTERACT,
447
                self::POST_INTERACT
448
            ],
449
            $annotationData
450
        );
451
    }
452
453 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...
454
    {
455
        return $this->getHooks(
456
            $names,
457
            [
458
                self::PRE_ARGUMENT_VALIDATOR,
459
                self::ARGUMENT_VALIDATOR,
460
                self::POST_ARGUMENT_VALIDATOR,
461
                self::PRE_COMMAND_HOOK,
462
                self::COMMAND_HOOK,
463
            ],
464
            $annotationData
465
        );
466
    }
467
468
    protected function getProcessResultHooks($names, AnnotationData $annotationData)
469
    {
470
        return $this->getHooks(
471
            $names,
472
            [
473
                self::PRE_PROCESS_RESULT,
474
                self::PROCESS_RESULT,
475
                self::POST_PROCESS_RESULT
476
            ],
477
            $annotationData
478
        );
479
    }
480
481 View Code Duplication
    protected function getAlterResultHooks($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...
482
    {
483
        return $this->getHooks(
484
            $names,
485
            [
486
                self::PRE_ALTER_RESULT,
487
                self::ALTER_RESULT,
488
                self::POST_ALTER_RESULT,
489
                self::POST_COMMAND_HOOK,
490
            ],
491
            $annotationData
492
        );
493
    }
494
495
    protected function getStatusDeterminers($names)
496
    {
497
        return $this->getHooks(
498
            $names,
499
            [
500
                self::STATUS_DETERMINER,
501
            ]
502
        );
503
    }
504
505
    protected function getOutputExtractors($names)
506
    {
507
        return $this->getHooks(
508
            $names,
509
            [
510
                self::EXTRACT_OUTPUT,
511
            ]
512
        );
513
    }
514
515
    /**
516
     * Get a set of hooks with the provided name(s). Include the
517
     * pre- and post- hooks, and also include the global hooks ('*')
518
     * in addition to the named hooks provided.
519
     *
520
     * @param string|array $names The name of the function being hooked.
521
     * @param string[] $hooks A list of hooks (e.g. [HookManager::ALTER_RESULT])
522
     *
523
     * @return callable[]
524
     */
525
    public function getHooks($names, $hooks, $annotationData = null)
526
    {
527
        return $this->get($this->addWildcardHooksToNames($names, $annotationData), $hooks);
528
    }
529
530
    protected function addWildcardHooksToNames($names, $annotationData = null)
531
    {
532
        $names = array_merge(
533
            (array)$names,
534
            ($annotationData == null) ? [] : array_map(function ($item) {
535
                return "@$item";
536
            }, $annotationData->keys())
537
        );
538
        $names[] = '*';
539
        return $names;
540
    }
541
542
    /**
543
     * Get a set of hooks with the provided name(s).
544
     *
545
     * @param string|array $names The name of the function being hooked.
546
     * @param string[] $hooks The list of hook names (e.g. [HookManager::ALTER_RESULT])
547
     *
548
     * @return callable[]
549
     */
550
    public function get($names, $hooks)
551
    {
552
        $result = [];
553
        foreach ((array)$hooks as $hook) {
554
            foreach ((array)$names as $name) {
555
                $result = array_merge($result, $this->getHook($name, $hook));
556
            }
557
        }
558
        return $result;
559
    }
560
561
    /**
562
     * Get a single named hook.
563
     *
564
     * @param string $name The name of the hooked method
565
     * @param string $hook The specific hook name (e.g. alter)
566
     *
567
     * @return callable[]
568
     */
569
    protected function getHook($name, $hook)
570
    {
571
        if (isset($this->hooks[$name][$hook])) {
572
            return $this->hooks[$name][$hook];
573
        }
574
        return [];
575
    }
576
577
    protected function callInjectConfigurationHook($provider, $input, AnnotationData $annotationData)
578
    {
579
        if ($provider instanceof InitializeHookInterface) {
580
            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...
581
        }
582
        if (is_callable($provider)) {
583
            return $provider($input, $annotationData);
584
        }
585
    }
586
587
    protected function callOptionHook($optionHook, $command, AnnotationData $annotationData)
588
    {
589
        if ($optionHook instanceof OptionHookInterface) {
590
            return $optionHook->getOptions($command, $annotationData);
591
        }
592
        if (is_callable($optionHook)) {
593
            return $optionHook($command, $annotationData);
594
        }
595
    }
596
597
    protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData)
598
    {
599
        if ($interactor instanceof InteractorInterface) {
600
            return $interactor->interact($input, $output, $annotationData);
601
        }
602
        if (is_callable($interactor)) {
603
            return $interactor($input, $output, $annotationData);
604
        }
605
    }
606
607
    protected function callValidator($validator, CommandData $commandData)
608
    {
609
        // TODO: Adding AnnotationData to ValidatorInterface would be
610
        // a breaking change. Either hold off until 2.x, or make
611
        // a new interface containing a method that takes the extra parameter.
612
        if ($validator instanceof ValidatorInterface) {
613
            return $validator->validate($commandData);
614
        }
615
        if (is_callable($validator)) {
616
            return $validator($commandData);
617
        }
618
    }
619
620
    protected function callProcessor($processor, $result, CommandData $commandData)
621
    {
622
        $processed = null;
623
        // TODO: Adding AnnotationData to ProcessResultInterface would be
624
        // a breaking change. Either hold off until 2.x, or make
625
        // a new interface containing a method that takes the extra parameter.
626
        if ($processor instanceof ProcessResultInterface) {
627
            $processed = $processor->process($result, $commandData);
628
        }
629
        if (is_callable($processor)) {
630
            $processed = $processor($result, $commandData);
631
        }
632
        if (isset($processed)) {
633
            return $processed;
634
        }
635
        return $result;
636
    }
637
638
    protected function callDeterminer($determiner, $result)
639
    {
640
        if ($determiner instanceof StatusDeterminerInterface) {
641
            return $determiner->determineStatusCode($result);
642
        }
643
        if (is_callable($determiner)) {
644
            return $determiner($result);
645
        }
646
    }
647
648
    protected function callExtractor($extractor, $result)
649
    {
650
        if ($extractor instanceof ExtractOutputInterface) {
651
            return $extractor->extractOutput($result);
652
        }
653
        if (is_callable($extractor)) {
654
            return $extractor($result);
655
        }
656
    }
657
658
    /**
659
     * @param ConsoleCommandEvent $event
660
     */
661
    public function callCommandEventHooks(ConsoleCommandEvent $event)
662
    {
663
        /* @var Command $command */
664
        $command = $event->getCommand();
665
        $names = [$command->getName()];
666
        $commandEventHooks = $this->getCommandEventHooks($names);
667
        foreach ($commandEventHooks as $commandEvent) {
668
            if (is_callable($commandEvent)) {
669
                $commandEvent($event);
670
            }
671
        }
672
    }
673
674
    public function findAndAddHookOptions($command)
675
    {
676
        if (!$command instanceof \Consolidation\AnnotatedCommand\AnnotatedCommand) {
677
            return;
678
        }
679
        $command->optionsHook();
680
    }
681
682
    /**
683
     * @{@inheritdoc}
684
     */
685
    public static function getSubscribedEvents()
686
    {
687
        return [ConsoleEvents::COMMAND => 'callCommandEventHooks'];
688
    }
689
}
690