Completed
Pull Request — master (#48)
by Greg
02:24
created

AnnotatedCommandFactory   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 295
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 40
c 1
b 0
f 0
lcom 1
cbo 8
dl 0
loc 295
rs 8.2608

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A setCommandProcessor() 0 4 1
A commandProcessor() 0 4 1
A setIncludeAllPublicMethods() 0 4 1
A getIncludeAllPublicMethods() 0 4 1
A hookManager() 0 4 1
A addListener() 0 4 1
A notify() 0 6 2
A addAutomaticOptionProvider() 0 4 1
A addCommandInfoAlterer() 0 4 1
A createCommandsFromClass() 0 11 2
A getCommandInfoListFromClass() 0 20 3
A createCommandInfo() 0 4 1
A createCommandsFromClassInfo() 0 14 2
A createSelectedCommandsFromClassInfo() 0 13 3
A isCommandMethod() 0 7 3
A registerCommandHooksFromClassInfo() 0 8 3
A registerCommandHook() 0 21 3
A getNthWord() 0 8 2
A createCommand() 0 16 1
A alterCommandInfo() 0 6 2
A callAutomaticOptionsProviders() 0 8 2
A automaticOptions() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like AnnotatedCommandFactory 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 AnnotatedCommandFactory, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Consolidation\AnnotatedCommand;
3
4
use Consolidation\AnnotatedCommand\Hooks\HookManager;
5
use Consolidation\AnnotatedCommand\Options\AutomaticOptionsProviderInterface;
6
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
7
use Consolidation\OutputFormatters\Options\FormatterOptions;
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Output\OutputInterface;
11
12
/**
13
 * The AnnotatedCommandFactory creates commands for your application.
14
 * Use with a Dependency Injection Container and the CommandFactory.
15
 * Alternately, use the CommandFileDiscovery to find commandfiles, and
16
 * then use AnnotatedCommandFactory::createCommandsFromClass() to create
17
 * commands.  See the README for more information.
18
 *
19
 * @package Consolidation\AnnotatedCommand
20
 */
21
class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
22
{
23
    /** var CommandProcessor */
24
    protected $commandProcessor;
25
26
    /** var CommandCreationListenerInterface[] */
27
    protected $listeners = [];
28
29
    /** var AutomaticOptionsProvider[] */
30
31
    protected $automaticOptionsProviderList = [];
32
33
    /** var boolean */
34
    protected $includeAllPublicMethods = true;
35
36
    /** var CommandInfoAltererInterface */
37
    protected $commandInfoAlterers = [];
38
39
    public function __construct()
40
    {
41
        $this->commandProcessor = new CommandProcessor(new HookManager());
42
        $this->addAutomaticOptionProvider($this);
43
    }
44
45
    public function setCommandProcessor(CommandProcessor $commandProcessor)
46
    {
47
        $this->commandProcessor = $commandProcessor;
48
    }
49
50
    /**
51
     * @return CommandProcessor
52
     */
53
    public function commandProcessor()
54
    {
55
        return $this->commandProcessor;
56
    }
57
58
    /**
59
     * Set the 'include all public methods flag'. If true (the default), then
60
     * every public method of each commandFile will be used to create commands.
61
     * If it is false, then only those public methods annotated with @command
62
     * or @name (deprecated) will be used to create commands.
63
     */
64
    public function setIncludeAllPublicMethods($includeAllPublicMethods)
65
    {
66
        $this->includeAllPublicMethods = $includeAllPublicMethods;
67
    }
68
69
    public function getIncludeAllPublicMethods()
70
    {
71
        return $this->includeAllPublicMethods;
72
    }
73
74
    /**
75
     * @return HookManager
76
     */
77
    public function hookManager()
78
    {
79
        return $this->commandProcessor()->hookManager();
80
    }
81
82
    /**
83
     * Add a listener that is notified immediately before the command
84
     * factory creates commands from a commandFile instance.  This
85
     * listener can use this opportunity to do more setup for the commandFile,
86
     * and so on.
87
     *
88
     * @param CommandCreationListenerInterface $listener
89
     */
90
    public function addListener(CommandCreationListenerInterface $listener)
91
    {
92
        $this->listeners[] = $listener;
93
    }
94
95
    /**
96
     * Call all command creation listeners
97
     *
98
     * @param object $commandFileInstance
99
     */
100
    protected function notify($commandFileInstance)
101
    {
102
        foreach ($this->listeners as $listener) {
103
            $listener->notifyCommandFileAdded($commandFileInstance);
104
        }
105
    }
106
107
    public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider)
108
    {
109
        $this->automaticOptionsProviderList[] = $optionsProvider;
110
    }
111
112
    public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer)
113
    {
114
        $this->commandInfoAlterers[] = $alterer;
115
    }
116
117
    /**
118
     * n.b. This registers all hooks from the commandfile instance as a side-effect.
119
     */
120
    public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null)
121
    {
122
        // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
123
        if (!isset($includeAllPublicMethods)) {
124
            $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
125
        }
126
        $this->notify($commandFileInstance);
127
        $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
128
        $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
129
        return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
130
    }
131
132
    public function getCommandInfoListFromClass($classNameOrInstance)
