Completed
Push — master ( 5d504e...a1b800 )
by Greg
01:38
created

HookManager   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 411
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 4
dl 0
loc 411
rs 8.2857
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A getAllHooks() 0 4 1
A add() 0 8 2
A recordHookOptions() 0 5 1
A getNames() 0 9 1
A getNamesUsingCommands() 0 7 1
A getClassNameFromCallback() 0 8 2
A addReplaceCommandHook() 0 5 1
A addCommandEvent() 0 7 1
A addInitializeHook() 0 5 1
A addOptionHook() 0 5 1
A addInteractor() 0 5 1
A addPreValidator() 0 5 1
A addValidator() 0 5 1
A addPreCommandHook() 0 5 1
A addPostCommandHook() 0 5 1
A addResultProcessor() 0 5 1
A addAlterResult() 0 5 1
A addStatusDeterminer() 0 5 1
A addOutputExtractor() 0 5 1
A getHookOptionsForCommand() 0 5 1
A getHookOptions() 0 10 3
A getHooks() 0 4 1
A addWildcardHooksToNames() 0 11 2
A get() 0 10 3
A getHook() 0 7 2
A callCommandEventHooks() 0 7 1
A getSubscribedEvents() 0 4 1
A addPreCommandEventDispatcher() 0 5 1
A addCommandEventDispatcher() 0 5 1
A addPostCommandEventDispatcher() 0 5 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
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
12
use Symfony\Component\EventDispatcher\EventDispatcher;
13
14
use Consolidation\AnnotatedCommand\ExitCodeInterface;
15
use Consolidation\AnnotatedCommand\OutputDataInterface;
16
use Consolidation\AnnotatedCommand\AnnotationData;
17
use Consolidation\AnnotatedCommand\CommandData;
18
use Consolidation\AnnotatedCommand\CommandError;
19
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\CommandEventHookDispatcher;
20
21
/**
22
 * Manage named callback hooks
23
 */
