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