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) ?: [],
250 return !preg_match('#^_#', $m);
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();
260 $commandInfoList[$commandMethodName] = $commandInfo;
264 return $commandInfoList;
267 public function createCommandInfo($classNameOrInstance, $commandMethodName)
269 return CommandInfo::create($classNameOrInstance, $commandMethodName);
272 public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
274 // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
275 if (!isset($includeAllPublicMethods)) {
276 $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
278 return $this->createSelectedCommandsFromClassInfo(
280 $commandFileInstance,
281 function ($commandInfo) use ($includeAllPublicMethods) {
282 return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
287 public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
289 $commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector);
291 function ($commandInfo) use ($commandFileInstance) {
292 return $this->createCommand($commandInfo, $commandFileInstance);
298 protected function filterCommandInfoList($commandInfoList, callable $commandSelector)
300 return array_filter($commandInfoList, $commandSelector);
303 public static function isCommandOrHookMethod($commandInfo, $includeAllPublicMethods)
305 return static::isHookMethod($commandInfo) || static::isCommandMethod($commandInfo, $includeAllPublicMethods);
308 public static function isHookMethod($commandInfo)
310 return $commandInfo->hasAnnotation('hook');
313 public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
315 // Ignore everything labeled @hook
316 if (static::isHookMethod($commandInfo)) {
319 // Include everything labeled @command
320 if ($commandInfo->hasAnnotation('command')) {
323 // Skip anything named like an accessor ('get' or 'set')
324 if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
328 // Default to the setting of 'include all public methods'.
329 return $includeAllPublicMethods;
332 public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
334 foreach ($commandInfoList as $commandInfo) {
335 if (static::isHookMethod($commandInfo)) {
336 $this->registerCommandHook($commandInfo, $commandFileInstance);
342 * Register a command hook given the CommandInfo for a method.
344 * The hook format is:
346 * @hook type name type
348 * For example, the pre-validate hook for the core:init command is:
350 * @hook pre-validate core:init
352 * If no command name is provided, then this hook will affect every
353 * command that is defined in the same file.
355 * If no hook is provided, then we will presume that ALTER_RESULT
358 * @param CommandInfo $commandInfo Information about the command hook method.
359 * @param object $commandFileInstance An instance of the CommandFile class.
361 public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
363 // Ignore if the command info has no @hook
364 if (!static::isHookMethod($commandInfo)) {
367 $hookData = $commandInfo->getAnnotation('hook');
368 $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
369 $commandName = $this->getNthWord($hookData, 1);
372 $callback = [$commandFileInstance, $commandInfo->getMethodName()];
373 $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
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);
383 protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
385 $words = explode($delimiter, $string);
386 if (!empty($words[$n])) {
392 public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
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);
410 * Give plugins an opportunity to update the commandInfo
412 public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
414 foreach ($this->commandInfoAlterers as $alterer) {
415 $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
420 * Get the options that are implied by annotations, e.g. @fields implies
421 * that there should be a --fields and a --format option.
423 * @return InputOption[]
425 public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
427 $automaticOptions = [];
428 foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
429 $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
431 return $automaticOptions;
435 * Get the options that are implied by annotations, e.g. @fields implies
436 * that there should be a --fields and a --format option.
438 * @return InputOption[]
440 public function automaticOptions(CommandInfo $commandInfo)
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);
450 return $automaticOptions;