annotate vendor/consolidation/annotated-command/src/AnnotatedCommandFactory.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2 namespace Consolidation\AnnotatedCommand;
Chris@0 3
Chris@0 4 use Consolidation\AnnotatedCommand\Cache\CacheWrapper;
Chris@0 5 use Consolidation\AnnotatedCommand\Cache\NullCache;
Chris@0 6 use Consolidation\AnnotatedCommand\Cache\SimpleCacheInterface;
Chris@0 7 use Consolidation\AnnotatedCommand\Hooks\HookManager;
Chris@0 8 use Consolidation\AnnotatedCommand\Options\AutomaticOptionsProviderInterface;
Chris@0 9 use Consolidation\AnnotatedCommand\Parser\CommandInfo;
Chris@0 10 use Consolidation\AnnotatedCommand\Parser\CommandInfoDeserializer;
Chris@0 11 use Consolidation\AnnotatedCommand\Parser\CommandInfoSerializer;
Chris@0 12 use Consolidation\OutputFormatters\Options\FormatterOptions;
Chris@0 13 use Symfony\Component\Console\Command\Command;
Chris@0 14 use Symfony\Component\Console\Input\InputInterface;
Chris@0 15 use Symfony\Component\Console\Output\OutputInterface;
Chris@0 16
Chris@0 17 /**
Chris@0 18 * The AnnotatedCommandFactory creates commands for your application.
Chris@0 19 * Use with a Dependency Injection Container and the CommandFactory.
Chris@0 20 * Alternately, use the CommandFileDiscovery to find commandfiles, and
Chris@0 21 * then use AnnotatedCommandFactory::createCommandsFromClass() to create
Chris@0 22 * commands. See the README for more information.
Chris@0 23 *
Chris@0 24 * @package Consolidation\AnnotatedCommand
Chris@0 25 */
Chris@0 26 class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
Chris@0 27 {
Chris@0 28 /** var CommandProcessor */
Chris@0 29 protected $commandProcessor;
Chris@0 30
Chris@0 31 /** var CommandCreationListenerInterface[] */
Chris@0 32 protected $listeners = [];
Chris@0 33
Chris@0 34 /** var AutomaticOptionsProvider[] */
Chris@0 35 protected $automaticOptionsProviderList = [];
Chris@0 36
Chris@0 37 /** var boolean */
Chris@0 38 protected $includeAllPublicMethods = true;
Chris@0 39
Chris@0 40 /** var CommandInfoAltererInterface */
Chris@0 41 protected $commandInfoAlterers = [];
Chris@0 42
Chris@0 43 /** var SimpleCacheInterface */
Chris@0 44 protected $dataStore;
Chris@0 45
Chris@0 46 public function __construct()
Chris@0 47 {
Chris@0 48 $this->dataStore = new NullCache();
Chris@0 49 $this->commandProcessor = new CommandProcessor(new HookManager());
Chris@0 50 $this->addAutomaticOptionProvider($this);
Chris@0 51 }
Chris@0 52
Chris@0 53 public function setCommandProcessor(CommandProcessor $commandProcessor)
Chris@0 54 {
Chris@0 55 $this->commandProcessor = $commandProcessor;
Chris@0 56 return $this;
Chris@0 57 }
Chris@0 58
Chris@0 59 /**
Chris@0 60 * @return CommandProcessor
Chris@0 61 */
Chris@0 62 public function commandProcessor()
Chris@0 63 {
Chris@0 64 return $this->commandProcessor;
Chris@0 65 }
Chris@0 66
Chris@0 67 /**
Chris@0 68 * Set the 'include all public methods flag'. If true (the default), then
Chris@0 69 * every public method of each commandFile will be used to create commands.
Chris@0 70 * If it is false, then only those public methods annotated with @command
Chris@0 71 * or @name (deprecated) will be used to create commands.
Chris@0 72 */
Chris@0 73 public function setIncludeAllPublicMethods($includeAllPublicMethods)
Chris@0 74 {
Chris@0 75 $this->includeAllPublicMethods = $includeAllPublicMethods;
Chris@0 76 return $this;
Chris@0 77 }
Chris@0 78
Chris@0 79 public function getIncludeAllPublicMethods()
Chris@0 80 {
Chris@0 81 return $this->includeAllPublicMethods;
Chris@0 82 }
Chris@0 83
Chris@0 84 /**
Chris@0 85 * @return HookManager
Chris@0 86 */
Chris@0 87 public function hookManager()
Chris@0 88 {
Chris@0 89 return $this->commandProcessor()->hookManager();
Chris@0 90 }
Chris@0 91
Chris@0 92 /**
Chris@0 93 * Add a listener that is notified immediately before the command
Chris@0 94 * factory creates commands from a commandFile instance. This
Chris@0 95 * listener can use this opportunity to do more setup for the commandFile,
Chris@0 96 * and so on.
Chris@0 97 *
Chris@0 98 * @param CommandCreationListenerInterface $listener
Chris@0 99 */
Chris@0 100 public function addListener(CommandCreationListenerInterface $listener)
Chris@0 101 {
Chris@0 102 $this->listeners[] = $listener;
Chris@0 103 return $this;
Chris@0 104 }
Chris@0 105
Chris@0 106 /**
Chris@0 107 * Add a listener that's just a simple 'callable'.
Chris@0 108 * @param callable $listener
Chris@0 109 */
Chris@0 110 public function addListernerCallback(callable $listener)
Chris@0 111 {
Chris@0 112 $this->addListener(new CommandCreationListener($listener));
Chris@0 113 return $this;
Chris@0 114 }
Chris@0 115
Chris@0 116 /**
Chris@0 117 * Call all command creation listeners
Chris@0 118 *
Chris@0 119 * @param object $commandFileInstance
Chris@0 120 */
Chris@0 121 protected function notify($commandFileInstance)
Chris@0 122 {
Chris@0 123 foreach ($this->listeners as $listener) {
Chris@0 124 $listener->notifyCommandFileAdded($commandFileInstance);
Chris@0 125 }
Chris@0 126 }
Chris@0 127
Chris@0 128 public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider)
Chris@0 129 {
Chris@0 130 $this->automaticOptionsProviderList[] = $optionsProvider;
Chris@0 131 }
Chris@0 132
Chris@0 133 public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer)
Chris@0 134 {
Chris@0 135 $this->commandInfoAlterers[] = $alterer;
Chris@0 136 }
Chris@0 137
Chris@0 138 /**
Chris@0 139 * n.b. This registers all hooks from the commandfile instance as a side-effect.
Chris@0 140 */
Chris@0 141 public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null)
Chris@0 142 {
Chris@0 143 // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
Chris@0 144 if (!isset($includeAllPublicMethods)) {
Chris@0 145 $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
Chris@0 146 }
Chris@0 147 $this->notify($commandFileInstance);
Chris@0 148 $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
Chris@0 149 $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
Chris@0 150 return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
Chris@0 151 }
Chris@0 152
Chris@0 153 public function getCommandInfoListFromClass($commandFileInstance)
Chris@0 154 {
Chris@0 155 $cachedCommandInfoList = $this->getCommandInfoListFromCache($commandFileInstance);
Chris@0 156 $commandInfoList = $this->createCommandInfoListFromClass($commandFileInstance, $cachedCommandInfoList);
Chris@0 157 if (!empty($commandInfoList)) {
Chris@0 158 $cachedCommandInfoList = array_merge($commandInfoList, $cachedCommandInfoList);
Chris@0 159 $this->storeCommandInfoListInCache($commandFileInstance, $cachedCommandInfoList);
Chris@0 160 }
Chris@0 161 return $cachedCommandInfoList;
Chris@0 162 }
Chris@0 163
Chris@0 164 protected function storeCommandInfoListInCache($commandFileInstance, $commandInfoList)
Chris@0 165 {
Chris@0 166 if (!$this->hasDataStore()) {
Chris@0 167 return;
Chris@0 168 }
Chris@0 169 $cache_data = [];
Chris@0 170 $serializer = new CommandInfoSerializer();
Chris@0 171 foreach ($commandInfoList as $i => $commandInfo) {
Chris@0 172 $cache_data[$i] = $serializer->serialize($commandInfo);
Chris@0 173 }
Chris@0 174 $className = get_class($commandFileInstance);
Chris@0 175 $this->getDataStore()->set($className, $cache_data);
Chris@0 176 }
Chris@0 177
Chris@0 178 /**
Chris@0 179 * Get the command info list from the cache
Chris@0 180 *
Chris@0 181 * @param mixed $commandFileInstance
Chris@0 182 * @return array
Chris@0 183 */
Chris@0 184 protected function getCommandInfoListFromCache($commandFileInstance)
Chris@0 185 {
Chris@0 186 $commandInfoList = [];
Chris@17 187 if (!is_object($commandFileInstance)) {
Chris@17 188 return [];
Chris@17 189 }
Chris@0 190 $className = get_class($commandFileInstance);
Chris@0 191 if (!$this->getDataStore()->has($className)) {
Chris@0 192 return [];
Chris@0 193 }
Chris@0 194 $deserializer = new CommandInfoDeserializer();
Chris@0 195
Chris@0 196 $cache_data = $this->getDataStore()->get($className);
Chris@0 197 foreach ($cache_data as $i => $data) {
Chris@0 198 if (CommandInfoDeserializer::isValidSerializedData((array)$data)) {
Chris@0 199 $commandInfoList[$i] = $deserializer->deserialize((array)$data);
Chris@0 200 }
Chris@0 201 }
Chris@0 202 return $commandInfoList;
Chris@0 203 }
Chris@0 204
Chris@0 205 /**
Chris@0 206 * Check to see if this factory has a cache datastore.
Chris@0 207 * @return boolean
Chris@0 208 */
Chris@0 209 public function hasDataStore()
Chris@0 210 {
Chris@0 211 return !($this->dataStore instanceof NullCache);
Chris@0 212 }
Chris@0 213
Chris@0 214 /**
Chris@0 215 * Set a cache datastore for this factory. Any object with 'set' and
Chris@0 216 * 'get' methods is acceptable. The key is the classname being cached,
Chris@0 217 * and the value is a nested associative array of strings.
Chris@0 218 *
Chris@0 219 * TODO: Typehint this to SimpleCacheInterface
Chris@0 220 *
Chris@0 221 * This is not done currently to allow clients to use a generic cache
Chris@0 222 * store that does not itself depend on the annotated-command library.
Chris@0 223 *
Chris@0 224 * @param Mixed $dataStore
Chris@0 225 * @return type
Chris@0 226 */
Chris@0 227 public function setDataStore($dataStore)
Chris@0 228 {
Chris@0 229 if (!($dataStore instanceof SimpleCacheInterface)) {
Chris@0 230 $dataStore = new CacheWrapper($dataStore);
Chris@0 231 }
Chris@0 232 $this->dataStore = $dataStore;
Chris@0 233 return $this;
Chris@0 234 }
Chris@0 235
Chris@0 236 /**
Chris@0 237 * Get the data store attached to this factory.
Chris@0 238 */
Chris@0 239 public function getDataStore()
Chris@0 240 {
Chris@0 241 return $this->dataStore;
Chris@0 242 }
Chris@0 243
Chris@0 244 protected function createCommandInfoListFromClass($classNameOrInstance, $cachedCommandInfoList)
Chris@0 245 {
Chris@0 246 $commandInfoList = [];
Chris@0 247
Chris@0 248 // Ignore special functions, such as __construct and __call, which
Chris@0 249 // can never be commands.
Chris@0 250 $commandMethodNames = array_filter(
Chris@0 251 get_class_methods($classNameOrInstance) ?: [],
Chris@0 252 function ($m) use ($classNameOrInstance) {
Chris@0 253 $reflectionMethod = new \ReflectionMethod($classNameOrInstance, $m);
Chris@0 254 return !$reflectionMethod->isStatic() && !preg_match('#^_#', $m);
Chris@0 255 }
Chris@0 256 );
Chris@0 257
Chris@0 258 foreach ($commandMethodNames as $commandMethodName) {
Chris@0 259 if (!array_key_exists($commandMethodName, $cachedCommandInfoList)) {
Chris@0 260 $commandInfo = CommandInfo::create($classNameOrInstance, $commandMethodName);
Chris@0 261 if (!static::isCommandOrHookMethod($commandInfo, $this->getIncludeAllPublicMethods())) {
Chris@0 262 $commandInfo->invalidate();
Chris@0 263 }
Chris@0 264 $commandInfoList[$commandMethodName] = $commandInfo;
Chris@0 265 }
Chris@0 266 }
Chris@0 267
Chris@0 268 return $commandInfoList;
Chris@0 269 }
Chris@0 270
Chris@0 271 public function createCommandInfo($classNameOrInstance, $commandMethodName)
Chris@0 272 {
Chris@0 273 return CommandInfo::create($classNameOrInstance, $commandMethodName);
Chris@0 274 }
Chris@0 275
Chris@0 276 public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
Chris@0 277 {
Chris@0 278 // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
Chris@0 279 if (!isset($includeAllPublicMethods)) {
Chris@0 280 $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
Chris@0 281 }
Chris@0 282 return $this->createSelectedCommandsFromClassInfo(
Chris@0 283 $commandInfoList,
Chris@0 284 $commandFileInstance,
Chris@0 285 function ($commandInfo) use ($includeAllPublicMethods) {
Chris@0 286 return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
Chris@0 287 }
Chris@0 288 );
Chris@0 289 }
Chris@0 290
Chris@0 291 public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
Chris@0 292 {
Chris@0 293 $commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector);
Chris@0 294 return array_map(
Chris@0 295 function ($commandInfo) use ($commandFileInstance) {
Chris@0 296 return $this->createCommand($commandInfo, $commandFileInstance);
Chris@0 297 },
Chris@0 298 $commandInfoList
Chris@0 299 );
Chris@0 300 }
Chris@0 301
Chris@0 302 protected function filterCommandInfoList($commandInfoList, callable $commandSelector)
Chris@0 303 {
Chris@0 304 return array_filter($commandInfoList, $commandSelector);
Chris@0 305 }
Chris@0 306
Chris@0 307 public static function isCommandOrHookMethod($commandInfo, $includeAllPublicMethods)
Chris@0 308 {
Chris@0 309 return static::isHookMethod($commandInfo) || static::isCommandMethod($commandInfo, $includeAllPublicMethods);
Chris@0 310 }
Chris@0 311
Chris@0 312 public static function isHookMethod($commandInfo)
Chris@0 313 {
Chris@0 314 return $commandInfo->hasAnnotation('hook');
Chris@0 315 }
Chris@0 316
Chris@0 317 public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
Chris@0 318 {
Chris@0 319 // Ignore everything labeled @hook
Chris@0 320 if (static::isHookMethod($commandInfo)) {
Chris@0 321 return false;
Chris@0 322 }
Chris@0 323 // Include everything labeled @command
Chris@0 324 if ($commandInfo->hasAnnotation('command')) {
Chris@0 325 return true;
Chris@0 326 }
Chris@0 327 // Skip anything that has a missing or invalid name.
Chris@0 328 $commandName = $commandInfo->getName();
Chris@0 329 if (empty($commandName) || preg_match('#[^a-zA-Z0-9:_-]#', $commandName)) {
Chris@0 330 return false;
Chris@0 331 }
Chris@0 332 // Skip anything named like an accessor ('get' or 'set')
Chris@0 333 if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
Chris@0 334 return false;
Chris@0 335 }
Chris@0 336
Chris@0 337 // Default to the setting of 'include all public methods'.
Chris@0 338 return $includeAllPublicMethods;
Chris@0 339 }
Chris@0 340
Chris@0 341 public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
Chris@0 342 {
Chris@0 343 foreach ($commandInfoList as $commandInfo) {
Chris@0 344 if (static::isHookMethod($commandInfo)) {
Chris@0 345 $this->registerCommandHook($commandInfo, $commandFileInstance);
Chris@0 346 }
Chris@0 347 }
Chris@0 348 }
Chris@0 349
Chris@0 350 /**
Chris@0 351 * Register a command hook given the CommandInfo for a method.
Chris@0 352 *
Chris@0 353 * The hook format is:
Chris@0 354 *
Chris@0 355 * @hook type name type
Chris@0 356 *
Chris@0 357 * For example, the pre-validate hook for the core:init command is:
Chris@0 358 *
Chris@0 359 * @hook pre-validate core:init
Chris@0 360 *
Chris@0 361 * If no command name is provided, then this hook will affect every
Chris@0 362 * command that is defined in the same file.
Chris@0 363 *
Chris@0 364 * If no hook is provided, then we will presume that ALTER_RESULT
Chris@0 365 * is intended.
Chris@0 366 *
Chris@0 367 * @param CommandInfo $commandInfo Information about the command hook method.
Chris@0 368 * @param object $commandFileInstance An instance of the CommandFile class.
Chris@0 369 */
Chris@0 370 public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
Chris@0 371 {
Chris@0 372 // Ignore if the command info has no @hook
Chris@0 373 if (!static::isHookMethod($commandInfo)) {
Chris@0 374 return;
Chris@0 375 }
Chris@0 376 $hookData = $commandInfo->getAnnotation('hook');
Chris@0 377 $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
Chris@0 378 $commandName = $this->getNthWord($hookData, 1);
Chris@0 379
Chris@0 380 // Register the hook
Chris@0 381 $callback = [$commandFileInstance, $commandInfo->getMethodName()];
Chris@0 382 $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
Chris@0 383
Chris@0 384 // If the hook has options, then also register the commandInfo
Chris@0 385 // with the hook manager, so that we can add options and such to
Chris@0 386 // the commands they hook.
Chris@0 387 if (!$commandInfo->options()->isEmpty()) {
Chris@0 388 $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName);
Chris@0 389 }
Chris@0 390 }
Chris@0 391
Chris@0 392 protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
Chris@0 393 {
Chris@0 394 $words = explode($delimiter, $string);
Chris@0 395 if (!empty($words[$n])) {
Chris@0 396 return $words[$n];
Chris@0 397 }
Chris@0 398 return $default;
Chris@0 399 }
Chris@0 400
Chris@0 401 public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
Chris@0 402 {
Chris@0 403 $this->alterCommandInfo($commandInfo, $commandFileInstance);
Chris@0 404 $command = new AnnotatedCommand($commandInfo->getName());
Chris@0 405 $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
Chris@0 406 $command->setCommandCallback($commandCallback);
Chris@0 407 $command->setCommandProcessor($this->commandProcessor);
Chris@0 408 $command->setCommandInfo($commandInfo);
Chris@0 409 $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo);
Chris@0 410 $command->setCommandOptions($commandInfo, $automaticOptions);
Chris@0 411 // Annotation commands are never bootstrap-aware, but for completeness
Chris@0 412 // we will notify on every created command, as some clients may wish to
Chris@0 413 // use this notification for some other purpose.
Chris@0 414 $this->notify($command);
Chris@0 415 return $command;
Chris@0 416 }
Chris@0 417
Chris@0 418 /**
Chris@0 419 * Give plugins an opportunity to update the commandInfo
Chris@0 420 */
Chris@0 421 public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
Chris@0 422 {
Chris@0 423 foreach ($this->commandInfoAlterers as $alterer) {
Chris@0 424 $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
Chris@0 425 }
Chris@0 426 }
Chris@0 427
Chris@0 428 /**
Chris@0 429 * Get the options that are implied by annotations, e.g. @fields implies
Chris@0 430 * that there should be a --fields and a --format option.
Chris@0 431 *
Chris@0 432 * @return InputOption[]
Chris@0 433 */
Chris@0 434 public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
Chris@0 435 {
Chris@0 436 $automaticOptions = [];
Chris@0 437 foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
Chris@0 438 $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
Chris@0 439 }
Chris@0 440 return $automaticOptions;
Chris@0 441 }
Chris@0 442
Chris@0 443 /**
Chris@0 444 * Get the options that are implied by annotations, e.g. @fields implies
Chris@0 445 * that there should be a --fields and a --format option.
Chris@0 446 *
Chris@0 447 * @return InputOption[]
Chris@0 448 */
Chris@0 449 public function automaticOptions(CommandInfo $commandInfo)
Chris@0 450 {
Chris@0 451 $automaticOptions = [];
Chris@0 452 $formatManager = $this->commandProcessor()->formatterManager();
Chris@0 453 if ($formatManager) {
Chris@0 454 $annotationData = $commandInfo->getAnnotations()->getArrayCopy();
Chris@0 455 $formatterOptions = new FormatterOptions($annotationData);
Chris@0 456 $dataType = $commandInfo->getReturnType();
Chris@0 457 $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
Chris@0 458 }
Chris@0 459 return $automaticOptions;
Chris@0 460 }
Chris@0 461 }