Completed
Push — master ( cb169f...e72b8e )
by Greg
02:15
created

HookManager::addWildcardHooksToNames()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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