Completed
Pull Request — master (#81)
by Greg
02:12
created

AnnotatedCommandFactory   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 57
c 1
b 0
f 0
lcom 1
cbo 12
dl 0
loc 412
rs 6.433

30 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A setCommandProcessor() 0 5 1
A commandProcessor() 0 4 1
A setIncludeAllPublicMethods() 0 5 1
A getIncludeAllPublicMethods() 0 4 1
A hookManager() 0 4 1
A addListener() 0 5 1
A addListernerCallback() 0 5 1
A notify() 0 6 2
A addAutomaticOptionProvider() 0 4 1
A addCommandInfoAlterer() 0 4 1
A createCommandsFromClass() 0 11 2
A getCommandInfoListFromClass() 0 10 2
A storeCommandInfoListInCache() 0 15 4
A getCommandInfoListFromCache() 0 15 4
A hasDataStore() 0 4 1
A setDataStore() 0 8 2
A getDataStore() 0 4 1
B createCommandInfoListFromClass() 0 22 4
A createCommandInfo() 0 4 1
A createCommandsFromClassInfo() 0 14 2
A createSelectedCommandsFromClassInfo() 0 13 3
A isCommandMethod() 0 18 4
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\Cache\NullCache;
5
use Consolidation\AnnotatedCommand\Cache\CacheWrapper;
6
use Consolidation\AnnotatedCommand\Cache\SimpleCacheInterface;
7
use Consolidation\AnnotatedCommand\Hooks\HookManager;
8
use Consolidation\AnnotatedCommand\Options\AutomaticOptionsProviderInterface;
9
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
10
use Consolidation\OutputFormatters\Options\FormatterOptions;
11
use Symfony\Component\Console\Command\Command;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Output\OutputInterface;
14
15
/**
16
 * The AnnotatedCommandFactory creates commands for your application.
17
 * Use with a Dependency Injection Container and the CommandFactory.
18
 * Alternately, use the CommandFileDiscovery to find commandfiles, and
19
 * then use AnnotatedCommandFactory::createCommandsFromClass() to create
20
 * commands.  See the README for more information.
21
 *
22
 * @package Consolidation\AnnotatedCommand
23
 */
24
class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
25
{
26
    /** var CommandProcessor */
27
    protected $commandProcessor;
28
29
    /** var CommandCreationListenerInterface[] */
30
    protected $listeners = [];
31
32
    /** var AutomaticOptionsProvider[] */
33
    protected $automaticOptionsProviderList = [];
34
35
    /** var boolean */
36
    protected $includeAllPublicMethods = true;
37
38
    /** var CommandInfoAltererInterface */
39
    protected $commandInfoAlterers = [];
40
41
    /** var SimpleCacheInterface */
42
    protected $dataStore;
43
44
    public function __construct()
45
    {
46
        $this->dataStore = new NullCache();
47
        $this->commandProcessor = new CommandProcessor(new HookManager());
48
        $this->addAutomaticOptionProvider($this);
49
    }
50
51
    public function setCommandProcessor(CommandProcessor $commandProcessor)
52
    {
53
        $this->commandProcessor = $commandProcessor;
54
        return $this;
55
    }
56
57
    /**
58
     * @return CommandProcessor
59
     */
60
    public function commandProcessor()
61
    {
62
        return $this->commandProcessor;
63
    }
64
65
    /**
66
     * Set the 'include all public methods flag'. If true (the default), then
67
     * every public method of each commandFile will be used to create commands.
68
     * If it is false, then only those public methods annotated with @command
69
     * or @name (deprecated) will be used to create commands.
70
     */
71
    public function setIncludeAllPublicMethods($includeAllPublicMethods)
72
    {
73
        $this->includeAllPublicMethods = $includeAllPublicMethods;
74
        return $this;
75
    }
76
77
    public function getIncludeAllPublicMethods()
78
    {
79
        return $this->includeAllPublicMethods;
80
    }
81
82
    /**
83
     * @return HookManager
84
     */
85
    public function hookManager()
86
    {
87
        return $this->commandProcessor()->hookManager();
88
    }
89
90
    /**
91
     * Add a listener that is notified immediately before the command
92
     * factory creates commands from a commandFile instance.  This
93
     * listener can use this opportunity to do more setup for the commandFile,
94
     * and so on.
95
     *
96
     * @param CommandCreationListenerInterface $listener
97
     */
98
    public function addListener(CommandCreationListenerInterface $listener)
99
    {
100
        $this->listeners[] = $listener;
101
        return $this;
102
    }
103
104
    /**
105
     * Add a listener that's just a simple 'callable'.
106
     * @param callable $listener
107
     */
108
    public function addListernerCallback(callable $listener)
109
    {
110
        $this->addListener(new CommandCreationListener($listener));
111
        return $this;
112
    }
113
114
    /**
115
     * Call all command creation listeners
116
     *
117
     * @param object $commandFileInstance
118
     */
119
    protected function notify($commandFileInstance)
120
    {
121
        foreach ($this->listeners as $listener) {
122
            $listener->notifyCommandFileAdded($commandFileInstance);
123
        }
124
    }
125
126
    public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider)
127
    {
128
        $this->automaticOptionsProviderList[] = $optionsProvider;
129
    }
130
131
    public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer)
