annotate vendor/consolidation/annotated-command/src/AnnotatedCommandFactory.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 129ea1e6d783
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@0 187 $className = get_class($commandFileInstance);
Chris@0 188 if (!$this->getDataStore()->has($className)) {
Chris@0 189 return [];
Chris@0 190 }
Chris@0 191 $deserializer = new CommandInfoDeserializer();
Chris@0 192
Chris@0 193 $cache_data = $this->getDataStore()->get($className);
Chris@0 194 foreach ($cache_data as $i => $data) {
Chris@0 195 if (CommandInfoDeserializer::isValidSerializedData((array)$data)) {
Chris@0 196 $commandInfoList[$i] = $deserializer->deserialize((array)$data);
Chris@0 197 }
Chris@0 198 }
Chris@0 199 return $commandInfoList;
Chris@0 200 }
Chris@0 201
Chris@0 202 /**
Chris@0 203 * Check to see if this factory has a cache datastore.
Chris@0 204 * @return boolean
Chris@0 205 */
Chris@0 206 public function hasDataStore()
Chris@0 207 {
Chris@0 208 return !($this->dataStore instanceof NullCache);
Chris@0 209 }
Chris@0 210
Chris@0 211 /**
Chris@0 212 * Set a cache datastore for this factory. Any object with 'set' and
Chris@0 213 * 'get' methods is acceptable. The key is the classname being cached,
Chris@0 214 * and the value is a nested associative array of strings.
Chris@0 215 *
Chris@0 216 * TODO: Typehint this to SimpleCacheInterface
Chris@0 217 *
Chris@0 218 * This is not done currently to allow clients to use a generic cache
Chris@0 219 * store that does not itself depend on the annotated-command library.
Chris@0 220 *
Chris@0 221 * @param Mixed $dataStore
Chris@0 222 * @return type
Chris@0 223 */
Chris@0 224 public function setDataStore($dataStore)
Chris@0 225 {
Chris@0 226 if (!($dataStore instanceof SimpleCacheInterface)) {
Chris@0 227 $dataStore = new CacheWrapper($dataStore);
Chris@0 228 }
Chris@0 229 $this->dataStore = $dataStore;
Chris@0 230 return $this;
Chris@0 231 }
Chris@0 232
Chris@0 233 /**
Chris@0 234 * Get the data store attached to this factory.
Chris@0 235 */
Chris@0 236 public function getDataStore()
Chris@0 237 {
Chris@0 238 return $this->dataStore;
Chris@0 239 }
Chris@0 240
Chris@0 241 protected function createCommandInfoListFromClass($classNameOrInstance, $cachedCommandInfoList)
Chris@0 242 {
Chris@0 243 $commandInfoList = [];
Chris@0 244
Chris@0 245 // Ignore special functions, such as __construct and __call, which
Chris@0 246 // can never be commands.
Chris@0 247 $commandMethodNames = array_filter(
Chris@0 248 get_class_methods($classNameOrInstance) ?: [],
Chris@0 249 function ($m) use ($classNameOrInstance) {
Chris@0 250 $reflectionMethod = new \ReflectionMethod($classNameOrInstance, $m);
Chris@0 251 return !$reflectionMethod->isStatic() && !preg_match('#^_#', $m);
Chris@0 252 }
Chris@0 253 );
Chris@0 254
Chris@0 255 foreach ($commandMethodNames as $commandMethodName) {
Chris@0 256 if (!array_key_exists($commandMethodName, $cachedCommandInfoList)) {
Chris@0 257 $commandInfo = CommandInfo::create($classNameOrInstance, $commandMethodName);
Chris@0 258 if (!static::isCommandOrHookMethod($commandInfo, $this->getIncludeAllPublicMethods())) {
Chris@0 259 $commandInfo->invalidate();
Chris@0 260 }
Chris@0 261 $commandInfoList[$commandMethodName] = $commandInfo;
Chris@0 262 }
Chris@0 263 }
Chris@0 264
Chris@0 265 return $commandInfoList;
Chris@0 266 }
Chris@0 267
Chris@0 268 public function createCommandInfo($classNameOrInstance, $commandMethodName)
Chris@0 269 {
Chris@0 270 return CommandInfo::create($classNameOrInstance, $commandMethodName);
Chris@0 271 }
Chris@0 272
Chris@0 273 public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
Chris@0 274 {
Chris@0 275 // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
Chris@0 276 if (!isset($includeAllPublicMethods)) {
Chris@0 277 $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
Chris@0 278 }
Chris@0 279 return $this->createSelectedCommandsFromClassInfo(
Chris@0 280 $commandInfoList,
Chris@0 281 $commandFileInstance,
Chris@0 282 function ($commandInfo) use ($includeAllPublicMethods) {
Chris@0 283 return static::isCommandMethod($commandInfo, $includeAllPublicMethods);
Chris@0 284 }
Chris@0 285 );
Chris@0 286 }
Chris@0 287
Chris@0 288 public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
Chris@0 289 {
Chris@0 290 $commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector);
Chris@0 291 return array_map(
Chris@0 292 function ($commandInfo) use ($commandFileInstance) {
Chris@0 293 return $this->createCommand($commandInfo, $commandFileInstance);
Chris@0 294 },
Chris@0 295 $commandInfoList
Chris@0 296 );
Chris@0 297 }
Chris@0 298
Chris@0 299 protected function filterCommandInfoList($commandInfoList, callable $commandSelector)
Chris@0 300 {
Chris@0 301 return array_filter($commandInfoList, $commandSelector);
Chris@0 302 }
Chris@0 303
Chris@0 304 public static function isCommandOrHookMethod($commandInfo, $includeAllPublicMethods)
Chris@0 305 {
Chris@0 306 return static::isHookMethod($commandInfo) || static::isCommandMethod($commandInfo, $includeAllPublicMethods);
Chris@0 307 }
Chris@0 308
Chris@0 309 public static function isHookMethod($commandInfo)
Chris@0 310 {
Chris@0 311 return $commandInfo->hasAnnotation('hook');
Chris@0 312 }
Chris@0 313
Chris@0 314 public static function isCommandMethod($commandInfo, $includeAllPublicMethods)
Chris@0 315 {
Chris@0 316 // Ignore everything labeled @hook
Chris@0 317 if (static::isHookMethod($commandInfo)) {
Chris@0 318 return false;
Chris@0 319 }
Chris@0 320 // Include everything labeled @command
Chris@0 321 if ($commandInfo->hasAnnotation('command')) {
Chris@0 322 return true;
Chris@0 323 }
Chris@0 324 // Skip anything that has a missing or invalid name.
Chris@0 325 $commandName = $commandInfo->getName();
Chris@0 326 if (empty($commandName) || preg_match('#[^a-zA-Z0-9:_-]#', $commandName)) {
Chris@0 327 return false;
Chris@0 328 }
Chris@0 329 // Skip anything named like an accessor ('get' or 'set')
Chris@0 330 if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
Chris@0 331 return false;
Chris@0 332 }
Chris@0 333
Chris@0 334 // Default to the setting of 'include all public methods'.
Chris@0 335 return $includeAllPublicMethods;
Chris@0 336 }
Chris@0 337
Chris@0 338 public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
Chris@0 339 {
Chris@0 340 foreach ($commandInfoList as $commandInfo) {
Chris@0 341 if (static::isHookMethod($commandInfo)) {
Chris@0 342 $this->registerCommandHook($commandInfo, $commandFileInstance);
Chris@0 343 }
Chris@0 344 }
Chris@0 345 }
Chris@0 346
Chris@0 347 /**
Chris@0 348 * Register a command hook given the CommandInfo for a method.
Chris@0 349 *
Chris@0 350 * The hook format is:
Chris@0 351 *
Chris@0 352 * @hook type name type
Chris@0 353 *
Chris@0 354 * For example, the pre-validate hook for the core:init command is:
Chris@0 355 *
Chris@0 356 * @hook pre-validate core:init
Chris@0 357 *
Chris@0 358 * If no command name is provided, then this hook will affect every
Chris@0 359 * command that is defined in the same file.
Chris@0 360 *
Chris@0 361 * If no hook is provided, then we will presume that ALTER_RESULT
Chris@0 362 * is intended.
Chris@0 363 *
Chris@0 364 * @param CommandInfo $commandInfo Information about the command hook method.
Chris@0 365 * @param object $commandFileInstance An instance of the CommandFile class.
Chris@0 366 */
Chris@0 367 public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
Chris@0 368 {
Chris@0 369 // Ignore if the command info has no @hook
Chris@0 370 if (!static::isHookMethod($commandInfo)) {
Chris@0 371 return;
Chris@0 372 }
Chris@0 373 $hookData = $commandInfo->getAnnotation('hook');
Chris@0 374 $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
Chris@0 375 $commandName = $this->getNthWord($hookData, 1);
Chris@0 376
Chris@0 377 // Register the hook
Chris@0 378 $callback = [$commandFileInstance, $commandInfo->getMethodName()];
Chris@0 379 $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
Chris@0 380
Chris@0 381 // If the hook has options, then also register the commandInfo
Chris@0 382 // with the hook manager, so that we can add options and such to
Chris@0 383 // the commands they hook.
Chris@0 384 if (!$commandInfo->options()->isEmpty()) {
Chris@0 385 $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName);
Chris@0 386 }
Chris@0 387 }
Chris@0 388
Chris@0 389 protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
Chris@0 390 {
Chris@0 391 $words = explode($delimiter, $string);
Chris@0 392 if (!empty($words[$n])) {
Chris@0 393 return $words[$n];
Chris@0 394 }
Chris@0 395 return $default;
Chris@0 396 }
Chris@0 397
Chris@0 398 public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
Chris@0 399 {
Chris@0 400 $this->alterCommandInfo($commandInfo, $commandFileInstance);
Chris@0 401 $command = new AnnotatedCommand($commandInfo->getName());
Chris@0 402 $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
Chris@0 403 $command->setCommandCallback($commandCallback);
Chris@0 404 $command->setCommandProcessor($this->commandProcessor);
Chris@0 405 $command->setCommandInfo($commandInfo);
Chris@0 406 $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo);
Chris@0 407 $command->setCommandOptions($commandInfo, $automaticOptions);
Chris@0 408 // Annotation commands are never bootstrap-aware, but for completeness
Chris@0 409 // we will notify on every created command, as some clients may wish to
Chris@0 410 // use this notification for some other purpose.
Chris@0 411 $this->notify($command);
Chris@0 412 return $command;
Chris@0 413 }
Chris@0 414
Chris@0 415 /**
Chris@0 416 * Give plugins an opportunity to update the commandInfo
Chris@0 417 */
Chris@0 418 public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance)
Chris@0 419 {
Chris@0 420 foreach ($this->commandInfoAlterers as $alterer) {
Chris@0 421 $alterer->alterCommandInfo($commandInfo, $commandFileInstance);
Chris@0 422 }
Chris@0 423 }
Chris@0 424
Chris@0 425 /**
Chris@0 426 * Get the options that are implied by annotations, e.g. @fields implies
Chris@0 427 * that there should be a --fields and a --format option.
Chris@0 428 *
Chris@0 429 * @return InputOption[]
Chris@0 430 */
Chris@0 431 public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
Chris@0 432 {
Chris@0 433 $automaticOptions = [];
Chris@0 434 foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
Chris@0 435 $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
Chris@0 436 }
Chris@0 437 return $automaticOptions;
Chris@0 438 }
Chris@0 439
Chris@0 440 /**
Chris@0 441 * Get the options that are implied by annotations, e.g. @fields implies
Chris@0 442 * that there should be a --fields and a --format option.
Chris@0 443 *
Chris@0 444 * @return InputOption[]
Chris@0 445 */
Chris@0 446 public function automaticOptions(CommandInfo $commandInfo)
Chris@0 447 {
Chris@0 448 $automaticOptions = [];
Chris@0 449 $formatManager = $this->commandProcessor()->formatterManager();
Chris@0 450 if ($formatManager) {
Chris@0 451 $annotationData = $commandInfo->getAnnotations()->getArrayCopy();
Chris@0 452 $formatterOptions = new FormatterOptions($annotationData);
Chris@0 453 $dataType = $commandInfo->getReturnType();
Chris@0 454 $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
Chris@0 455 }
Chris@0 456 return $automaticOptions;
Chris@0 457 }
Chris@0 458 }