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; } }