Completed
Push — master ( 7a5d54...ee8e05 )
by Greg
02:43
created

AnnotatedCommandFactory::createCommand()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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