24
class HookManager implements EventSubscriberInterface
25
{
26
    protected $hooks = [];
27
    /** var CommandInfo[] */
28
    protected $hookOptions = [];
29
30
    const REPLACE_COMMAND_HOOK = 'replace-command';
31
    const PRE_COMMAND_EVENT = 'pre-command-event';
32
    const COMMAND_EVENT = 'command-event';
33
    const POST_COMMAND_EVENT = 'post-command-event';
34
    const PRE_OPTION_HOOK = 'pre-option';
35
    const OPTION_HOOK = 'option';
36
    const POST_OPTION_HOOK = 'post-option';
37
    const PRE_INITIALIZE = 'pre-init';
38
    const INITIALIZE = 'init';
39
    const POST_INITIALIZE = 'post-init';
40
    const PRE_INTERACT = 'pre-interact';
41
    const INTERACT = 'interact';
42
    const POST_INTERACT = 'post-interact';
43
    const PRE_ARGUMENT_VALIDATOR = 'pre-validate';
44
    const ARGUMENT_VALIDATOR = 'validate';
45
    const POST_ARGUMENT_VALIDATOR = 'post-validate';
46
    const PRE_COMMAND_HOOK = 'pre-command';
47
    const COMMAND_HOOK = 'command';
48
    const POST_COMMAND_HOOK = 'post-command';
49
    const PRE_PROCESS_RESULT = 'pre-process';
50
    const PROCESS_RESULT = 'process';
51
    const POST_PROCESS_RESULT = 'post-process';
52
    const PRE_ALTER_RESULT = 'pre-alter';
53
    const ALTER_RESULT = 'alter';
54
    const POST_ALTER_RESULT = 'post-alter';
55
    const STATUS_DETERMINER = 'status';
56
    const EXTRACT_OUTPUT = 'extract';
57
    const ON_EVENT = 'on-event';
58
59
    public function __construct()
60
    {
61
    }
62
63
    public function getAllHooks()
64
    {
65
        return $this->hooks;
66
    }
67
68
    /**
69
     * Add a hook
70
     *
71
     * @param mixed $callback The callback function to call
72
     * @param string   $hook     The name of the hook to add
73
     * @param string   $name     The name of the command to hook
74
     *   ('*' for all)
75
     */
76
    public function add(callable $callback, $hook, $name = '*')
77
    {
78
        if (empty($name)) {
79
            $name = static::getClassNameFromCallback($callback);
80
        }
81
        $this->hooks[$name][$hook][] = $callback;
82
        return $this;
83
    }
84
85
    public function recordHookOptions($commandInfo, $name)
86
    {
87
        $this->hookOptions[$name][] = $commandInfo;
88
        return $this;
89
    }
90
91
    public static function getNames($command, $callback)
92
    {
93
        return array_filter(
94
            array_merge(
95
                static::getNamesUsingCommands($command),
96
                [static::getClassNameFromCallback($callback)]
97
            )
98
        );
99
    }
100
101
    protected static function getNamesUsingCommands($command)
102
    {
103
        return array_merge(
104
            [$command->getName()],
105
            $command->getAliases()
106
        );
107
    }
108
109
    /**
110
     * If a command hook does not specify any particular command
111
     * name that it should be attached to, then it will be applied
112
     * to every command that is defined in the same class as the hook.
113
     * This is controlled by using the namespace + class name of
114
     * the implementing class of the callback hook.
115
     */
116
    protected static function getClassNameFromCallback($callback)
117
    {
118
        if (!is_array($callback)) {
119
            return '';
120
        }
121
        $reflectionClass = new \ReflectionClass($callback[0]);
122
        return $reflectionClass->getName();
123
    }
124
125
    /**
126
     * Add a replace command hook
127
     *
128
     * @param type ReplaceCommandHookInterface $provider
129
     * @param type string $command_name The name of the command to replace
130
     */
131
    public function addReplaceCommandHook(ReplaceCommandHookInterface $replaceCommandHook, $name)
132
    {
133
        $this->hooks[$name][self::REPLACE_COMMAND_HOOK][] = $replaceCommandHook;
134
        return $this;
135
    }
136
137
    public function addPreCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*')
138
    {
139
        $this->hooks[$name][self::PRE_COMMAND_EVENT][] = $eventDispatcher;
140
        return $this;
141
    }
142
143
    public function addCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*')
144
    {
145
        $this->hooks[$name][self::COMMAND_EVENT][] = $eventDispatcher;
146
        return $this;
147
    }
148
149
    public function addPostCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*')
150
    {
151
        $this->hooks[$name][self::POST_COMMAND_EVENT][] = $eventDispatcher;
152
        return $this;
153
    }
154
155
    public function addCommandEvent(EventSubscriberInterface $eventSubscriber)
156
    {
157
        // Wrap the event subscriber in a dispatcher and add it
158
        $dispatcher = new EventDispatcher();
159
        $dispatcher->addSubscriber($eventSubscriber);
160
        return $this->addCommandEventDispatcher($dispatcher);
161
    }
162
163
    /**
164
     * Add an configuration provider hook
165
     *
166
     * @param type InitializeHookInterface $provider
167
     * @param type $name The name of the command to hook
168
     *   ('*' for all)
169
     */
170
    public function addInitializeHook(InitializeHookInterface $initializeHook, $name = '*')
171
    {
172
        $this->hooks[$name][self::INITIALIZE][] = $initializeHook;
173
        return $this;
174
    }
175
176
    /**
177
     * Add an option hook
178
     *
179
     * @param type ValidatorInterface $validator
180
     * @param type $name The name of the command to hook
181
     *   ('*' for all)
182
     */
183
    public function addOptionHook(OptionHookInterface $interactor, $name = '*')
184
    {
185
        $this->hooks[$name][self::INTERACT][] = $interactor;
186
        return $this;
187
    }
188
189
    /**
190
     * Add an interact hook
191
     *
192
     * @param type ValidatorInterface $validator
193
     * @param type $name The name of the command to hook
194
     *   ('*' for all)
195
     */
196
    public function addInteractor(InteractorInterface $interactor, $name = '*')
197
    {
198
        $this->hooks[$name][self::INTERACT][] = $interactor;
199
        return $this;
200
    }
201
202
    /**
203
     * Add a pre-validator hook
204
     *
205
     * @param type ValidatorInterface $validator
206
     * @param type $name The name of the command to hook
207
     *   ('*' for all)
208
     */
209
    public function addPreValidator(ValidatorInterface $validator, $name = '*')
210
    {
211
        $this->hooks[$name][self::PRE_ARGUMENT_VALIDATOR][] = $validator;
212
        return $this;
213
    }
214
215
    /**
216
     * Add a validator hook
217
     *
218
     * @param type ValidatorInterface $validator
219
     * @param type $name The name of the command to hook
220
     *   ('*' for all)
221
     */
222
    public function addValidator(ValidatorInterface $validator, $name = '*')
223
    {
224
        $this->hooks[$name][self::ARGUMENT_VALIDATOR][] = $validator;
225
        return $this;
226
    }
227
228
    /**
229
     * Add a pre-command hook.  This is the same as a validator hook, except
230
     * that it will run after all of the post-validator hooks.
231
     *
232
     * @param type ValidatorInterface $preCommand
233
     * @param type $name The name of the command to hook
234
     *   ('*' for all)
235
     */
236
    public function addPreCommandHook(ValidatorInterface $preCommand, $name = '*')
237
    {
238
        $this->hooks[$name][self::PRE_COMMAND_HOOK][] = $preCommand;
239
        return $this;
240
    }
241
242
    /**
243
     * Add a post-command hook.  This is the same as a pre-process hook,
244
     * except that it will run before the first pre-process hook.
245
     *
246
     * @param type ProcessResultInterface $postCommand
247
     * @param type $name The name of the command to hook
248
     *   ('*' for all)
249
     */
250
    public function addPostCommandHook(ProcessResultInterface $postCommand, $name = '*')
251
    {
252
        $this->hooks[$name][self::POST_COMMAND_HOOK][] = $postCommand;
253
        return $this;
254
    }
255
256
    /**
257
     * Add a result processor.
258
     *
259
     * @param type ProcessResultInterface $resultProcessor
260
     * @param type $name The name of the command to hook
261
     *   ('*' for all)
262
     */
263
    public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*')
264
    {
265
        $this->hooks[$name][self::PROCESS_RESULT][] = $resultProcessor;
266
        return $this;
267
    }
268
269
    /**
270
     * Add a result alterer. After a result is processed
271
     * by a result processor, an alter hook may be used
272
     * to convert the result from one form to another.
273
     *
274
     * @param type AlterResultInterface $resultAlterer
275
     * @param type $name The name of the command to hook
276
     *   ('*' for all)
277
     */
278
    public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*')
279
    {
280
        $this->hooks[$name][self::ALTER_RESULT][] = $resultAlterer;
281
        return $this;
282
    }
283
284
    /**
285
     * Add a status determiner. Usually, a command should return
286
     * an integer on error, or a result object on success (which
287
     * implies a status code of zero). If a result contains the
288
     * status code in some other field, then a status determiner
289
     * can be used to call the appropriate accessor method to
290
     * determine the status code.  This is usually not necessary,
291
     * though; a command that fails may return a CommandError
292
     * object, which contains a status code and a result message
293
     * to display.
294
     * @see CommandError::getExitCode()
295
     *
296
     * @param type StatusDeterminerInterface $statusDeterminer
297
     * @param type $name The name of the command to hook
298
     *   ('*' for all)
299
     */
300
    public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*')
301
    {
302
        $this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer;
303
        return $this;
304
    }
305
306
    /**
307
     * Add an output extractor. If a command returns an object
308
     * object, by default it is passed directly to the output
309
     * formatter (if in use) for rendering. If the result object
310
     * contains more information than just the data to render, though,
311
     * then an output extractor can be used to call the appopriate
312
     * accessor method of the result object to get the data to
313
     * rendered.  This is usually not necessary, though; it is preferable
314
     * to have complex result objects implement the OutputDataInterface.
315
     * @see OutputDataInterface::getOutputData()
316
     *
317
     * @param type ExtractOutputInterface $outputExtractor
318
     * @param type $name The name of the command to hook
319
     *   ('*' for all)
320
     */
321
    public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*')
322
    {
323
        $this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor;
324
        return $this;
325
    }
326
327
    public function getHookOptionsForCommand($command)
328
    {
329
        $names = $this->addWildcardHooksToNames($command->getNames(), $command->getAnnotationData());
330
        return $this->getHookOptions($names);
331
    }
332
333
    /**
334
     * @return CommandInfo[]
335
     */
336
    public function getHookOptions($names)
337
    {
338
        $result = [];
339
        foreach ($names as $name) {
340
            if (isset($this->hookOptions[$name])) {
341
                $result = array_merge($result, $this->hookOptions[$name]);
342
            }
343
        }
344
        return $result;
345
    }
346
347
    /**
348
     * Get a set of hooks with the provided name(s). Include the
349
     * pre- and post- hooks, and also include the global hooks ('*')
350
     * in addition to the named hooks provided.
351
     *
352
     * @param string|array $names The name of the function being hooked.
353
     * @param string[] $hooks A list of hooks (e.g. [HookManager::ALTER_RESULT])
354
     *
355
     * @return callable[]
356
     */
357
    public function getHooks($names, $hooks, $annotationData = null)
358
    {
359
        return $this->get($this->addWildcardHooksToNames($names, $annotationData), $hooks);
360
    }
361
362
    protected function addWildcardHooksToNames($names, $annotationData = null)
363
    {
364
        $names = array_merge(
365
            (array)$names,
366
            ($annotationData == null) ? [] : array_map(function ($item) {
367
                return "@$item";
368
            }, $annotationData->keys())
369
        );
370
        $names[] = '*';
371
        return array_unique($names);
372
    }
373
374
    /**
375
     * Get a set of hooks with the provided name(s).
376
     *
377
     * @param string|array $names The name of the function being hooked.
378
     * @param string[] $hooks The list of hook names (e.g. [HookManager::ALTER_RESULT])
379
     *
380
     * @return callable[]
381
     */
382
    public function get($names, $hooks)
383
    {
384
        $result = [];
385
        foreach ((array)$hooks as $hook) {
386
            foreach ((array)$names as $name) {
387
                $result = array_merge($result, $this->getHook($name, $hook));
388
            }
389
        }
390
        return $result;
391
    }
392
393
    /**
394
     * Get a single named hook.
395
     *
396
     * @param string $name The name of the hooked method
397
     * @param string $hook The specific hook name (e.g. alter)
398
     *
399
     * @return callable[]
400
     */
401
    public function getHook($name, $hook)
402
    {
403
        if (isset($this->hooks[$name][$hook])) {
404
            return $this->hooks[$name][$hook];
405
        }
406
        return [];
407
    }
408
409
    /**
410
     * Call the command event hooks.
411
     *
412
     * TODO: This should be moved to CommandEventHookDispatcher, which
413
     * should become the class that implements EventSubscriberInterface.
414
     * This change would break all clients, though, so postpone until next
415
     * major release.
416
     *
417
     * @param ConsoleCommandEvent $event
418
     */
419
    public function callCommandEventHooks(ConsoleCommandEvent $event)
420
    {
421
        /* @var Command $command */
422
        $command = $event->getCommand();
423
        $dispatcher = new CommandEventHookDispatcher($this, [$command->getName()]);
424
        $dispatcher->callCommandEventHooks($event);
425
    }
426
427
    /**
428
     * @{@inheritdoc}
429
     */
430
    public static function getSubscribedEvents()
431
    {
432
        return [ConsoleEvents::COMMAND => 'callCommandEventHooks'];
433
    }
434
}
435