Completed
Push — master ( 303d3b...22ea3e )
by Greg
07:12
created

HookManager::addPreValidator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
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
23
    const PRE_INTERACT = 'pre-interact';
24
    const INTERACT = 'interact';
25
    const POST_INTERACT = 'post-interact';
26
    const PRE_COMMAND_EVENT = 'pre-command';
27
    const COMMAND_EVENT = 'command';
28
    const POST_COMMAND_EVENT = 'post-command';
29
    const PRE_ARGUMENT_VALIDATOR = 'pre-validate';
30
    const ARGUMENT_VALIDATOR = 'validate';
31
    const POST_ARGUMENT_VALIDATOR = 'post-validate';
32
    const PRE_PROCESS_RESULT = 'pre-process';
33
    const PROCESS_RESULT = 'process';
34
    const POST_PROCESS_RESULT = 'post-process';
35
    const PRE_ALTER_RESULT = 'pre-alter';
36
    const ALTER_RESULT = 'alter';
37
    const POST_ALTER_RESULT = 'post-alter';
38
    const STATUS_DETERMINER = 'status';
39
    const EXTRACT_OUTPUT = 'extract';
40
41
    public function __construct()
42
    {
43
    }
44
45
    /**
46
     * Add a hook
47
     *
48
     * @param mixed $callback The callback function to call
49
     * @param string   $hook     The name of the hook to add
50
     * @param string   $name     The name of the command to hook
51
     *   ('*' for all)
52
     */
53
    public function add(callable $callback, $hook, $name = '*')
54
    {
55
        $this->hooks[$name][$hook][] = $callback;
56
    }
57
58
    /**
59
     * Add an interact hook
60
     *
61
     * @param type ValidatorInterface $validator
62
     * @param type $name The name of the command to hook
63
     *   ('*' for all)
64
     */
65
    public function addInteractor(InteractorInterface $interactor, $name = '*')
66
    {
67
        $this->hooks[$name][self::INTERACT][] = $interactor;
68
    }
69
70
    /**
71
     * Add a pre-validator hook
72
     *
73
     * @param type ValidatorInterface $validator
74
     * @param type $name The name of the command to hook
75
     *   ('*' for all)
76
     */
77
    public function addPreValidator(ValidatorInterface $validator, $name = '*')
78
    {
79
        $this->hooks[$name][self::PRE_ARGUMENT_VALIDATOR][] = $validator;
80
    }
81
82
    /**
83
     * Add a validator hook
84
     *
85
     * @param type ValidatorInterface $validator
86
     * @param type $name The name of the command to hook
87
     *   ('*' for all)
88
     */
89
    public function addValidator(ValidatorInterface $validator, $name = '*')
90
    {
91
        $this->hooks[$name][self::ARGUMENT_VALIDATOR][] = $validator;
92
    }
93
94
    /**
95
     * Add a result processor.
96
     *
97
     * @param type ProcessResultInterface $resultProcessor
98
     * @param type $name The name of the command to hook
99
     *   ('*' for all)
100
     */
101
    public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*')
102
    {
103
        $this->hooks[$name][self::PROCESS_RESULT][] = $resultProcessor;
104
    }
105
106
    /**
107
     * Add a result alterer. After a result is processed
108
     * by a result processor, an alter hook may be used
109
     * to convert the result from one form to another.
110
     *
111
     * @param type AlterResultInterface $resultAlterer
112
     * @param type $name The name of the command to hook
113
     *   ('*' for all)
114
     */
115
    public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*')
116
    {
117
        $this->hooks[$name][self::ALTER_RESULT][] = $resultAlterer;
118
    }
119
120
    /**
121
     * Add a status determiner. Usually, a command should return
122
     * an integer on error, or a result object on success (which
123
     * implies a status code of zero). If a result contains the
124
     * status code in some other field, then a status determiner
125
     * can be used to call the appropriate accessor method to
126
     * determine the status code.  This is usually not necessary,
127
     * though; a command that fails may return a CommandError
128
     * object, which contains a status code and a result message
129
     * to display.
130
     * @see CommandError::getExitCode()
131
     *
132
     * @param type StatusDeterminerInterface $statusDeterminer
133
     * @param type $name The name of the command to hook
134
     *   ('*' for all)
135
     */
136
    public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*')
137
    {
138
        $this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer;
139
    }
