Completed
Push — master ( 80d1a0...6a89b8 )
by Greg
02:11
created

AnnotatedCommandFactory   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 426
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 60
c 1
b 0
f 0
lcom 1
cbo 12
dl 0
loc 426
rs 6.0975

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 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 24 5
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
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::isCommandOrHookMethod($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
                if (static::isCommandOrHookMethod($commandInfo, $this->getIncludeAllPublicMethods())) {
256
                    $commandInfoList[$commandMethodName] =  $commandInfo;
257
                }
258
            }
259
        }
260
261
        return $commandInfoList;
262
    }
263
264
    public function createCommandInfo($classNameOrInstance, $commandMethodName)
265
    {
266
        return CommandInfo::create($classNameOrInstance, $commandMethodName);
267
    }
268
269
    public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
270
    {
271
        // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
272
        if (!isset($includeAllPublicMethods)) {
273
            $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
274
        }
275
        return $this->createSelectedCommandsFromClassInfo(
276
            $commandInfoList,
277
            $commandFileInstance,
278
            function ($commandInfo) use ($includeAllPublicMethods) {
279
                return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
280
            }
281
        );
282
    }
283
284
    public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
285
    {
286
        $commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector);
287
        return array_map(
288
            function ($commandInfo) use ($commandFileInstance) {
289
                return $this->createCommand($commandInfo, $commandFileInstance);
290
            },
291
            $commandInfoList
292
        );
293
    }
294
295
    protected function filterCommandInfoList($commandInfoList, callable $commandSelector)
296
    {
297
        return array_filter($commandInfoList, $commandSelector);
298
    }
299
300
    public static function isCommandOrHookMethod($commandInfo, $includeAllPublicMethods)
301
    {
302
        return static::isHookMethod($commandInfo) || static::isCommandMethod($commandInfo, $includeAllPublicMethods);
303
    }
304
305
    public static function isHookMethod($commandInfo)
306
    {
307
        return $commandInfo->hasAnnotation('hook');
308
    }
309
310
    public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
311
    {
312
        // Ignore everything labeled @hook
313
        if (static::isHookMethod($commandInfo)) {
314
            return false;
315
        }
316
        // Include everything labeled @command
317
        if ($commandInfo->hasAnnotation('command')) {
318
            return true;
319
        }
320
        // Skip anything named like an accessor ('get' or 'set')
321
        if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
322
            return false;
323
        }
324
325
        // Default to the setting of 'include all public methods'.
326
        return $includeAllPublicMethods;
327
    }
328
329
    public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
330
    {
331
        foreach ($commandInfoList as $commandInfo) {
332
            if (static::isHookMethod($commandInfo)) {
333
                $this->registerCommandHook($commandInfo, $commandFileInstance);
334
            }
335
        }
336
    }
337
338
    /**
339
     * Register a command hook given the CommandInfo for a method.
340
     *
341
     * The hook format is:
342
     *
343
     *   @hook type name type
344
     *
345
     * For example, the pre-validate hook for the core:init command is:
346
     *
347
     *   @hook pre-validate core:init
348
     *
349
     * If no command name is provided, then this hook will affect every
350
     * command that is defined in the same file.
351
     *
352
     * If no hook is provided, then we will presume that ALTER_RESULT
353
     * is intended.
354
     *
355
     * @param CommandInfo $commandInfo Information about the command hook method.
356
     * @param object $commandFileInstance An instance of the CommandFile class.
357
     */
358
    public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
359
    {
360
        // Ignore if the command info has no @hook
361
        if (!static::isHookMethod($commandInfo)) {
362
            return;
363
        }
364
        $hookData = $commandInfo->getAnnotation('hook');
365
        $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
366
        $commandName = $this->getNthWord($hookData, 1);
367
368
        // Register the hook
369
        $callback = [$commandFileInstance, $commandInfo->getMethodName()];
370
        $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
371
372
        // If the hook has options, then also register the commandInfo
373
        // with the hook manager, so that we can add options and such to
374
        // the commands they hook.
375
        if (!$commandInfo->options()->isEmpty()) {
376
            $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName);
377
        }
378
    }
379
380
    protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
381
    {
382
        $words = explode($delimiter, $string);
383
        if (!empty($words[$n])) {
384
            return $words[$n];
385
        }
386
        return $default;
387
    }
388
389
    public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
390
    {
391
        $this->alterCommandInfo($commandInfo, $commandFileInstance);
392
        $command = new AnnotatedCommand($commandInfo->getName());
393
        $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
394
        $command->setCommandCallback($commandCallback);
395
        $command->setCommandProcessor($this->commandProcessor);
396
        $command->setCommandInfo($commandInfo);
397
        $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo);
398
        $command->setCommandOptions($commandInfo, $automaticOptions);
399
        // Annotation commands are never bootstrap-aware, but for completeness
400
        // we will notify on every created command, as some clients may wish to
401
        // use this notification for some other purpose.
402
        $this->notify($command);
403
        return $command;
404
    }
405
406
    /**
407
     * Give plugins an opportunity to update the commandInfo
408
     */
409
    public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
410
    {
411
        foreach ($this->commandInfoAlterers as $alterer) {
412
            $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
413
        }
414
    }
415
416
    /**
417
     * Get the options that are implied by annotations, e.g. @fields implies
418
     * that there should be a --fields and a --format option.
419
     *
420
     * @return InputOption[]
421
     */
422
    public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
423
    {
424
        $automaticOptions = [];
425
        foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
426
            $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
427
        }
428
        return $automaticOptions;
429
    }
430
431
    /**
432
     * Get the options that are implied by annotations, e.g. @fields implies
433
     * that there should be a --fields and a --format option.
434
     *
435
     * @return InputOption[]
436
     */
437
    public function automaticOptions(CommandInfo $commandInfo)
438
    {
439
        $automaticOptions = [];
440
        $formatManager = $this->commandProcessor()->formatterManager();
441
        if ($formatManager) {
442
            $annotationData = $commandInfo->getAnnotations()->getArrayCopy();
443
            $formatterOptions = new FormatterOptions($annotationData);
444
            $dataType = $commandInfo->getReturnType();
445
            $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
446
        }
447
        return $automaticOptions;
448
    }
449
}
450