687acd5ca97ba6a1c8bb22682809b6aa76201a9f
[yaffs-website] / vendor / consolidation / annotated-command / src / AnnotatedCommandFactory.php
1 <?php
2 namespace Consolidation\AnnotatedCommand;
3
4 use Consolidation\AnnotatedCommand\Cache\CacheWrapper;
5 use Consolidation\AnnotatedCommand\Cache\NullCache;
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\AnnotatedCommand\Parser\CommandInfoDeserializer;
11 use Consolidation\AnnotatedCommand\Parser\CommandInfoSerializer;
12 use Consolidation\OutputFormatters\Options\FormatterOptions;
13 use Symfony\Component\Console\Command\Command;
14 use Symfony\Component\Console\Input\InputInterface;
15 use Symfony\Component\Console\Output\OutputInterface;
16
17 /**
18  * The AnnotatedCommandFactory creates commands for your application.
19  * Use with a Dependency Injection Container and the CommandFactory.
20  * Alternately, use the CommandFileDiscovery to find commandfiles, and
21  * then use AnnotatedCommandFactory::createCommandsFromClass() to create
22  * commands.  See the README for more information.
23  *
24  * @package Consolidation\AnnotatedCommand
25  */
26 class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
27 {
28     /** var CommandProcessor */
29     protected $commandProcessor;
30
31     /** var CommandCreationListenerInterface[] */
32     protected $listeners = [];
33
34     /** var AutomaticOptionsProvider[] */
35     protected $automaticOptionsProviderList = [];
36
37     /** var boolean */
38     protected $includeAllPublicMethods = true;
39
40     /** var CommandInfoAltererInterface */
41     protected $commandInfoAlterers = [];
42
43     /** var SimpleCacheInterface */
44     protected $dataStore;
45
46     public function __construct()
47     {
48         $this->dataStore = new NullCache();
49         $this->commandProcessor = new CommandProcessor(new HookManager());
50         $this->addAutomaticOptionProvider($this);
51     }
52
53     public function setCommandProcessor(CommandProcessor $commandProcessor)
54     {
55         $this->commandProcessor = $commandProcessor;
56         return $this;
57     }
58
59     /**
60      * @return CommandProcessor
61      */
62     public function commandProcessor()
63     {
64         return $this->commandProcessor;
65     }
66
67     /**
68      * Set the 'include all public methods flag'. If true (the default), then
69      * every public method of each commandFile will be used to create commands.
70      * If it is false, then only those public methods annotated with @command
71      * or @name (deprecated) will be used to create commands.
72      */
73     public function setIncludeAllPublicMethods($includeAllPublicMethods)
74     {
75         $this->includeAllPublicMethods = $includeAllPublicMethods;
76         return $this;
77     }
78
79     public function getIncludeAllPublicMethods()
80     {
81         return $this->includeAllPublicMethods;
82     }
83
84     /**
85      * @return HookManager
86      */
87     public function hookManager()
88     {
89         return $this->commandProcessor()->hookManager();
90     }
91
92     /**
93      * Add a listener that is notified immediately before the command
94      * factory creates commands from a commandFile instance.  This
95      * listener can use this opportunity to do more setup for the commandFile,
96      * and so on.
97      *
98      * @param CommandCreationListenerInterface $listener
99      */
100     public function addListener(CommandCreationListenerInterface $listener)
101     {
102         $this->listeners[] = $listener;
103         return $this;
104     }
105
106     /**
107      * Add a listener that's just a simple 'callable'.
108      * @param callable $listener
109      */
110     public function addListernerCallback(callable $listener)
111     {
112         $this->addListener(new CommandCreationListener($listener));
113         return $this;
114     }
115
116     /**
117      * Call all command creation listeners
118      *
119      * @param object $commandFileInstance
120      */
121     protected function notify($commandFileInstance)
122     {
123         foreach ($this->listeners as $listener) {
124             $listener->notifyCommandFileAdded($commandFileInstance);
125         }
126     }
127
128     public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider)
129     {
130         $this->automaticOptionsProviderList[] = $optionsProvider;
131     }
132
133     public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer)
134     {
135         $this->commandInfoAlterers[] = $alterer;
136     }
137
138     /**
139      * n.b. This registers all hooks from the commandfile instance as a side-effect.
140      */
141     public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null)
142     {
143         // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
144         if (!isset($includeAllPublicMethods)) {
145             $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
146         }
147         $this->notify($commandFileInstance);
148         $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
149         $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
150         return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
151     }
152
153     public function getCommandInfoListFromClass($commandFileInstance)
154     {
155         $cachedCommandInfoList = $this->getCommandInfoListFromCache($commandFileInstance);
156         $commandInfoList = $this->createCommandInfoListFromClass($commandFileInstance, $cachedCommandInfoList);
157         if (!empty($commandInfoList)) {
158             $cachedCommandInfoList = array_merge($commandInfoList, $cachedCommandInfoList);
159             $this->storeCommandInfoListInCache($commandFileInstance, $cachedCommandInfoList);
160         }
161         return $cachedCommandInfoList;
162     }
163
164     protected function storeCommandInfoListInCache($commandFileInstance, $commandInfoList)
165     {
166         if (!$this->hasDataStore()) {
167             return;
168         }
169         $cache_data = [];
170         $serializer = new CommandInfoSerializer();
171         foreach ($commandInfoList as $i => $commandInfo) {
172             $cache_data[$i] = $serializer->serialize($commandInfo);
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         $deserializer = new CommandInfoDeserializer();
192
193         $cache_data = $this->getDataStore()->get($className);
194         foreach ($cache_data as $i => $data) {
195             if (CommandInfoDeserializer::isValidSerializedData((array)$data)) {
196                 $commandInfoList[$i] = $deserializer->deserialize((array)$data);
197             }
198         }
199         return $commandInfoList;
200     }
201
202     /**
203      * Check to see if this factory has a cache datastore.
204      * @return boolean
205      */
206     public function hasDataStore()
207     {
208         return !($this->dataStore instanceof NullCache);
209     }
210
211     /**
212      * Set a cache datastore for this factory. Any object with 'set' and
213      * 'get' methods is acceptable. The key is the classname being cached,
214      * and the value is a nested associative array of strings.
215      *
216      * TODO: Typehint this to SimpleCacheInterface
217      *
218      * This is not done currently to allow clients to use a generic cache
219      * store that does not itself depend on the annotated-command library.
220      *
221      * @param Mixed $dataStore
222      * @return type
223      */
224     public function setDataStore($dataStore)
225     {
226         if (!($dataStore instanceof SimpleCacheInterface)) {
227             $dataStore = new CacheWrapper($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, $cachedCommandInfoList)
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             if (!array_key_exists($commandMethodName, $cachedCommandInfoList)) {
256                 $commandInfo = CommandInfo::create($classNameOrInstance, $commandMethodName);
257                 if (!static::isCommandOrHookMethod($commandInfo, $this->getIncludeAllPublicMethods())) {
258                     $commandInfo->invalidate();
259                 }
260                 $commandInfoList[$commandMethodName] =  $commandInfo;
261             }
262         }
263
264         return $commandInfoList;
265     }
266
267     public function createCommandInfo($classNameOrInstance, $commandMethodName)
268     {
269         return CommandInfo::create($classNameOrInstance, $commandMethodName);
270     }
271
272     public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
273     {
274         // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
275         if (!isset($includeAllPublicMethods)) {
276             $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
277         }
278         return $this->createSelectedCommandsFromClassInfo(
279             $commandInfoList,
280             $commandFileInstance,
281             function ($commandInfo) use ($includeAllPublicMethods) {
282                 return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
283             }
284         );
285     }
286
287     public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
288     {
289         $commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector);
290         return array_map(
291             function ($commandInfo) use ($commandFileInstance) {
292                 return $this->createCommand($commandInfo, $commandFileInstance);
293             },
294             $commandInfoList
295         );
296     }
297
298     protected function filterCommandInfoList($commandInfoList, callable $commandSelector)
299     {
300         return array_filter($commandInfoList, $commandSelector);
301     }
302
303     public static function isCommandOrHookMethod($commandInfo, $includeAllPublicMethods)
304     {
305         return static::isHookMethod($commandInfo) || static::isCommandMethod($commandInfo, $includeAllPublicMethods);
306     }
307
308     public static function isHookMethod($commandInfo)
309     {
310         return $commandInfo->hasAnnotation('hook');
311     }
312
313     public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
314     {
315         // Ignore everything labeled @hook
316         if (static::isHookMethod($commandInfo)) {
317             return false;
318         }
319         // Include everything labeled @command
320         if ($commandInfo->hasAnnotation('command')) {
321             return true;
322         }
323         // Skip anything named like an accessor ('get' or 'set')
324         if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
325             return false;
326         }
327
328         // Default to the setting of 'include all public methods'.
329         return $includeAllPublicMethods;
330     }
331
332     public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
333     {
334         foreach ($commandInfoList as $commandInfo) {
335             if (static::isHookMethod($commandInfo)) {
336                 $this->registerCommandHook($commandInfo, $commandFileInstance);
337             }
338         }
339     }
340
341     /**
342      * Register a command hook given the CommandInfo for a method.
343      *
344      * The hook format is:
345      *
346      *   @hook type name type
347      *
348      * For example, the pre-validate hook for the core:init command is:
349      *
350      *   @hook pre-validate core:init
351      *
352      * If no command name is provided, then this hook will affect every
353      * command that is defined in the same file.
354      *
355      * If no hook is provided, then we will presume that ALTER_RESULT
356      * is intended.
357      *
358      * @param CommandInfo $commandInfo Information about the command hook method.
359      * @param object $commandFileInstance An instance of the CommandFile class.
360      */
361     public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
362     {
363         // Ignore if the command info has no @hook
364         if (!static::isHookMethod($commandInfo)) {
365             return;
366         }
367         $hookData = $commandInfo->getAnnotation('hook');
368         $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
369         $commandName = $this->getNthWord($hookData, 1);
370
371         // Register the hook
372         $callback = [$commandFileInstance, $commandInfo->getMethodName()];
373         $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
374
375         // If the hook has options, then also register the commandInfo
376         // with the hook manager, so that we can add options and such to
377         // the commands they hook.
378         if (!$commandInfo->options()->isEmpty()) {
379             $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName);
380         }
381     }
382
383     protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
384     {
385         $words = explode($delimiter, $string);
386         if (!empty($words[$n])) {
387             return $words[$n];
388         }
389         return $default;
390     }
391
392     public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
393     {
394         $this->alterCommandInfo($commandInfo, $commandFileInstance);
395         $command = new AnnotatedCommand($commandInfo->getName());
396         $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
397         $command->setCommandCallback($commandCallback);
398         $command->setCommandProcessor($this->commandProcessor);
399         $command->setCommandInfo($commandInfo);
400         $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo);
401         $command->setCommandOptions($commandInfo, $automaticOptions);
402         // Annotation commands are never bootstrap-aware, but for completeness
403         // we will notify on every created command, as some clients may wish to
404         // use this notification for some other purpose.
405         $this->notify($command);
406         return $command;
407     }
408
409     /**
410      * Give plugins an opportunity to update the commandInfo
411      */
412     public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
413     {
414         foreach ($this->commandInfoAlterers as $alterer) {
415             $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
416         }
417     }
418
419     /**
420      * Get the options that are implied by annotations, e.g. @fields implies
421      * that there should be a --fields and a --format option.
422      *
423      * @return InputOption[]
424      */
425     public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
426     {
427         $automaticOptions = [];
428         foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
429             $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
430         }
431         return $automaticOptions;
432     }
433
434     /**
435      * Get the options that are implied by annotations, e.g. @fields implies
436      * that there should be a --fields and a --format option.
437      *
438      * @return InputOption[]
439      */
440     public function automaticOptions(CommandInfo $commandInfo)
441     {
442         $automaticOptions = [];
443         $formatManager = $this->commandProcessor()->formatterManager();
444         if ($formatManager) {
445             $annotationData = $commandInfo->getAnnotations()->getArrayCopy();
446             $formatterOptions = new FormatterOptions($annotationData);
447             $dataType = $commandInfo->getReturnType();
448             $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
449         }
450         return $automaticOptions;
451     }
452 }