Completed
Pull Request — master (#97)
by
unknown
02:57 queued 15s
created

HookManager::add()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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