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