140
141
    /**
142
     * Add an output extractor. If a command returns an object
143
     * object, by default it is passed directly to the output
144
     * formatter (if in use) for rendering. If the result object
145
     * contains more information than just the data to render, though,
146
     * then an output extractor can be used to call the appopriate
147
     * accessor method of the result object to get the data to
148
     * rendered.  This is usually not necessary, though; it is preferable
149
     * to have complex result objects implement the OutputDataInterface.
150
     * @see OutputDataInterface::getOutputData()
151
     *
152
     * @param type ExtractOutputInterface $outputExtractor
153
     * @param type $name The name of the command to hook
154
     *   ('*' for all)
155
     */
156
    public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*')
157
    {
158
        $this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor;
159
    }
160
161
    public function interact(
162
        InputInterface $input,
163
        OutputInterface $output,
164
        $names,
165
        AnnotationData $annotationData
166
    ) {
167
        $interactors = $this->getInteractors($names, $annotationData);
168
        foreach ($interactors as $interactor) {
169
            $this->callInteractor($interactor, $input, $output, $annotationData);
170
        }
171
    }
172
173 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...
174
    {
175
        $validators = $this->getValidators($names, $annotationData);
176
        foreach ($validators as $validator) {
177
            $validated = $this->callValidator($validator, $args, $annotationData);
178
            if (is_object($validated)) {
179
                return $validated;
180
            }
181
            if (is_array($validated)) {
182
                $args = $validated;
183
            }
184
        }
185
        return $args;
186
    }
187
188
    /**
189
     * Process result and decide what to do with it.
190
     * Allow client to add transformation / interpretation
191
     * callbacks.
192
     */
193
    public function alterResult($names, $result, $args, AnnotationData $annotationData)
194
    {
195
        $processors = $this->getProcessResultHooks($names, $annotationData);
196
        foreach ($processors as $processor) {
197
            $result = $this->callProcessor($processor, $result, $args, $annotationData);
198
        }
199
        $alterers = $this->getAlterResultHooks($names, $annotationData);
200
        foreach ($alterers as $alterer) {
201
            $result = $this->callProcessor($alterer, $result, $args, $annotationData);
202
        }
203
204
        return $result;
205
    }
206
207
    /**
208
     * Call all status determiners, and see if any of them
209
     * know how to convert to a status code.
210
     */
211
    public function determineStatusCode($names, $result)
212
    {
213
        // If the result (post-processing) is an object that
214
        // implements ExitCodeInterface, then we will ask it
215
        // to give us the status code.
216
        if ($result instanceof ExitCodeInterface) {
217
            return $result->getExitCode();
218
        }
219
220
        // If the result does not implement ExitCodeInterface,
221
        // then we'll see if there is a determiner that can
222
        // extract a status code from the result.
223
        $determiners = $this->getStatusDeterminers($names);
224
        foreach ($determiners as $determiner) {
225
            $status = $this->callDeterminer($determiner, $result);
226
            if (isset($status)) {
227
                return $status;
228
            }
229
        }
230
    }
231
232
    /**
233
     * Convert the result object to printable output in
234
     * structured form.
235
     */
236
    public function extractOutput($names, $result)
237
    {
238
        if ($result instanceof OutputDataInterface) {
239
            return $result->getOutputData();
240
        }
241
242
        $extractors = $this->getOutputExtractors($names);
243
        foreach ($extractors as $extractor) {
244
            $structuredOutput = $this->callExtractor($extractor, $result);
245
            if (isset($structuredOutput)) {
246
                return $structuredOutput;
247
            }
248
        }
249
250
        return $result;
251
    }
252
253
    protected function getInteractors($names, AnnotationData $annotationData)
254
    {
255
        return $this->getHooks($names, self::INTERACT, $annotationData);
256
    }
257
258
    protected function getValidators($names, AnnotationData $annotationData)
259
    {
260
        return $this->getHooks($names, self::ARGUMENT_VALIDATOR, $annotationData);
261
    }
262
263
    protected function getStatusDeterminers($names)
264
    {
265
        return $this->getHooks($names, self::STATUS_DETERMINER);
266
    }
267
268
    protected function getProcessResultHooks($names, AnnotationData $annotationData)
269
    {
270
        return $this->getHooks($names, self::PROCESS_RESULT, $annotationData);
271
    }
272
273
    protected function getAlterResultHooks($names, AnnotationData $annotationData)
274
    {
275
        return $this->getHooks($names, self::ALTER_RESULT, $annotationData);
276
    }
277
278
    protected function getOutputExtractors($names)
279
    {
280
        return $this->getHooks($names, self::EXTRACT_OUTPUT);
281
    }
282
283
    protected function getCommandEvents($names)
284
    {
285
        return $this->getHooks($names, self::COMMAND_EVENT);
286
    }
