2 namespace Consolidation\AnnotatedCommand;
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;
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.
24 * @package Consolidation\AnnotatedCommand
26 class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
28 /** var CommandProcessor */
29 protected $commandProcessor;
31 /** var CommandCreationListenerInterface[] */
32 protected $listeners = [];
34 /** var AutomaticOptionsProvider[] */
35 protected $automaticOptionsProviderList = [];
38 protected $includeAllPublicMethods = true;
40 /** var CommandInfoAltererInterface */
41 protected $commandInfoAlterers = [];
43 /** var SimpleCacheInterface */
46 public function __construct()
48 $this->dataStore = new NullCache();
49 $this->commandProcessor = new CommandProcessor(new HookManager());
50 $this->addAutomaticOptionProvider($this);
53 public function setCommandProcessor(CommandProcessor $commandProcessor)
55 $this->commandProcessor = $commandProcessor;
60 * @return CommandProcessor
62 public function commandProcessor()
64 return $this->commandProcessor;
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.
73 public function setIncludeAllPublicMethods($includeAllPublicMethods)
75 $this->includeAllPublicMethods = $includeAllPublicMethods;
79 public function getIncludeAllPublicMethods()
81 return $this->includeAllPublicMethods;
87 public function hookManager()
89 return $this->commandProcessor()->hookManager();
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,
98 * @param CommandCreationListenerInterface $listener
100 public function addListener(CommandCreationListenerInterface $listener)
102 $this->listeners[] = $listener;
107 * Add a listener that's just a simple 'callable'.
108 * @param callable $listener
110 public function addListernerCallback(callable $listener)
112 $this->addListener(new CommandCreationListener($listener));
117 * Call all command creation listeners
119 * @param object $commandFileInstance
121 protected function notify($commandFileInstance)
123 foreach ($this->listeners as $listener) {
124 $listener->notifyCommandFileAdded($commandFileInstance);
128 public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider)
130 $this->automaticOptionsProviderList[] = $optionsProvider;
133 public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer)
135 $this->commandInfoAlterers[] = $alterer;
139 * n.b. This registers all hooks from the commandfile instance as a side-effect.
141 public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null)
143 // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
144 if (!isset($includeAllPublicMethods)) {
145 $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
147 $this->notify($commandFileInstance);
148 $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
149 $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
150 return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
153 public function getCommandInfoListFromClass($commandFileInstance)
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);
161 return $cachedCommandInfoList;
164 protected function storeCommandInfoListInCache($commandFileInstance, $commandInfoList)
166 if (!$this->hasDataStore()) {
170 $serializer = new CommandInfoSerializer();
171 foreach ($commandInfoList as $i => $commandInfo) {
172 $cache_data[$i] = $serializer->serialize($commandInfo);
174 $className = get_class($commandFileInstance);
175 $this->getDataStore()->set($className, $cache_data);
179 * Get the command info list from the cache
181 * @param mixed $commandFileInstance
184 protected function getCommandInfoListFromCache($commandFileInstance)
186 $commandInfoList = [];
187 $className = get_class($commandFileInstance);
188 if (!$this->getDataStore()->has($className)) {
191 $deserializer = new CommandInfoDeserializer();
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);
199 return $commandInfoList;
203 * Check to see if this factory has a cache datastore.
206 public function hasDataStore()
208 return !($this->dataStore instanceof NullCache);
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.
216 * TODO: Typehint this to SimpleCacheInterface
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.
221 * @param Mixed $dataStore
224 public function setDataStore($dataStore)
226 if (!($dataStore instanceof SimpleCacheInterface)) {
227 $dataStore = new CacheWrapper($dataStore);
229 $this->dataStore = $dataStore;
234 * Get the data store attached to this factory.
236 public function getDataStore()
238 return $this->dataStore;
241 protected function createCommandInfoListFromClass($classNameOrInstance, $cachedCommandInfoList)
243 $commandInfoList = [];
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);
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();
261 $commandInfoList[$commandMethodName] = $commandInfo;
265 return $commandInfoList;
268 public function createCommandInfo($classNameOrInstance, $commandMethodName)
270 return CommandInfo::create($classNameOrInstance, $commandMethodName);
273 public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
275 // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
276 if (!isset($includeAllPublicMethods)) {
277 $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
279 return $this->createSelectedCommandsFromClassInfo(
281 $commandFileInstance,
282 function ($commandInfo) use ($includeAllPublicMethods) {
283 return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
288 public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
290 $commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector);
292 function ($commandInfo) use ($commandFileInstance) {
293 return $this->createCommand($commandInfo, $commandFileInstance);
299 protected function filterCommandInfoList($commandInfoList, callable $commandSelector)
301 return array_filter($commandInfoList, $commandSelector);
304 public static function isCommandOrHookMethod($commandInfo, $includeAllPublicMethods)
306 return static::isHookMethod($commandInfo) || static::isCommandMethod($commandInfo, $includeAllPublicMethods);
309 public static function isHookMethod($commandInfo)
311 return $commandInfo->hasAnnotation('hook');
314 public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
316 // Ignore everything labeled @hook
317 if (static::isHookMethod($commandInfo)) {
320 // Include everything labeled @command
321 if ($commandInfo->hasAnnotation('command')) {
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)) {
329 // Skip anything named like an accessor ('get' or 'set')
330 if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
334 // Default to the setting of 'include all public methods'.
335 return $includeAllPublicMethods;
338 public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
340 foreach ($commandInfoList as $commandInfo) {
341 if (static::isHookMethod($commandInfo)) {
342 $this->registerCommandHook($commandInfo, $commandFileInstance);
348 * Register a command hook given the CommandInfo for a method.
350 * The hook format is:
352 * @hook type name type
354 * For example, the pre-validate hook for the core:init command is:
356 * @hook pre-validate core:init
358 * If no command name is provided, then this hook will affect every
359 * command that is defined in the same file.
361 * If no hook is provided, then we will presume that ALTER_RESULT
364 * @param CommandInfo $commandInfo Information about the command hook method.
365 * @param object $commandFileInstance An instance of the CommandFile class.
367 public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
369 // Ignore if the command info has no @hook
370 if (!static::isHookMethod($commandInfo)) {
373 $hookData = $commandInfo->getAnnotation('hook');
374 $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
375 $commandName = $this->getNthWord($hookData, 1);
378 $callback = [$commandFileInstance, $commandInfo->getMethodName()];
379 $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
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);
389 protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
391 $words = explode($delimiter, $string);
392 if (!empty($words[$n])) {
398 public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
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);
416 * Give plugins an opportunity to update the commandInfo
418 public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
420 foreach ($this->commandInfoAlterers as $alterer) {
421 $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
426 * Get the options that are implied by annotations, e.g. @fields implies
427 * that there should be a --fields and a --format option.
429 * @return InputOption[]
431 public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
433 $automaticOptions = [];
434 foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
435 $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
437 return $automaticOptions;
441 * Get the options that are implied by annotations, e.g. @fields implies
442 * that there should be a --fields and a --format option.
444 * @return InputOption[]
446 public function automaticOptions(CommandInfo $commandInfo)
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);
456 return $automaticOptions;