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

getCommandInfoListFromCache()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 15
rs 9.2
cc 4
eloc 10
nc 4
nop 1
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
        $commandInfoList = $this->getCommandInfoListFromCache($commandFileInstance);
154
        if (!empty($commandInfoList)) {
155
            return $commandInfoList;
156
        }
157
        $commandInfoList = $this->createCommandInfoListFromClass($commandFileInstance);
158
        $this->storeCommandInfoListInCache($commandFileInstance, $commandInfoList);
159
160
        return $commandInfoList;
161
    }
162
163
    protected function storeCommandInfoListInCache($commandFileInstance, $commandInfoList)
164
    {
165
        if (!$this->hasDataStore()) {
166
            return;
167
        }
168
        $cache_data = [];
169
        $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
170
        foreach ($commandInfoList as $i => $commandInfo) {
171
            if (static::isCommandMethod($commandInfo, $includeAllPublicMethods)) {
172
                $cache_data[$i] = $commandInfo->serialize();
173
            }
174
        }
175
        $className = get_class($commandFileInstance);
176
        $this->getDataStore()->set($className, $cache_data);
177
    }
178
179
    /**
180
     * Get the command info list from the cache
181
     *
182
     * @param mixed $commandFileInstance
183
     * @return array
184
     */
185
    protected function getCommandInfoListFromCache($commandFileInstance)
186
    {
187
        $commandInfoList = [];
188
        $className = get_class($commandFileInstance);
189
        if (!$this->getDataStore()->has($className)) {
190
            return [];
191
        }
192
        $cache_data = $this->getDataStore()->get($className);
193
        foreach ($cache_data as $i => $data) {
194
            if (CommandInfo::isValidSerializedData((array)$data)) {
195
                $commandInfoList[$i] = CommandInfo::deserialize((array)$data);
196
            }
197
        }
198
        return $commandInfoList;
199
    }
200
201
    /**
202
     * Check to see if this factory has a cache datastore.
203
     * @return boolean
204
     */
205
    public function hasDataStore()
206
    {
207
        return !($this->dataStore instanceof NullCache);
208
    }
209
210
    /**
211
     * Set a cache datastore for this factory. Any object with 'set' and
212
     * 'get' methods is acceptable. The key is the classname being cached,
213
     * and the value is a nested associative array of strings.
214
     *
215
     * TODO: Typehint this to SimpleCacheInterface
216
     *
217
     * This is not done currently to allow clients to use a generic cache
218
     * store that does not itself depend on the annotated-command library.
219
     *
220
     * @param Mixed $dataStore
221
     * @return type
222
     */
223
    public function setDataStore($dataStore)
224
    {
225
        if (!($datastore instanceof SimpleCacheInterface)) {
0 ignored issues
show
Bug introduced by
The variable $datastore seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
226
            $datastore = new CacheWrapper($datastore);
0 ignored issues
show
Bug introduced by
The variable $datastore seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Unused Code introduced by
$datastore is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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