Completed
Pull Request — master (#75)
by Greg
02:13
created

HookManager::callInitializeHook()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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