132
    {
133
        $this->commandInfoAlterers[] = $alterer;
134
    }
135
136
    /**
137
     * n.b. This registers all hooks from the commandfile instance as a side-effect.
138
     */
139
    public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null)
140
    {
141
        // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
142
        if (!isset($includeAllPublicMethods)) {
143
            $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
144
        }
145
        $this->notify($commandFileInstance);
146
        $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
147
        $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
148
        return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
149
    }
150
151
    public function getCommandInfoListFromClass($commandFileInstance)
152
    {
153
        $cachedCommandInfoList = $this->getCommandInfoListFromCache($commandFileInstance);
154
        $commandInfoList = $this->createCommandInfoListFromClass($commandFileInstance, $cachedCommandInfoList);
155
        if (!empty($commandInfoList)) {
156
            $cachedCommandInfoList = array_merge($commandInfoList, $cachedCommandInfoList);
157
            $this->storeCommandInfoListInCache($commandFileInstance, $cachedCommandInfoList);
158
        }
159
        return $cachedCommandInfoList;
160
    }
161
162
    protected function storeCommandInfoListInCache($commandFileInstance, $commandInfoList)
163
    {
164
        if (!$this->hasDataStore()) {
165
            return;
166
        }
167
        $cache_data = [];
168
        $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
169
        foreach ($commandInfoList as $i => $commandInfo) {
170
            if (static::isCommandMethod($commandInfo, $includeAllPublicMethods)) {
171
                $cache_data[$i] = $commandInfo->serialize();
172
            }
173
        }
174
        $className = get_class($commandFileInstance);
175
        $this->getDataStore()->set($className, $cache_data);
176
    }
177
178
    /**
179
     * Get the command info list from the cache
180
     *
181
     * @param mixed $commandFileInstance
182
     * @return array
183
     */
184
    protected function getCommandInfoListFromCache($commandFileInstance)
185
    {
186
        $commandInfoList = [];
187
        $className = get_class($commandFileInstance);
188
        if (!$this->getDataStore()->has($className)) {
189
            return [];
190
        }
191
        $cache_data = $this->getDataStore()->get($className);
192
        foreach ($cache_data as $i => $data) {
193
            if (CommandInfo::isValidSerializedData((array)$data)) {
194
                $commandInfoList[$i] = CommandInfo::deserialize((array)$data);
195
            }
196
        }
197
        return $commandInfoList;
198
    }
199
200
    /**
201
     * Check to see if this factory has a cache datastore.
202
     * @return boolean
203
     */
204
    public function hasDataStore()
205
    {
206
        return !($this->dataStore instanceof NullCache);
207
    }
208
209
    /**
210
     * Set a cache datastore for this factory. Any object with 'set' and
211
     * 'get' methods is acceptable. The key is the classname being cached,
212
     * and the value is a nested associative array of strings.
213
     *
214
     * TODO: Typehint this to SimpleCacheInterface
215
     *
216
     * This is not done currently to allow clients to use a generic cache
217
     * store that does not itself depend on the annotated-command library.
218
     *
219
     * @param Mixed $dataStore
220
     * @return type
221
     */
222
    public function setDataStore($dataStore)
223
    {
224
        if (!($dataStore instanceof SimpleCacheInterface)) {
225
            $dataStore = new CacheWrapper($dataStore);
226
        }
227
        $this->dataStore = $dataStore;
228
        return $this;
229
    }
230
231
    /**
232
     * Get the data store attached to this factory.
233
     */
234
    public function getDataStore()
235
    {
236
        return $this->dataStore;
237
    }
238
239
    protected function createCommandInfoListFromClass($classNameOrInstance, $cachedCommandInfoList)
240
    {
241
        $commandInfoList = [];
242
243
        // Ignore special functions, such as __construct and __call, which
244
        // can never be commands.
245
        $commandMethodNames = array_filter(
246
            get_class_methods($classNameOrInstance) ?: [],
247
            function ($m) {
248
                return !preg_match('#^_#', $m);
249
            }
250
        );
251
252
        foreach ($commandMethodNames as $commandMethodName) {
253
            if (!array_key_exists($commandMethodName, $cachedCommandInfoList)) {
254
                $commandInfo = CommandInfo::create($classNameOrInstance, $commandMethodName);
255
                $commandInfoList[$commandInfo->getMethodName()] =  $commandInfo;
256
            }
257
        }
258
259
        return $commandInfoList;
260
    }
261
262
    public function createCommandInfo($classNameOrInstance, $commandMethodName)
263
    {
264
        return CommandInfo::create($classNameOrInstance, $commandMethodName);
265
    }
266
267
    public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