133
    {
134
        $commandInfoList = [];
135
136
        // Ignore special functions, such as __construct and __call, and
137
        // accessor methods such as getFoo and setFoo, while allowing
138
        // set or setup.
139
        $commandMethodNames = array_filter(
140
            get_class_methods($classNameOrInstance) ?: [],
141
            function ($m) {
142
                return !preg_match('#^(_|get[A-Z]|set[A-Z])#', $m);
143
            }
144
        );
145
146
        foreach ($commandMethodNames as $commandMethodName) {
147
            $commandInfoList[] = new CommandInfo($classNameOrInstance, $commandMethodName);
148
        }
149
150
        return $commandInfoList;
151
    }
152
153
    public function createCommandInfo($classNameOrInstance, $commandMethodName)
154
    {
155
        return new CommandInfo($classNameOrInstance, $commandMethodName);
156
    }
157
158
    public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
159
    {
160
        // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
161
        if (!isset($includeAllPublicMethods)) {
162
            $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
163
        }
164
        return $this->createSelectedCommandsFromClassInfo(
165
            $commandInfoList,
166
            $commandFileInstance,
167
            function ($commandInfo) use ($includeAllPublicMethods) {
168
                return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
169
            }
170
        );
171
    }
172
173
    public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
174
    {
175
        $commandList = [];
176
177
        foreach ($commandInfoList as $commandInfo) {
178
            if ($commandSelector($commandInfo)) {
179
                $command = $this->createCommand($commandInfo, $commandFileInstance);
180
                $commandList[] = $command;
181
            }
182
        }
183
184
        return $commandList;
185
    }
186
187
    public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
188
    {
189
        if ($commandInfo->hasAnnotation('hook')) {
190
            return false;
191
        }
192
        return $includeAllPublicMethods || $commandInfo->hasAnnotation('command');
193
    }
194
195
    public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
196
    {
197
        foreach ($commandInfoList as $commandInfo) {
198
            if ($commandInfo->hasAnnotation('hook')) {
199
                $this->registerCommandHook($commandInfo, $commandFileInstance);
200
            }
201
        }
202
    }
203
204
    /**
205
     * Register a command hook given the CommandInfo for a method.
206
     *
207
     * The hook format is:
208
     *
209
     *   @hook type name type
210
     *
211
     * For example, the pre-validate hook for the core:init command is:
212
     *
213
     *   @hook pre-validate core:init
214
     *
215
     * If no command name is provided, then this hook will affect every
216
     * command that is defined in the same file.
217
     *
218
     * If no hook is provided, then we will presume that ALTER_RESULT
219
     * is intended.
220
     *
221
     * @param CommandInfo $commandInfo Information about the command hook method.
222
     * @param object $commandFileInstance An instance of the CommandFile class.
223
     */
224
    public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
225
    {
226
        // Ignore if the command info has no @hook
227
        if (!$commandInfo->hasAnnotation('hook')) {
228
            return;
229
        }
230
        $hookData = $commandInfo->getAnnotation('hook');
231
        $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
232
        $commandName = $this->getNthWord($hookData, 1);
233
234
        // Register the hook
235
        $callback = [$commandFileInstance, $commandInfo->getMethodName()];
236
        $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
237
238
        // If the hook has options, then also register the commandInfo
239
        // with the hook manager, so that we can add options and such to
240
        // the commands they hook.
241
        if (!$commandInfo->options()->isEmpty()) {
242
            $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName);
243
        }
244
    }
245
246
    protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
247
    {
248
        $words = explode($delimiter, $string);
249
        if (!empty($words[$n])) {
250
            return $words[$n];
251
        }
252
        return $default;
253
    }
254
255
    public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
256
    {
257
        $this->alterCommandInfo($commandInfo, $commandFileInstance);
258
        $command = new AnnotatedCommand($commandInfo->getName());
259
        $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
260
        $command->setCommandCallback($commandCallback);
261
        $command->setCommandProcessor($this->commandProcessor);
262
        $command->setCommandInfo($commandInfo);
263
        $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo);
264
        $command->setCommandOptions($commandInfo, $automaticOptions);
265
        // Annotation commands are never bootstrap-aware, but for completeness
266
        // we will notify on every created command, as some clients may wish to
267
        // use this notification for some other purpose.
268
        $this->notify($command);
269
        return $command;
270
    }
271
272
    /**
273
     * Give plugins an opportunity to update the commandInfo
274
     */
275
    public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
276
    {
277
        foreach ($this->commandInfoAlterers as $alterer) {
278
            $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
279
        }
280
    }
281
282
    /**
283
     * Get the options that are implied by annotations, e.g. @fields implies
284
     * that there should be a --fields and a --format option.
285
     *
286
     * @return InputOption[]
287
     */
288
    public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
289
    {
290
        $automaticOptions = [];
291
        foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
292
            $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
293
        }
294
        return $automaticOptions;
295
    }
296
297
    /**
298
     * Get the options that are implied by annotations, e.g. @fields implies
299
     * that there should be a --fields and a --format option.
300
     *
301
     * @return InputOption[]
302
     */
303
    public function automaticOptions(CommandInfo $commandInfo)
304
    {
305
        $automaticOptions = [];
306
        $formatManager = $this->commandProcessor()->formatterManager();
307
        if ($formatManager) {
308
            $annotationData = $commandInfo->getAnnotations()->getArrayCopy();
309
            $formatterOptions = new FormatterOptions($annotationData);
310
            $dataType = $commandInfo->getReturnType();
311
            $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
312
        }
313
        return $automaticOptions;
314
    }
315
}
316