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

getCommandInfoListFromCache()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 29
rs 5.3846
cc 8
eloc 16
nc 8
nop 1
1
<?php
2
namespace Consolidation\AnnotatedCommand;
3
4
use Consolidation\AnnotatedCommand\Hooks\HookManager;
5
use Consolidation\AnnotatedCommand\Options\AutomaticOptionsProviderInterface;
6
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
7
use Consolidation\OutputFormatters\Options\FormatterOptions;
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Output\OutputInterface;
11
12
/**
13
 * The AnnotatedCommandFactory creates commands for your application.
14
 * Use with a Dependency Injection Container and the CommandFactory.
15
 * Alternately, use the CommandFileDiscovery to find commandfiles, and
16
 * then use AnnotatedCommandFactory::createCommandsFromClass() to create
17
 * commands.  See the README for more information.
18
 *
19
 * @package Consolidation\AnnotatedCommand
20
 */
21
class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
22
{
23
    /** var CommandProcessor */
24
    protected $commandProcessor;
25
26
    /** var CommandCreationListenerInterface[] */
27
    protected $listeners = [];
28
29
    /** var AutomaticOptionsProvider[] */
30
31
    protected $automaticOptionsProviderList = [];
32
33
    /** var boolean */
34
    protected $includeAllPublicMethods = true;
35
36
    /** var CommandInfoAltererInterface */
37
    protected $commandInfoAlterers = [];
38
39
    protected $dataStore;
40
41
    public function __construct()
42
    {
43
        $this->commandProcessor = new CommandProcessor(new HookManager());
44
        $this->addAutomaticOptionProvider($this);
45
    }
46
47
    public function setCommandProcessor(CommandProcessor $commandProcessor)
48
    {
49
        $this->commandProcessor = $commandProcessor;
50
        return $this;
51
    }
52
53
    /**
54
     * @return CommandProcessor
55
     */
56
    public function commandProcessor()
57
    {
58
        return $this->commandProcessor;
59
    }
60
61
    /**
62
     * Set the 'include all public methods flag'. If true (the default), then
63
     * every public method of each commandFile will be used to create commands.
64
     * If it is false, then only those public methods annotated with @command
65
     * or @name (deprecated) will be used to create commands.
66
     */
67
    public function setIncludeAllPublicMethods($includeAllPublicMethods)
68
    {
69
        $this->includeAllPublicMethods = $includeAllPublicMethods;
70
        return $this;
71
    }
72
73
    public function getIncludeAllPublicMethods()
74
    {
75
        return $this->includeAllPublicMethods;
76
    }
77
78
    /**
79
     * @return HookManager
80
     */
81
    public function hookManager()
82
    {
83
        return $this->commandProcessor()->hookManager();
84
    }
85
86
    /**
87
     * Add a listener that is notified immediately before the command
88
     * factory creates commands from a commandFile instance.  This
89
     * listener can use this opportunity to do more setup for the commandFile,
90
     * and so on.
91
     *
92
     * @param CommandCreationListenerInterface $listener
93
     */
94
    public function addListener(CommandCreationListenerInterface $listener)
95
    {
96
        $this->listeners[] = $listener;
97
        return $this;
98
    }
99
100
    /**
101
     * Add a listener that's just a simple 'callable'.
102
     * @param callable $listener
103
     */
104
    public function addListernerCallback(callable $listener)
105
    {
106
        $this->addListener(new CommandCreationListener($listener));
107
        return $this;
108
    }
109
110
    /**
111
     * Call all command creation listeners
112
     *
113
     * @param object $commandFileInstance
114
     */
115
    protected function notify($commandFileInstance)
116
    {
117
        foreach ($this->listeners as $listener) {
118
            $listener->notifyCommandFileAdded($commandFileInstance);
119
        }
120
    }
121
122
    public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider)
123
    {
124
        $this->automaticOptionsProviderList[] = $optionsProvider;
125
    }
126
127
    public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer)
128
    {
129
        $this->commandInfoAlterers[] = $alterer;
130
    }
131
132
    /**
133
     * n.b. This registers all hooks from the commandfile instance as a side-effect.
134
     */
135
    public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null)
136
    {
137
        // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
138
        if (!isset($includeAllPublicMethods)) {
139
            $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
140
        }
141
        $this->notify($commandFileInstance);
142
        $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
143
        $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
144
        return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
145
    }
146
147
    public function getCommandInfoListFromClass($commandFileInstance)
148
    {
149
        $commandInfoList = $this->getCommandInfoListFromCache($commandFileInstance);
150
        if (!empty($commandInfoList)) {
151
            return $commandInfoList;
152
        }
153
        $commandInfoList = $this->createCommandInfoListFromClass($commandFileInstance);
154
        $this->storeCommandInfoListInCache($commandFileInstance, $commandInfoList);
155
156
        return $commandInfoList;
157
    }
158
159
    protected function storeCommandInfoListInCache($commandFileInstance, $commandInfoList)
160
    {
161
        if (!$this->hasDataStore()) {
162
            return;
163
        }
164
        $cache_data = [];
165
        $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
166
        foreach ($commandInfoList as $i => $commandInfo) {
167
            if (static::isCommandMethod($commandInfo, $includeAllPublicMethods)) {
168
                $cache_data[$i] = $commandInfo->serialize();
169
            }
170
        }
171
        $className = get_class($commandFileInstance);
172
        $this->getDataStore()->set($className, $cache_data);
173
    }
174
175
    protected function getCommandInfoListFromCache($commandFileInstance)
176
    {
177
        if (!$this->hasDataStore()) {
178
            return [];
179
        }
180
        $className = get_class($commandFileInstance);
181
        // TODO: Once we typehint our data store as a SimpleCacheInterface,
182
        // then we will not need to use 'method_exists' here.
183
        // If the data store has a 'has' method, then we will use it.
184
        // This allows the data store to throw on get if they key does not
185
        // exist, if desired.
186
        if (method_exists($this->getDataStore(), 'has') && !$this->getDataStore()->has($className)) {
187
            return [];
188
        }
189
        $cache_data = (array) $this->getDataStore()->get($className);
190
        if (!$cache_data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cache_data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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