Completed
Push — master ( d8dace...c65c33 )
by Greg
02:20
created

HookManager   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 325
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 47
c 2
b 0
f 1
lcom 1
cbo 6
dl 0
loc 325
rs 8.439

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A add() 0 4 1
A addValidator() 0 5 1
A addResultProcessor() 0 5 1
A addAlterResult() 0 5 1
A addStatusDeterminer() 0 4 1
A addOutputExtractor() 0 4 1
A get() 0 8 2
A validateArguments() 0 14 4
A alterResult() 0 13 3
A determineStatusCode() 0 20 4
A extractOutput() 0 16 4
A getValidators() 0 4 1
A getStatusDeterminers() 0 4 1
A getProcessResultHooks() 0 4 1
A getAlterResultHooks() 0 4 1
A getOutputExtractors() 0 4 1
A getHooks() 0 10 1
A getHook() 0 7 2
A callValidator() 0 9 3
A callProcessor() 0 14 4
A callDeterminer() 0 9 3
A callExtractor() 0 9 3
A checkValidStage() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like HookManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HookManager, and based on these observations, apply Extract Interface, too.

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 Consolidation\AnnotatedCommand\ExitCodeInterface;
9
use Consolidation\AnnotatedCommand\OutputDataInterface;
10
11
/**
12
 * Manage named callback hooks
13
 */
