Completed
Push — master ( e508dc...446937 )
by Greg
02:29
created

HookManager::getHookOptions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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