AnnotatedCommandFactory   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 436
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
wmc 63
lcom 1
cbo 14
dl 0
loc 436
rs 3.36
c 0
b 0
f 0

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