268
    {
269
        // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
270
        if (!isset($includeAllPublicMethods)) {
271
            $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
272
        }
273
        return $this->createSelectedCommandsFromClassInfo(
274
            $commandInfoList,
275
            $commandFileInstance,
276
            function ($commandInfo) use ($includeAllPublicMethods) {
277
                return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
278
            }
279
        );
280
    }
281
282
    public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
283
    {
284
        $commandList = [];
285
286
        foreach ($commandInfoList as $commandInfo) {
287
            if ($commandSelector($commandInfo)) {
288
                $command = $this->createCommand($commandInfo, $commandFileInstance);
289
                $commandList[] = $command;
290
            }
291
        }
292
293
        return $commandList;
294
    }
295
296
    public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
297
    {
298
        // Ignore everything labeled @hook
299
        if ($commandInfo->hasAnnotation('hook')) {
300
            return false;
301
        }
302
        // Include everything labeled @command
303
        if ($commandInfo->hasAnnotation('command')) {
304
            return true;
305
        }
306
        // Skip anything named like an accessor ('get' or 'set')
307
        if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
308
            return false;
309
        }
310
311
        // Default to the setting of 'include all public methods'.
312
        return $includeAllPublicMethods;
313
    }
314
315
    public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
316
    {
317
        foreach ($commandInfoList as $commandInfo) {
318
            if ($commandInfo->hasAnnotation('hook')) {
319
                $this->registerCommandHook($commandInfo, $commandFileInstance);
320
            }
321
        }
322
    }
323
324
    /**
325
     * Register a command hook given the CommandInfo for a method.
326
     *
327
     * The hook format is:
328
     *
329
     *   @hook type name type
330
     *
331
     * For example, the pre-validate hook for the core:init command is:
332
     *
333
     *   @hook pre-validate core:init
334
     *
335
     * If no command name is provided, then this hook will affect every
336
     * command that is defined in the same file.
337
     *
338
     * If no hook is provided, then we will presume that ALTER_RESULT
339
     * is intended.
340
     *
341
     * @param CommandInfo $commandInfo Information about the command hook method.
342
     * @param object $commandFileInstance An instance of the CommandFile class.
343
     */
344
    public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
345
    {
346
        // Ignore if the command info has no @hook
347
        if (!$commandInfo->hasAnnotation('hook')) {
348
            return;
349
        }
350
        $hookData = $commandInfo->getAnnotation('hook');
351
        $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
352
        $commandName = $this->getNthWord($hookData, 1);
353
354
        // Register the hook
355
        $callback = [$commandFileInstance, $commandInfo->getMethodName()];
356
        $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
357
358
        // If the hook has options, then also register the commandInfo
359
        // with the hook manager, so that we can add options and such to
360
        // the commands they hook.
361
        if (!$commandInfo->options()->isEmpty()) {
362
            $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName);
363
        }
364
    }
365
366
    protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
367
    {
368
        $words = explode($delimiter, $string);
369
        if (!empty($words[$n])) {
370
            return $words[$n];
371
        }
372
        return $default;
373
    }
374
375
    public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
376
    {
377
        $this->alterCommandInfo($commandInfo, $commandFileInstance);
378
        $command = new AnnotatedCommand($commandInfo->getName());
379
        $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
380
        $command->setCommandCallback($commandCallback);
381
        $command->setCommandProcessor($this->commandProcessor);
382
        $command->setCommandInfo($commandInfo);
383
        $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo);
384
        $command->setCommandOptions($commandInfo, $automaticOptions);
385
        // Annotation commands are never bootstrap-aware, but for completeness
386
        // we will notify on every created command, as some clients may wish to
387
        // use this notification for some other purpose.
388
        $this->notify($command);
389
        return $command;
390
    }
391
392
    /**
393
     * Give plugins an opportunity to update the commandInfo
394
     */
395
    public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
396
    {
397
        foreach ($this->commandInfoAlterers as $alterer) {
398
            $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
399
        }
400
    }
401
402
    /**
403
     * Get the options that are implied by annotations, e.g. @fields implies
404
     * that there should be a --fields and a --format option.
405
     *
406
     * @return InputOption[]
407
     */
408
    public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
409
    {
410
        $automaticOptions = [];
411
        foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
412
            $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
413
        }
414
        return $automaticOptions;
415
    }
416
417
    /**
418
     * Get the options that are implied by annotations, e.g. @fields implies
419
     * that there should be a --fields and a --format option.
420
     *
421
     * @return InputOption[]
422
     */
423
    public function automaticOptions(CommandInfo $commandInfo)
424
    {
425
        $automaticOptions = [];
426
        $formatManager = $this->commandProcessor()->formatterManager();
427
        if ($formatManager) {
428
            $annotationData = $commandInfo->getAnnotations()->getArrayCopy();
429
            $formatterOptions = new FormatterOptions($annotationData);
430
            $dataType = $commandInfo->getReturnType();
431
            $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
432
        }
433
        return $automaticOptions;
434
    }
435
}
436