Completed
Pull Request — master (#81)
by Greg
02:10
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)) {
226
            $dataStore = new CacheWrapper($dataStore);
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