14
class HookManager
15
{
16
    protected $hooks = [];
17
18
    const ARGUMENT_VALIDATOR = 'validate';
19
    const PROCESS_RESULT = 'process';
20
    const ALTER_RESULT = 'alter';
21
    const STATUS_DETERMINER = 'status';
22
    const EXTRACT_OUTPUT = 'extract';
23
24
    const PRE_STAGE = 'pre-';
25
    const PRIMARY_STAGE = '';
26
    const POST_STAGE = 'post-';
27
28
    public function __construct()
29
    {
30
    }
31
32
    /**
33
     * Add a hook
34
     *
35
     * @param string   $name     The name of the command to hook
36
     *   ('*' for all)
37
     * @param string   $hook     The name of the hook to add
38
     * @param mixed $callback The callback function to call
39
     */
40
    public function add($name, $hook, callable $callback)
41
    {
42
        $this->hooks[$name][$hook][] = $callback;
43
    }
44
45
    /**
46
     * Add a validator hook
47
     *
48
     * @param type ValidatorInterface $validator
49
     * @param type $name The name of the command to hook
50
     *   ('*' for all)
51
     */
52
    public function addValidator(ValidatorInterface $validator, $name = '*', $stage = self::PRIMARY_STAGE)
53
    {
54
        $this->checkValidStage($stage);
55
        $this->hooks[$name][$stage . self::ARGUMENT_VALIDATOR][] = $validator;
56
    }
57
58
    /**
59
     * Add a result processor.
60
     *
61
     * @param type ProcessResultInterface $resultProcessor
62
     * @param type $name The name of the command to hook
63
     *   ('*' for all)
64
     */
65
    public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*', $stage = self::PRIMARY_STAGE)
66
    {
67
        $this->checkValidStage($stage);
68
        $this->hooks[$name][$stage . self::PROCESS_RESULT][] = $resultProcessor;
69
    }
70
71
    /**
72
     * Add a result alterer. After a result is processed
73
     * by a result processor, an alter hook may be used
74
     * to convert the result from one form to another.
75
     *
76
     * @param type AlterResultInterface $resultAlterer
77
     * @param type $name The name of the command to hook
78
     *   ('*' for all)
79
     */
80
    public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*', $stage = self::PRIMARY_STAGE)
81
    {
82
        $this->checkValidStage($stage);
83
        $this->hooks[$name][$stage . self::ALTER_RESULT][] = $resultAlterer;
84
    }
85
86
    /**
87
     * Add a status determiner. Usually, a command should return
88
     * an integer on error, or a result object on success (which
89
     * implies a status code of zero). If a result contains the
90
     * status code in some other field, then a status determiner
91
     * can be used to call the appropriate accessor method to
92
     * determine the status code.  This is usually not necessary,
93
     * though; a command that fails may return a CommandError
94
     * object, which contains a status code and a result message
95
     * to display.
96
     * @see CommandError::getExitCode()
97
     *
98
     * @param type StatusDeterminerInterface $statusDeterminer
99
     * @param type $name The name of the command to hook
100
     *   ('*' for all)
101
     */
102
    public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*')
103
    {
104
        $this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer;
105
    }
106
107
    /**
108
     * Add an output extractor. If a command returns an object
109
     * object, by default it is passed directly to the output
110
     * formatter (if in use) for rendering. If the result object
111
     * contains more information than just the data to render, though,
112
     * then an output extractor can be used to call the appopriate
113
     * accessor method of the result object to get the data to
114
     * rendered.  This is usually not necessary, though; it is preferable
115
     * to have complex result objects implement the OutputDataInterface.
116
     * @see OutputDataInterface::getOutputData()
117
     *
118
     * @param type ExtractOutputInterface $outputExtractor
119
     * @param type $name The name of the command to hook
120
     *   ('*' for all)
121
     */
122
    public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*')
123
    {
124
        $this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor;
125
    }
126
127
    /**
128
     * Get a set of hooks with the provided name(s).
129
     *
130
     * @param string|array $names The name of the function being hooked.
131
     * @param string $hook The specific hook name (e.g. alter)
132
     *
133
     * @return callable[]
134
     */
135
    public function get($names, $hook)
136
    {
137
        $hooks = [];
138
        foreach ((array)$names as $name) {
139
            $hooks = array_merge($hooks, $this->getHook($name, $hook));
140
        }
141
        return $hooks;
142
    }
143
144
    public function validateArguments($names, $args)
145
    {
146
        $validators = $this->getValidators($names);
147
        foreach ($validators as $validator) {
148
            $validated = $this->callValidator($validator, $args);
149
            if (is_object($validated)) {
150
                return $validated;
151
            }
152
            if (is_array($validated)) {
153
                $args = $validated;
154
            }
155
        }
156
        return $args;
157
    }
158
159
    /**
160
     * Process result and decide what to do with it.
161
     * Allow client to add transformation / interpretation
162
     * callbacks.
163
     */
164
    public function alterResult($names, $result, $args)
165
    {
166
        $processors = $this->getProcessResultHooks($names);
167
        foreach ($processors as $processor) {
168
            $result = $this->callProcessor($processor, $result, $args);
169
        }
170
        $alterers = $this->getAlterResultHooks($names);
171
        foreach ($alterers as $alterer) {
172
            $result = $this->callProcessor($alterer, $result, $args);
173
        }
174
175
        return $result;
176
    }
177
178
    /**
179
     * Call all status determiners, and see if any of them
180
     * know how to convert to a status code.
181
     */
182
    public function determineStatusCode($names, $result)
183
    {
184
        // If the result (post-processing) is an object that
185
        // implements ExitCodeInterface, then we will ask it
186
        // to give us the status code.
187
        if ($result instanceof ExitCodeInterface) {
188
            return $result->getExitCode();
189
        }
190
191
        // If the result does not implement ExitCodeInterface,
192
        // then we'll see if there is a determiner that can
193
        // extract a status code from the result.
194
        $determiners = $this->getStatusDeterminers($names);
195
        foreach ($determiners as $determiner) {
196
            $status = $this->callDeterminer($determiner, $result);
197
            if (isset($status)) {
198
                return $status;
199
            }
200
        }
201
    }
202
203
    /**
204
     * Convert the result object to printable output in
205
     * structured form.
206
     */
207
    public function extractOutput($names, $result)
208
    {
209
        if ($result instanceof OutputDataInterface) {
210
            return $result->getOutputData();
211
        }
212
213
        $extractors = $this->getOutputExtractors($names);
214
        foreach ($extractors as $extractor) {
215
            $structuredOutput = $this->callExtractor($extractor, $result);
216
            if (isset($structuredOutput)) {
217
                return $structuredOutput;
218
            }
219
        }
220
221
        return $result;
222
    }
223
224
    protected function getValidators($names)
225
    {
226
        return $this->getHooks($names, self::ARGUMENT_VALIDATOR);
227
    }
228
229
    protected function getStatusDeterminers($names)
230
    {
231
        return $this->getHooks($names, self::STATUS_DETERMINER);
232
    }
233
234
    protected function getProcessResultHooks($names)
235
    {
236
        return $this->getHooks($names, self::PROCESS_RESULT);
237
    }
238
239
    protected function getAlterResultHooks($names)
240
    {
241
        return $this->getHooks($names, self::ALTER_RESULT);
242
    }
243
244
    protected function getOutputExtractors($names)
245
    {
246
        return $this->getHooks($names, self::EXTRACT_OUTPUT);
247
    }
248
249
    /**
250
     * Get a set of hooks with the provided name(s). Include the
251
     * pre- and post- hooks, and also include the global hooks ('*')
252
     * in addition to the named hooks provided.
253
     *
254
     * @param string|array $names The name of the function being hooked.
255
     * @param string $hook The specific hook name (e.g. alter)
256
     *
257
     * @return callable[]
258
     */
259
    protected function getHooks($names, $hook)
260
    {
261
        $names = (array)$names;
262
        $names[] = '*';
263
        return array_merge(
264
            $this->get($names, "pre-$hook"),
265
            $this->get($names, $hook),
266
            $this->get($names, "post-$hook")
267
        );
268
    }
269
270
    /**
271
     * Get a single named hook.
272
     *
273
     * @param string $name The name of the hooked method
274
     * @param string $hook The specific hook name (e.g. alter)
275
     *
276
     * @return callable[]
277
     */
278
    protected function getHook($name, $hook)
279
    {
280
        if (isset($this->hooks[$name][$hook])) {
281
            return $this->hooks[$name][$hook];
282
        }
283
        return [];
284
    }
285
286
    protected function callValidator($validator, $args)
287
    {
288
        if ($validator instanceof ValidatorInterface) {
289
            return $validator->validate($args);
290
        }
291
        if (is_callable($validator)) {
292
            return $validator($args);
293
        }
294
    }
295
296
    protected function callProcessor($processor, $result, $args)
297
    {
298
        $processed = null;
299
        if ($processor instanceof ProcessResultInterface) {
300
            $processed = $processor->process($result, $args);
301
        }
302
        if (is_callable($processor)) {
303
            $processed = $processor($result, $args);
304
        }
305
        if (isset($processed)) {
306
            return $processed;
307
        }
308
        return $result;
309
    }
310
311
    protected function callDeterminer($determiner, $result)
312
    {
313
        if ($determiner instanceof StatusDeterminerInterface) {
314
            return $determiner->determineStatusCode($result);
315
        }
316
        if (is_callable($determiner)) {
317
            return $determiner($result);
318
        }
319
    }
320
321
    protected function callExtractor($extractor, $result)
322
    {
323
        if ($extractor instanceof ExtractOutputInterface) {
324
            return $extractor->extractOutput($result);
325
        }
326
        if (is_callable($extractor)) {
327
            return $extractor($result);
328
        }
329
    }
330
331
    protected function checkValidStage($stage)
332
    {
333
        $validStages = [self::PRE_STAGE, self::PRIMARY_STAGE, self::POST_STAGE];
334
        if (!in_array($stage, $validStages)) {
335
            throw new \Exception("Invalid stage '$stage' specified; must be one of " . implode(',', $validStages));
336
        }
337
    }
338
}
339