--- /dev/null
+<?php
+namespace Consolidation\AnnotatedCommand;
+
+use Consolidation\AnnotatedCommand\Cache\CacheWrapper;
+use Consolidation\AnnotatedCommand\Cache\NullCache;
+use Consolidation\AnnotatedCommand\Cache\SimpleCacheInterface;
+use Consolidation\AnnotatedCommand\Hooks\HookManager;
+use Consolidation\AnnotatedCommand\Options\AutomaticOptionsProviderInterface;
+use Consolidation\AnnotatedCommand\Parser\CommandInfo;
+use Consolidation\AnnotatedCommand\Parser\CommandInfoDeserializer;
+use Consolidation\AnnotatedCommand\Parser\CommandInfoSerializer;
+use Consolidation\OutputFormatters\Options\FormatterOptions;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * The AnnotatedCommandFactory creates commands for your application.
+ * Use with a Dependency Injection Container and the CommandFactory.
+ * Alternately, use the CommandFileDiscovery to find commandfiles, and
+ * then use AnnotatedCommandFactory::createCommandsFromClass() to create
+ * commands. See the README for more information.
+ *
+ * @package Consolidation\AnnotatedCommand
+ */
+class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
+{
+ /** var CommandProcessor */
+ protected $commandProcessor;
+
+ /** var CommandCreationListenerInterface[] */
+ protected $listeners = [];
+
+ /** var AutomaticOptionsProvider[] */
+ protected $automaticOptionsProviderList = [];
+
+ /** var boolean */
+ protected $includeAllPublicMethods = true;
+
+ /** var CommandInfoAltererInterface */
+ protected $commandInfoAlterers = [];
+
+ /** var SimpleCacheInterface */
+ protected $dataStore;
+
+ public function __construct()
+ {
+ $this->dataStore = new NullCache();
+ $this->commandProcessor = new CommandProcessor(new HookManager());
+ $this->addAutomaticOptionProvider($this);
+ }
+
+ public function setCommandProcessor(CommandProcessor $commandProcessor)
+ {
+ $this->commandProcessor = $commandProcessor;
+ return $this;
+ }
+
+ /**
+ * @return CommandProcessor
+ */
+ public function commandProcessor()
+ {
+ return $this->commandProcessor;
+ }
+
+ /**
+ * Set the 'include all public methods flag'. If true (the default), then
+ * every public method of each commandFile will be used to create commands.
+ * If it is false, then only those public methods annotated with @command
+ * or @name (deprecated) will be used to create commands.
+ */
+ public function setIncludeAllPublicMethods($includeAllPublicMethods)
+ {
+ $this->includeAllPublicMethods = $includeAllPublicMethods;
+ return $this;
+ }
+
+ public function getIncludeAllPublicMethods()
+ {
+ return $this->includeAllPublicMethods;
+ }
+
+ /**
+ * @return HookManager
+ */
+ public function hookManager()
+ {
+ return $this->commandProcessor()->hookManager();
+ }
+
+ /**
+ * Add a listener that is notified immediately before the command
+ * factory creates commands from a commandFile instance. This
+ * listener can use this opportunity to do more setup for the commandFile,
+ * and so on.
+ *
+ * @param CommandCreationListenerInterface $listener
+ */
+ public function addListener(CommandCreationListenerInterface $listener)
+ {
+ $this->listeners[] = $listener;
+ return $this;
+ }
+
+ /**
+ * Add a listener that's just a simple 'callable'.
+ * @param callable $listener
+ */
+ public function addListernerCallback(callable $listener)
+ {
+ $this->addListener(new CommandCreationListener($listener));
+ return $this;
+ }
+
+ /**
+ * Call all command creation listeners
+ *
+ * @param object $commandFileInstance
+ */
+ protected function notify($commandFileInstance)
+ {
+ foreach ($this->listeners as $listener) {
+ $listener->notifyCommandFileAdded($commandFileInstance);
+ }
+ }
+
+ public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider)
+ {
+ $this->automaticOptionsProviderList[] = $optionsProvider;
+ }
+
+ public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer)
+ {
+ $this->commandInfoAlterers[] = $alterer;
+ }
+
+ /**
+ * n.b. This registers all hooks from the commandfile instance as a side-effect.
+ */
+ public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null)
+ {
+ // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
+ if (!isset($includeAllPublicMethods)) {
+ $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
+ }
+ $this->notify($commandFileInstance);
+ $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
+ $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
+ return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
+ }
+
+ public function getCommandInfoListFromClass($commandFileInstance)
+ {
+ $cachedCommandInfoList = $this->getCommandInfoListFromCache($commandFileInstance);
+ $commandInfoList = $this->createCommandInfoListFromClass($commandFileInstance, $cachedCommandInfoList);
+ if (!empty($commandInfoList)) {
+ $cachedCommandInfoList = array_merge($commandInfoList, $cachedCommandInfoList);
+ $this->storeCommandInfoListInCache($commandFileInstance, $cachedCommandInfoList);
+ }
+ return $cachedCommandInfoList;
+ }
+
+ protected function storeCommandInfoListInCache($commandFileInstance, $commandInfoList)
+ {
+ if (!$this->hasDataStore()) {
+ return;
+ }
+ $cache_data = [];
+ $serializer = new CommandInfoSerializer();
+ foreach ($commandInfoList as $i => $commandInfo) {
+ $cache_data[$i] = $serializer->serialize($commandInfo);
+ }
+ $className = get_class($commandFileInstance);
+ $this->getDataStore()->set($className, $cache_data);
+ }
+
+ /**
+ * Get the command info list from the cache
+ *
+ * @param mixed $commandFileInstance
+ * @return array
+ */
+ protected function getCommandInfoListFromCache($commandFileInstance)
+ {
+ $commandInfoList = [];
+ $className = get_class($commandFileInstance);
+ if (!$this->getDataStore()->has($className)) {
+ return [];
+ }
+ $deserializer = new CommandInfoDeserializer();
+
+ $cache_data = $this->getDataStore()->get($className);
+ foreach ($cache_data as $i => $data) {
+ if (CommandInfoDeserializer::isValidSerializedData((array)$data)) {
+ $commandInfoList[$i] = $deserializer->deserialize((array)$data);
+ }
+ }
+ return $commandInfoList;
+ }
+
+ /**
+ * Check to see if this factory has a cache datastore.
+ * @return boolean
+ */
+ public function hasDataStore()
+ {
+ return !($this->dataStore instanceof NullCache);
+ }
+
+ /**
+ * Set a cache datastore for this factory. Any object with 'set' and
+ * 'get' methods is acceptable. The key is the classname being cached,
+ * and the value is a nested associative array of strings.
+ *
+ * TODO: Typehint this to SimpleCacheInterface
+ *
+ * This is not done currently to allow clients to use a generic cache
+ * store that does not itself depend on the annotated-command library.
+ *
+ * @param Mixed $dataStore
+ * @return type
+ */
+ public function setDataStore($dataStore)
+ {
+ if (!($dataStore instanceof SimpleCacheInterface)) {
+ $dataStore = new CacheWrapper($dataStore);
+ }
+ $this->dataStore = $dataStore;
+ return $this;
+ }
+
+ /**
+ * Get the data store attached to this factory.
+ */
+ public function getDataStore()
+ {
+ return $this->dataStore;
+ }
+
+ protected function createCommandInfoListFromClass($classNameOrInstance, $cachedCommandInfoList)
+ {
+ $commandInfoList = [];
+
+ // Ignore special functions, such as __construct and __call, which
+ // can never be commands.
+ $commandMethodNames = array_filter(
+ get_class_methods($classNameOrInstance) ?: [],
+ function ($m) {
+ return !preg_match('#^_#', $m);
+ }
+ );
+
+ foreach ($commandMethodNames as $commandMethodName) {
+ if (!array_key_exists($commandMethodName, $cachedCommandInfoList)) {
+ $commandInfo = CommandInfo::create($classNameOrInstance, $commandMethodName);
+ if (!static::isCommandOrHookMethod($commandInfo, $this->getIncludeAllPublicMethods())) {
+ $commandInfo->invalidate();
+ }
+ $commandInfoList[$commandMethodName] = $commandInfo;
+ }
+ }
+
+ return $commandInfoList;
+ }
+
+ public function createCommandInfo($classNameOrInstance, $commandMethodName)
+ {
+ return CommandInfo::create($classNameOrInstance, $commandMethodName);
+ }
+
+ public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
+ {
+ // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
+ if (!isset($includeAllPublicMethods)) {
+ $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
+ }
+ return $this->createSelectedCommandsFromClassInfo(
+ $commandInfoList,
+ $commandFileInstance,
+ function ($commandInfo) use ($includeAllPublicMethods) {
+ return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
+ }
+ );
+ }
+
+ public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
+ {
+ $commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector);
+ return array_map(
+ function ($commandInfo) use ($commandFileInstance) {
+ return $this->createCommand($commandInfo, $commandFileInstance);
+ },
+ $commandInfoList
+ );
+ }
+
+ protected function filterCommandInfoList($commandInfoList, callable $commandSelector)
+ {
+ return array_filter($commandInfoList, $commandSelector);
+ }
+
+ public static function isCommandOrHookMethod($commandInfo, $includeAllPublicMethods)
+ {
+ return static::isHookMethod($commandInfo) || static::isCommandMethod($commandInfo, $includeAllPublicMethods);
+ }
+
+ public static function isHookMethod($commandInfo)
+ {
+ return $commandInfo->hasAnnotation('hook');
+ }
+
+ public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
+ {
+ // Ignore everything labeled @hook
+ if (static::isHookMethod($commandInfo)) {
+ return false;
+ }
+ // Include everything labeled @command
+ if ($commandInfo->hasAnnotation('command')) {
+ return true;
+ }
+ // Skip anything named like an accessor ('get' or 'set')
+ if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
+ return false;
+ }
+
+ // Default to the setting of 'include all public methods'.
+ return $includeAllPublicMethods;
+ }
+
+ public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
+ {
+ foreach ($commandInfoList as $commandInfo) {
+ if (static::isHookMethod($commandInfo)) {
+ $this->registerCommandHook($commandInfo, $commandFileInstance);
+ }
+ }
+ }
+
+ /**
+ * Register a command hook given the CommandInfo for a method.
+ *
+ * The hook format is:
+ *
+ * @hook type name type
+ *
+ * For example, the pre-validate hook for the core:init command is:
+ *
+ * @hook pre-validate core:init
+ *
+ * If no command name is provided, then this hook will affect every
+ * command that is defined in the same file.
+ *
+ * If no hook is provided, then we will presume that ALTER_RESULT
+ * is intended.
+ *
+ * @param CommandInfo $commandInfo Information about the command hook method.
+ * @param object $commandFileInstance An instance of the CommandFile class.
+ */
+ public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
+ {
+ // Ignore if the command info has no @hook
+ if (!static::isHookMethod($commandInfo)) {
+ return;
+ }
+ $hookData = $commandInfo->getAnnotation('hook');
+ $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
+ $commandName = $this->getNthWord($hookData, 1);
+
+ // Register the hook
+ $callback = [$commandFileInstance, $commandInfo->getMethodName()];
+ $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
+
+ // If the hook has options, then also register the commandInfo
+ // with the hook manager, so that we can add options and such to
+ // the commands they hook.
+ if (!$commandInfo->options()->isEmpty()) {
+ $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName);
+ }
+ }
+
+ protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
+ {
+ $words = explode($delimiter, $string);
+ if (!empty($words[$n])) {
+ return $words[$n];
+ }
+ return $default;
+ }
+
+ public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
+ {
+ $this->alterCommandInfo($commandInfo, $commandFileInstance);
+ $command = new AnnotatedCommand($commandInfo->getName());
+ $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
+ $command->setCommandCallback($commandCallback);
+ $command->setCommandProcessor($this->commandProcessor);
+ $command->setCommandInfo($commandInfo);
+ $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo);
+ $command->setCommandOptions($commandInfo, $automaticOptions);
+ // Annotation commands are never bootstrap-aware, but for completeness
+ // we will notify on every created command, as some clients may wish to
+ // use this notification for some other purpose.
+ $this->notify($command);
+ return $command;
+ }
+
+ /**
+ * Give plugins an opportunity to update the commandInfo
+ */
+ public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
+ {
+ foreach ($this->commandInfoAlterers as $alterer) {
+ $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
+ }
+ }
+
+ /**
+ * Get the options that are implied by annotations, e.g. @fields implies
+ * that there should be a --fields and a --format option.
+ *
+ * @return InputOption[]
+ */
+ public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
+ {
+ $automaticOptions = [];
+ foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
+ $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
+ }
+ return $automaticOptions;
+ }
+
+ /**
+ * Get the options that are implied by annotations, e.g. @fields implies
+ * that there should be a --fields and a --format option.
+ *
+ * @return InputOption[]
+ */
+ public function automaticOptions(CommandInfo $commandInfo)
+ {
+ $automaticOptions = [];
+ $formatManager = $this->commandProcessor()->formatterManager();
+ if ($formatManager) {
+ $annotationData = $commandInfo->getAnnotations()->getArrayCopy();
+ $formatterOptions = new FormatterOptions($annotationData);
+ $dataType = $commandInfo->getReturnType();
+ $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
+ }
+ return $automaticOptions;
+ }
+}