287
288
    /**
289
     * Get a set of hooks with the provided name(s). Include the
290
     * pre- and post- hooks, and also include the global hooks ('*')
291
     * in addition to the named hooks provided.
292
     *
293
     * @param string|array $names The name of the function being hooked.
294
     * @param string $hook The specific hook name (e.g. alter)
295
     *
296
     * @return callable[]
297
     */
298
    protected function getHooks($names, $hook, $annotationData = null)
299
    {
300
        $names = array_merge(
301
            (array)$names,
302
            ($annotationData == null) ? [] : array_map(function ($item) {
303
                return "@$item";
304
            }, $annotationData->keys())
305
        );
306
        $names[] = '*';
307
        return array_merge(
308
            $this->get($names, "pre-$hook"),
309
            $this->get($names, $hook),
310
            $this->get($names, "post-$hook")
311
        );
312
    }
313
314
    /**
315
     * Get a set of hooks with the provided name(s).
316
     *
317
     * @param string|array $names The name of the function being hooked.
318
     * @param string $hook The specific hook name (e.g. alter)
319
     *
320
     * @return callable[]
321
     */
322
    public function get($names, $hook)
323
    {
324
        $hooks = [];
325
        foreach ((array)$names as $name) {
326
            $hooks = array_merge($hooks, $this->getHook($name, $hook));
327
        }
328
        return $hooks;
329
    }
330
331
    /**
332
     * Get a single named hook.
333
     *
334
     * @param string $name The name of the hooked method
335
     * @param string $hook The specific hook name (e.g. alter)
336
     *
337
     * @return callable[]
338
     */
339
    protected function getHook($name, $hook)
340
    {
341
        if (isset($this->hooks[$name][$hook])) {
342
            return $this->hooks[$name][$hook];
343
        }
344
        return [];
345
    }
346
347
    protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData)
348
    {
349
        if ($interactor instanceof InteractorInterface) {
350
            return $interactor->interact($input, $output, $annotationData);
351
        }
352
        if (is_callable($interactor)) {
353
            return $interactor($input, $output, $annotationData);
354
        }
355
    }
356
357
    protected function callValidator($validator, $args, AnnotationData $annotationData)
358
    {
359
        // TODO: Adding AnnotationData to ValidatorInterface would be
360
        // a breaking change. Either hold off until 2.x, or make
361
        // a new interface containing a method that takes the extra parameter.
362
        if ($validator instanceof ValidatorInterface) {
363
            return $validator->validate($args);
364
        }
365
        if (is_callable($validator)) {
366
            return $validator($args, $annotationData);
367
        }
368
    }
369
370
    protected function callProcessor($processor, $result, $args, AnnotationData $annotationData)
371
    {
372
        $processed = null;
373
        // TODO: Adding AnnotationData to ProcessResultInterface would be
374
        // a breaking change. Either hold off until 2.x, or make
375
        // a new interface containing a method that takes the extra parameter.
376
        if ($processor instanceof ProcessResultInterface) {
377
            $processed = $processor->process($result, $args);
378
        }
379
        if (is_callable($processor)) {
380
            $processed = $processor($result, $args, $annotationData);
381
        }
382
        if (isset($processed)) {
383
            return $processed;
384
        }
385
        return $result;
386
    }
387
388
    protected function callDeterminer($determiner, $result)
389
    {
390
        if ($determiner instanceof StatusDeterminerInterface) {
391
            return $determiner->determineStatusCode($result);
392
        }
393
        if (is_callable($determiner)) {
394
            return $determiner($result);
395
        }
396
    }
397
398
    protected function callExtractor($extractor, $result)
399
    {
400
        if ($extractor instanceof ExtractOutputInterface) {
401
            return $extractor->extractOutput($result);
402
        }
403
        if (is_callable($extractor)) {
404
            return $extractor($result);
405
        }
406
    }
407
408
    /**
409
     * @param ConsoleCommandEvent $event
410
     */
411
    public function callCommandEventHooks(ConsoleCommandEvent $event)
412
    {
413
        /* @var Command $command */
414
        $command = $event->getCommand();
415
        $names = [$command->getName()];
416
        $commandEventHooks = $this->getCommandEvents($names);
417
        foreach ($commandEventHooks as $commandEvent) {
418
            if (is_callable($commandEvent)) {
419
                $commandEvent($event);
420
            }
421
        }
422
    }
423
424
    /**
425
     * @{@inheritdoc}
426
     */
427
    public static function getSubscribedEvents()
428
    {
429
        return [ConsoleEvents::COMMAND => 'callCommandEventHooks'];
430
    }
431
}
432