Add pear modules, mail and net_smtp via composer (#93)

Add pear modules, mail and net_smtp via composer, remove php 5.6 build due to phpunit 6
This commit is contained in:
Thilina Hasantha
2018-01-08 23:13:43 +01:00
committed by GitHub
parent 359e3f8382
commit e7792e7d79
2349 changed files with 117270 additions and 83170 deletions

View File

@@ -3,8 +3,7 @@ namespace Consolidation\AnnotatedCommand;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
use Consolidation\OutputFormatters\FormatterManager;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\AnnotatedCommand\Help\HelpDocumentAlter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -26,11 +25,13 @@ use Symfony\Component\Console\Output\OutputInterface;
*
* @package Consolidation\AnnotatedCommand
*/
class AnnotatedCommand extends Command
class AnnotatedCommand extends Command implements HelpDocumentAlter
{
protected $commandCallback;
protected $commandProcessor;
protected $annotationData;
protected $examples = [];
protected $topics = [];
protected $usesInputInterface;
protected $usesOutputInterface;
protected $returnType;
@@ -46,7 +47,7 @@ class AnnotatedCommand extends Command
// AnnotatedCommand. Alternately, we break out a new subclass.
// The command factory instantiates the subclass.
if (get_class($this) != 'Consolidation\AnnotatedCommand\AnnotatedCommand') {
$commandInfo = new CommandInfo($this, 'execute');
$commandInfo = CommandInfo::create($this, 'execute');
if (!isset($name)) {
$name = $commandInfo->getName();
}
@@ -106,21 +107,114 @@ class AnnotatedCommand extends Command
return $this;
}
public function getTopics()
{
return $this->topics;
}
public function setTopics($topics)
{
$this->topics = $topics;
return $this;
}
public function setCommandInfo($commandInfo)
{
$this->setDescription($commandInfo->getDescription());
$this->setHelp($commandInfo->getHelp());
$this->setAliases($commandInfo->getAliases());
$this->setAnnotationData($commandInfo->getAnnotations());
$this->setTopics($commandInfo->getTopics());
foreach ($commandInfo->getExampleUsages() as $usage => $description) {
// Symfony Console does not support attaching a description to a usage
$this->addUsage($usage);
$this->addUsageOrExample($usage, $description);
}
$this->setCommandArguments($commandInfo);
$this->setReturnType($commandInfo->getReturnType());
// Hidden commands available since Symfony 3.2
// http://symfony.com/doc/current/console/hide_commands.html
if (method_exists($this, 'setHidden')) {
$this->setHidden($commandInfo->getHidden());
}
return $this;
}
public function getExampleUsages()
{
return $this->examples;
}
protected function addUsageOrExample($usage, $description)
{
$this->addUsage($usage);
if (!empty($description)) {
$this->examples[$usage] = $description;
}
}
public function helpAlter(\DomDocument $originalDom)
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($commandXML = $dom->createElement('command'));
$commandXML->setAttribute('id', $this->getName());
$commandXML->setAttribute('name', $this->getName());
// Get the original <command> element and its top-level elements.
$originalCommandXML = $this->getSingleElementByTagName($dom, $originalDom, 'command');
$originalUsagesXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'usages');
$originalDescriptionXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'description');
$originalHelpXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'help');
$originalArgumentsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'arguments');
$originalOptionsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'options');
// Keep only the first of the <usage> elements
$newUsagesXML = $dom->createElement('usages');
$firstUsageXML = $this->getSingleElementByTagName($dom, $originalUsagesXML, 'usage');
$newUsagesXML->appendChild($firstUsageXML);
// Create our own <example> elements
$newExamplesXML = $dom->createElement('examples');
foreach ($this->examples as $usage => $description) {
$newExamplesXML->appendChild($exampleXML = $dom->createElement('example'));
$exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage));
$exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description));
}
// Create our own <alias> elements
$newAliasesXML = $dom->createElement('aliases');
foreach ($this->getAliases() as $alias) {
$newAliasesXML->appendChild($dom->createElement('alias', $alias));
}
// Create our own <topic> elements
$newTopicsXML = $dom->createElement('topics');
foreach ($this->getTopics() as $topic) {
$newTopicsXML->appendChild($topicXML = $dom->createElement('topic', $topic));
}
// Place the different elements into the <command> element in the desired order
$commandXML->appendChild($newUsagesXML);
$commandXML->appendChild($newExamplesXML);
$commandXML->appendChild($originalDescriptionXML);
$commandXML->appendChild($originalArgumentsXML);
$commandXML->appendChild($originalOptionsXML);
$commandXML->appendChild($originalHelpXML);
$commandXML->appendChild($newAliasesXML);
$commandXML->appendChild($newTopicsXML);
return $dom;
}
protected function getSingleElementByTagName($dom, $parent, $tagName)
{
// There should always be exactly one '<command>' element.
$elements = $parent->getElementsByTagName($tagName);
$result = $elements->item(0);
$result = $dom->importNode($result, true);
return $result;
}
protected function setCommandArguments($commandInfo)
{
$this->setUsesInputInterface($commandInfo);
@@ -134,8 +228,11 @@ class AnnotatedCommand extends Command
*/
protected function checkUsesInputInterface($params)
{
/** @var \ReflectionParameter $firstParam */
$firstParam = reset($params);
return $firstParam instanceof InputInterface;
return $firstParam && $firstParam->getClass() && $firstParam->getClass()->implementsInterface(
'\\Symfony\\Component\\Console\\Input\\InputInterface'
);
}
/**
@@ -160,7 +257,11 @@ class AnnotatedCommand extends Command
$index = $this->checkUsesInputInterface($params) ? 1 : 0;
$this->usesOutputInterface =
(count($params) > $index) &&
($params[$index] instanceof OutputInterface);
$params[$index]->getClass() &&
$params[$index]->getClass()->implementsInterface(
'\\Symfony\\Component\\Console\\Output\\OutputInterface'
)
;
return $this;
}
@@ -264,7 +365,7 @@ class AnnotatedCommand extends Command
$this->addOptions($inputOptions);
foreach ($commandInfo->getExampleUsages() as $usage => $description) {
if (!in_array($usage, $this->getUsages())) {
$this->addUsage($usage);
$this->addUsageOrExample($usage, $description);
}
}
}
@@ -337,10 +438,15 @@ class AnnotatedCommand extends Command
);
$commandData->setUseIOInterfaces(
$this->usesOutputInterface,
$this->usesInputInterface
$this->usesInputInterface,
$this->usesOutputInterface
);
// Allow the commandData to cache the list of options with
// special default values ('null' and 'true'), as these will
// need special handling. @see CommandData::options().
$commandData->cacheSpecialDefaults($this->getDefinition());
return $commandData;
}
}

View File

@@ -1,9 +1,14 @@
<?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;
@@ -27,7 +32,6 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
protected $listeners = [];
/** var AutomaticOptionsProvider[] */
protected $automaticOptionsProviderList = [];
/** var boolean */
@@ -36,8 +40,12 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
/** var CommandInfoAltererInterface */
protected $commandInfoAlterers = [];
/** var SimpleCacheInterface */
protected $dataStore;
public function __construct()
{
$this->dataStore = new NullCache();
$this->commandProcessor = new CommandProcessor(new HookManager());
$this->addAutomaticOptionProvider($this);
}
@@ -92,6 +100,17 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
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;
}
/**
@@ -131,7 +150,95 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
}
public function getCommandInfoListFromClass($classNameOrInstance)
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 = [];
@@ -139,13 +246,20 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
// can never be commands.
$commandMethodNames = array_filter(
get_class_methods($classNameOrInstance) ?: [],
function ($m) {
return !preg_match('#^_#', $m);
function ($m) use ($classNameOrInstance) {
$reflectionMethod = new \ReflectionMethod($classNameOrInstance, $m);
return !$reflectionMethod->isStatic() && !preg_match('#^_#', $m);
}
);
foreach ($commandMethodNames as $commandMethodName) {
$commandInfoList[] = new CommandInfo($classNameOrInstance, $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;
@@ -153,7 +267,7 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
public function createCommandInfo($classNameOrInstance, $commandMethodName)
{
return new CommandInfo($classNameOrInstance, $commandMethodName);
return CommandInfo::create($classNameOrInstance, $commandMethodName);
}
public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
@@ -173,28 +287,45 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
{
$commandList = [];
$commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector);
return array_map(
function ($commandInfo) use ($commandFileInstance) {
return $this->createCommand($commandInfo, $commandFileInstance);
},
$commandInfoList
);
}
foreach ($commandInfoList as $commandInfo) {
if ($commandSelector($commandInfo)) {
$command = $this->createCommand($commandInfo, $commandFileInstance);
$commandList[] = $command;
}
}
protected function filterCommandInfoList($commandInfoList, callable $commandSelector)
{
return array_filter($commandInfoList, $commandSelector);
}
return $commandList;
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 ($commandInfo->hasAnnotation('hook')) {
if (static::isHookMethod($commandInfo)) {
return false;
}
// Include everything labeled @command
if ($commandInfo->hasAnnotation('command')) {
return true;
}
// Skip anything that has a missing or invalid name.
$commandName = $commandInfo->getName();
if (empty($commandName) || preg_match('#[^a-zA-Z0-9:_-]#', $commandName)) {
return false;
}
// Skip anything named like an accessor ('get' or 'set')
if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) {
return false;
@@ -207,7 +338,7 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
{
foreach ($commandInfoList as $commandInfo) {
if ($commandInfo->hasAnnotation('hook')) {
if (static::isHookMethod($commandInfo)) {
$this->registerCommandHook($commandInfo, $commandFileInstance);
}
}
@@ -236,7 +367,7 @@ class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
{
// Ignore if the command info has no @hook
if (!$commandInfo->hasAnnotation('hook')) {
if (!static::isHookMethod($commandInfo)) {
return;
}
$hookData = $commandInfo->getAnnotation('hook');

View File

@@ -1,11 +1,18 @@
<?php
namespace Consolidation\AnnotatedCommand;
use Consolidation\AnnotatedCommand\Parser\Internal\CsvUtils;
class AnnotationData extends \ArrayObject
{
public function get($key, $default)
public function get($key, $default = '')
{
return $this->has($key) ? $this[$key] : $default;
return $this->has($key) ? CsvUtils::toString($this[$key]) : $default;
}
public function getList($key, $default = [])
{
return $this->has($key) ? CsvUtils::toList($this[$key]) : $default;
}
public function has($key)

View File

@@ -0,0 +1,49 @@
<?php
namespace Consolidation\AnnotatedCommand\Cache;
/**
* Make a generic cache object conform to our expected interface.
*/
class CacheWrapper implements SimpleCacheInterface
{
protected $dataStore;
public function __construct($dataStore)
{
$this->dataStore = $dataStore;
}
/**
* Test for an entry from the cache
* @param string $key
* @return boolean
*/
public function has($key)
{
if (method_exists($this->dataStore, 'has')) {
return $this->dataStore->has($key);
}
$test = $this->dataStore->get($key);
return !empty($test);
}
/**
* Get an entry from the cache
* @param string $key
* @return array
*/
public function get($key)
{
return (array) $this->dataStore->get($key);
}
/**
* Store an entry in the cache
* @param string $key
* @param array $data
*/
public function set($key, $data)
{
$this->dataStore->set($key, $data);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Consolidation\AnnotatedCommand\Cache;
/**
* An empty cache that never stores or fetches any objects.
*/
class NullCache implements SimpleCacheInterface
{
/**
* Test for an entry from the cache
* @param string $key
* @return boolean
*/
public function has($key)
{
return false;
}
/**
* Get an entry from the cache
* @param string $key
* @return array
*/
public function get($key)
{
return [];
}
/**
* Store an entry in the cache
* @param string $key
* @param array $data
*/
public function set($key, $data)
{
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Consolidation\AnnotatedCommand\Cache;
/**
* Documentation interface.
*
* Clients that use AnnotatedCommandFactory::setDataStore()
* are encouraged to provide a data store that implements
* this interface.
*
* This is not currently required to allow clients to use a generic cache
* store that does not itself depend on the annotated-command library.
* This might be required in a future version.
*/
interface SimpleCacheInterface
{
/**
* Test for an entry from the cache
* @param string $key
* @return boolean
*/
public function has($key);
/**
* Get an entry from the cache
* @param string $key
* @return array
*/
public function get($key);
/**
* Store an entry in the cache
* @param string $key
* @param array $data
*/
public function set($key, $data);
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Consolidation\AnnotatedCommand;
/**
* Command cration listeners can be added to the annotation
* command factory. These will be notified whenever a new
* commandfile is provided to the factory. This is useful for
* initializing new commandfile objects.
*
* @see AnnotatedCommandFactory::addListener()
*/
class CommandCreationListener implements CommandCreationListenerInterface
{
protected $listener;
public function __construct($listener)
{
$this->listener = $listener;
}
public function notifyCommandFileAdded($command)
{
call_user_func($this->listener, $command);
}
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Consolidation\AnnotatedCommand;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -18,6 +19,8 @@ class CommandData
protected $usesOutputInterface;
/** var boolean */
protected $includeOptionsInArgs;
/** var array */
protected $specialDefaults = [];
public function __construct(
AnnotationData $annotationData,
@@ -80,7 +83,83 @@ class CommandData
public function options()
{
return $this->input->getOptions();
// We cannot tell the difference between '--foo' (an option without
// a value) and the absence of '--foo' when the option has an optional
// value, and the current vallue of the option is 'null' using only
// the public methods of InputInterface. We'll try to figure out
// which is which by other means here.
$options = $this->getAdjustedOptions();
// Make two conversions here:
// --foo=0 wil convert $value from '0' to 'false' for binary options.
// --foo with $value of 'true' will be forced to 'false' if --no-foo exists.
foreach ($options as $option => $value) {
if ($this->shouldConvertOptionToFalse($options, $option, $value)) {
$options[$option] = false;
}
}
return $options;
}
/**
* Use 'hasParameterOption()' to attempt to disambiguate option states.
*/
protected function getAdjustedOptions()
{
$options = $this->input->getOptions();
// If Input isn't an ArgvInput, then return the options as-is.
if (!$this->input instanceof ArgvInput) {
return $options;
}
// If we have an ArgvInput, then we can determine if options
// are missing from the command line. If the option value is
// missing from $input, then we will keep the value `null`.
// If it is present, but has no explicit value, then change it its
// value to `true`.
foreach ($options as $option => $value) {
if (($value === null) && ($this->input->hasParameterOption("--$option"))) {
$options[$option] = true;
}
}
return $options;
}
protected function shouldConvertOptionToFalse($options, $option, $value)
{
// If the value is 'true' (e.g. the option is '--foo'), then convert
// it to false if there is also an option '--no-foo'. n.b. if the
// commandline has '--foo=bar' then $value will not be 'true', and
// --no-foo will be ignored.
if ($value === true) {
// Check if the --no-* option exists. Note that none of the other
// alteration apply in the $value == true case, so we can exit early here.
$negation_key = 'no-' . $option;
return array_key_exists($negation_key, $options) && $options[$negation_key];
}
// If the option is '--foo=0', convert the '0' to 'false' when appropriate.
if ($value !== '0') {
return false;
}
// The '--foo=0' convertion is only applicable when the default value
// is not in the special defaults list. i.e. you get a literal '0'
// when your default is a string.
return in_array($option, $this->specialDefaults);
}
public function cacheSpecialDefaults($definition)
{
foreach ($definition->getOptions() as $option => $inputOption) {
$defaultValue = $inputOption->getDefault();
if (($defaultValue === null) || ($defaultValue === true)) {
$this->specialDefaults[] = $option;
}
}
}
public function getArgsWithoutAppName()
@@ -93,14 +172,14 @@ class CommandData
// to the beginning.
array_shift($args);
if ($this->usesInputInterface) {
array_unshift($args, $this->input());
}
if ($this->usesOutputInterface) {
array_unshift($args, $this->output());
}
if ($this->usesInputInterface) {
array_unshift($args, $this->input());
}
return $args;
}

View File

@@ -40,6 +40,8 @@ class CommandFileDiscovery
protected $includeFilesAtBase = true;
/** @var integer */
protected $searchDepth = 2;
/** @var bool */
protected $followLinks = false;
public function __construct()
{
@@ -101,6 +103,16 @@ class CommandFileDiscovery
return $this;
}
/**
* Specify that the discovery object should follow symlinks. By
* default, symlinks are not followed.
*/
public function followLinks($followLinks = true)
{
$this->followLinks = $followLinks;
return $this;
}
/**
* Set the list of search locations to examine in each directory where
* command files may be found. This replaces whatever was there before.
@@ -325,6 +337,10 @@ class CommandFileDiscovery
$finder->exclude($item);
}
if ($this->followLinks) {
$finder->followLinks();
}
return $finder;
}
@@ -357,13 +373,14 @@ class CommandFileDiscovery
*/
protected function joinPaths(array $pathParts)
{
return $this->joinParts(
$path = $this->joinParts(
'/',
$pathParts,
function ($item) {
return !empty($item);
}
);
return str_replace(DIRECTORY_SEPARATOR, '/', $path);
}
/**
@@ -375,6 +392,12 @@ class CommandFileDiscovery
*/
protected function joinParts($delimiter, $parts, $filterFunction)
{
$parts = array_map(
function ($item) use ($delimiter) {
return rtrim($item, $delimiter);
},
$parts
);
return implode(
$delimiter,
array_filter($parts, $filterFunction)

View File

@@ -1,7 +1,9 @@
<?php
namespace Consolidation\AnnotatedCommand;
use Symfony\Component\Console\Command\Command;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\ReplaceCommandHookDispatcher;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
@@ -11,14 +13,24 @@ use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\Options\PrepareFormatter;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\InitializeHookDispatcher;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\OptionsHookDispatcher;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\InteractHookDispatcher;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\ValidateHookDispatcher;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\ProcessResultHookDispatcher;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\StatusDeterminerHookDispatcher;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\ExtracterHookDispatcher;
/**
* Process a command, including hooks and other callbacks.
* There should only be one command processor per application.
* Provide your command processor to the AnnotatedCommandFactory
* via AnnotatedCommandFactory::setCommandProcessor().
*/
class CommandProcessor
class CommandProcessor implements LoggerAwareInterface
{
use LoggerAwareTrait;
/** var HookManager */
protected $hookManager;
/** var FormatterManager */
@@ -27,6 +39,8 @@ class CommandProcessor
protected $displayErrorFunction;
/** var PrepareFormatterOptions[] */
protected $prepareOptionsList = [];
/** var boolean */
protected $passExceptions;
public function __construct(HookManager $hookManager)
{
@@ -59,6 +73,32 @@ class CommandProcessor
return $this;
}
/**
* Set a mode to make the annotated command library re-throw
* any exception that it catches while processing a command.
*
* The default behavior in the current (2.x) branch is to catch
* the exception and replace it with a CommandError object that
* may be processed by the normal output processing passthrough.
*
* In the 3.x branch, exceptions will never be caught; they will
* be passed through, as if setPassExceptions(true) were called.
* This is the recommended behavior.
*/
public function setPassExceptions($passExceptions)
{
$this->passExceptions = $passExceptions;
return $this;
}
public function commandErrorForException(\Exception $e)
{
if ($this->passExceptions) {
throw $e;
}
return new CommandError($e->getMessage(), $e->getCode());
}
/**
* Return the formatter manager
* @return FormatterManager
@@ -73,7 +113,8 @@ class CommandProcessor
$names,
AnnotationData $annotationData
) {
return $this->hookManager()->initializeHook($input, $names, $annotationData);
$initializeDispatcher = new InitializeHookDispatcher($this->hookManager(), $names);
return $initializeDispatcher->initialize($input, $annotationData);
}
public function optionsHook(
@@ -81,7 +122,8 @@ class CommandProcessor
$names,
AnnotationData $annotationData
) {
$this->hookManager()->optionsHook($command, $names, $annotationData);
$optionsDispatcher = new OptionsHookDispatcher($this->hookManager(), $names);
$optionsDispatcher->getOptions($command, $annotationData);
}
public function interact(
@@ -90,7 +132,8 @@ class CommandProcessor
$names,
AnnotationData $annotationData
) {
return $this->hookManager()->interact($input, $output, $names, $annotationData);
$interactDispatcher = new InteractHookDispatcher($this->hookManager(), $names);
return $interactDispatcher->interact($input, $output, $annotationData);
}
public function process(
@@ -108,7 +151,7 @@ class CommandProcessor
);
return $this->handleResults($output, $names, $result, $commandData);
} catch (\Exception $e) {
$result = new CommandError($e->getMessage(), $e->getCode());
$result = $this->commandErrorForException($e);
return $this->handleResults($output, $names, $result, $commandData);
}
}
@@ -120,11 +163,20 @@ class CommandProcessor
) {
// Validators return any object to signal a validation error;
// if the return an array, it replaces the arguments.
$validated = $this->hookManager()->validateArguments($names, $commandData);
$validateDispatcher = new ValidateHookDispatcher($this->hookManager(), $names);
$validated = $validateDispatcher->validate($commandData);
if (is_object($validated)) {
return $validated;
}
$replaceDispatcher = new ReplaceCommandHookDispatcher($this->hookManager(), $names);
if ($this->logger) {
$replaceDispatcher->setLogger($this->logger);
}
if ($replaceDispatcher->hasReplaceCommandHook()) {
$commandCallback = $replaceDispatcher->getReplacementCommand($commandData);
}
// Run the command, alter the results, and then handle output and status
$result = $this->runCommandCallback($commandCallback, $commandData);
return $this->processResults($names, $result, $commandData);
@@ -132,7 +184,8 @@ class CommandProcessor
public function processResults($names, $result, CommandData $commandData)
{
return $this->hookManager()->alterResult($names, $result, $commandData);
$processDispatcher = new ProcessResultHookDispatcher($this->hookManager(), $names);
return $processDispatcher->process($result, $commandData);
}
/**
@@ -140,7 +193,8 @@ class CommandProcessor
*/
public function handleResults(OutputInterface $output, $names, $result, CommandData $commandData)
{
$status = $this->hookManager()->determineStatusCode($names, $result);
$statusCodeDispatcher = new StatusDeterminerHookDispatcher($this->hookManager(), $names);
$status = $statusCodeDispatcher->determineStatusCode($result);
// If the result is an integer and no separate status code was provided, then use the result as the status and do no output.
if (is_integer($result) && !isset($status)) {
return $result;
@@ -148,7 +202,8 @@ class CommandProcessor
$status = $this->interpretStatusCode($status);
// Get the structured output, the output stream and the formatter
$structuredOutput = $this->hookManager()->extractOutput($names, $result);
$extractDispatcher = new ExtracterHookDispatcher($this->hookManager(), $names);
$structuredOutput = $extractDispatcher->extractOutput($result);
$output = $this->chooseOutputStream($output, $status);
if ($status != 0) {
return $this->writeErrorMessage($output, $status, $structuredOutput, $result);
@@ -179,7 +234,7 @@ class CommandProcessor
$args = $commandData->getArgsAndOptions();
$result = call_user_func_array($commandCallback, $args);
} catch (\Exception $e) {
$result = new CommandError($e->getMessage(), $e->getCode());
$result = $this->commandErrorForException($e);
}
return $result;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Consolidation\AnnotatedCommand\Events;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
interface CustomEventAwareInterface
{
/**
* Set a reference to the hook manager for later use
* @param HookManager $hookManager
*/
public function setHookManager(HookManager $hookManager);
/**
* Get all of the defined event handlers of the specified name.
* @param string $eventName
* @return Callable[]
*/
public function getCustomEventHandlers($eventName);
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Consolidation\AnnotatedCommand\Events;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
trait CustomEventAwareTrait
{
/** var HookManager */
protected $hookManager;
/**
* {@inheritdoc}
*/
public function setHookManager(HookManager $hookManager)
{
$this->hookManager = $hookManager;
}
/**
* {@inheritdoc}
*/
public function getCustomEventHandlers($eventName)
{
if (!$this->hookManager) {
return [];
}
return $this->hookManager->getHook($eventName, HookManager::ON_EVENT);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Consolidation\AnnotatedCommand\Help;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Descriptor\XmlDescriptor;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class HelpCommand
{
/** var Application */
protected $application;
/**
* Create a help document from a Symfony Console command
*/
public function __construct(Application $application)
{
$this->application = $application;
}
public function getApplication()
{
return $this->application;
}
/**
* Run the help command
*
* @command my-help
* @return \Consolidation\AnnotatedCommand\Help\HelpDocument
*/
public function help($commandName = 'help')
{
$command = $this->getApplication()->find($commandName);
$helpDocument = $this->getHelpDocument($command);
return $helpDocument;
}
/**
* Create a help document.
*/
protected function getHelpDocument($command)
{
return new HelpDocument($command);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Consolidation\AnnotatedCommand\Help;
use Consolidation\OutputFormatters\StructuredData\Xml\DomDataInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Descriptor\XmlDescriptor;
class HelpDocument implements DomDataInterface
{
/** var Command */
protected $command;
/** var \DOMDocument */
protected $dom;
/**
* Create a help document from a Symfony Console command
*/
public function __construct(Command $command)
{
$dom = $this->generateBaseHelpDom($command);
$dom = $this->alterHelpDocument($command, $dom);
$this->command = $command;
$this->dom = $dom;
}
/**
* Convert data into a \DomDocument.
*
* @return \DomDocument
*/
public function getDomData()
{
return $this->dom;
}
/**
* Create the base help DOM prior to alteration by the Command object.
* @param Command $command
* @return \DomDocument
*/
protected function generateBaseHelpDom(Command $command)
{
// Use Symfony to generate xml text. If other formats are
// requested, convert from xml to the desired form.
$descriptor = new XmlDescriptor();
return $descriptor->getCommandDocument($command);
}
/**
* Alter the DOM document per the command object
* @param Command $command
* @param \DomDocument $dom
* @return \DomDocument
*/
protected function alterHelpDocument(Command $command, \DomDocument $dom)
{
if ($command instanceof HelpDocumentAlter) {
$dom = $command->helpAlter($dom);
}
return $dom;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Consolidation\AnnotatedCommand\Help;
interface HelpDocumentAlter
{
public function helpAlter(\DomDocument $dom);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Call hooks
*/
class CommandEventHookDispatcher extends HookDispatcher
{
/**
* @param ConsoleCommandEvent $event
*/
public function callCommandEventHooks(ConsoleCommandEvent $event)
{
$hooks = [
HookManager::PRE_COMMAND_EVENT,
HookManager::COMMAND_EVENT,
HookManager::POST_COMMAND_EVENT
];
$commandEventHooks = $this->getHooks($hooks);
foreach ($commandEventHooks as $commandEvent) {
if ($commandEvent instanceof EventDispatcherInterface) {
$commandEvent->dispatch(ConsoleEvents::COMMAND, $event);
}
if (is_callable($commandEvent)) {
$commandEvent($event);
}
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\Hooks\ExtractOutputInterface;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\OutputDataInterface;
/**
* Call hooks
*/
class ExtracterHookDispatcher extends HookDispatcher implements ExtractOutputInterface
{
/**
* Convert the result object to printable output in
* structured form.
*/
public function extractOutput($result)
{
if ($result instanceof OutputDataInterface) {
return $result->getOutputData();
}
$hooks = [
HookManager::EXTRACT_OUTPUT,
];
$extractors = $this->getHooks($hooks);
foreach ($extractors as $extractor) {
$structuredOutput = $this->callExtractor($extractor, $result);
if (isset($structuredOutput)) {
return $structuredOutput;
}
}
return $result;
}
protected function callExtractor($extractor, $result)
{
if ($extractor instanceof ExtractOutputInterface) {
return $extractor->extractOutput($result);
}
if (is_callable($extractor)) {
return $extractor($result);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\AnnotationData;
/**
* Call hooks
*/
class HookDispatcher
{
/** var HookManager */
protected $hookManager;
protected $names;
public function __construct(HookManager $hookManager, $names)
{
$this->hookManager = $hookManager;
$this->names = $names;
}
public function getHooks($hooks, $annotationData = null)
{
return $this->hookManager->getHooks($this->names, $hooks, $annotationData);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\AnnotationData;
use Consolidation\AnnotatedCommand\Hooks\InitializeHookInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
/**
* Call hooks
*/
class InitializeHookDispatcher extends HookDispatcher implements InitializeHookInterface
{
public function initialize(
InputInterface $input,
AnnotationData $annotationData
) {
$hooks = [
HookManager::PRE_INITIALIZE,
HookManager::INITIALIZE,
HookManager::POST_INITIALIZE
];
$providers = $this->getHooks($hooks, $annotationData);
foreach ($providers as $provider) {
$this->callInitializeHook($provider, $input, $annotationData);
}
}
protected function callInitializeHook($provider, $input, AnnotationData $annotationData)
{
if ($provider instanceof InitializeHookInterface) {
return $provider->initialize($input, $annotationData);
}
if (is_callable($provider)) {
return $provider($input, $annotationData);
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\AnnotationData;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\Hooks\InteractorInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Call hooks
*/
class InteractHookDispatcher extends HookDispatcher
{
public function interact(
InputInterface $input,
OutputInterface $output,
AnnotationData $annotationData
) {
$hooks = [
HookManager::PRE_INTERACT,
HookManager::INTERACT,
HookManager::POST_INTERACT
];
$interactors = $this->getHooks($hooks, $annotationData);
foreach ($interactors as $interactor) {
$this->callInteractor($interactor, $input, $output, $annotationData);
}
}
protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData)
{
if ($interactor instanceof InteractorInterface) {
return $interactor->interact($input, $output, $annotationData);
}
if (is_callable($interactor)) {
return $interactor($input, $output, $annotationData);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Symfony\Component\Console\Command\Command;
use Consolidation\AnnotatedCommand\AnnotatedCommand;
use Consolidation\AnnotatedCommand\AnnotationData;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\Hooks\OptionHookInterface;
/**
* Call hooks
*/
class OptionsHookDispatcher extends HookDispatcher implements OptionHookInterface
{
public function getOptions(
Command $command,
AnnotationData $annotationData
) {
$hooks = [
HookManager::PRE_OPTION_HOOK,
HookManager::OPTION_HOOK,
HookManager::POST_OPTION_HOOK
];
$optionHooks = $this->getHooks($hooks, $annotationData);
foreach ($optionHooks as $optionHook) {
$this->callOptionHook($optionHook, $command, $annotationData);
}
$commandInfoList = $this->hookManager->getHookOptionsForCommand($command);
if ($command instanceof AnnotatedCommand) {
$command->optionsHookForHookAnnotations($commandInfoList);
}
}
protected function callOptionHook($optionHook, $command, AnnotationData $annotationData)
{
if ($optionHook instanceof OptionHookInterface) {
return $optionHook->getOptions($command, $annotationData);
}
if (is_callable($optionHook)) {
return $optionHook($command, $annotationData);
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\AnnotationData;
use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\Hooks\ProcessResultInterface;
/**
* Call hooks
*/
class ProcessResultHookDispatcher extends HookDispatcher implements ProcessResultInterface
{
/**
* Process result and decide what to do with it.
* Allow client to add transformation / interpretation
* callbacks.
*/
public function process($result, CommandData $commandData)
{
$hooks = [
HookManager::PRE_PROCESS_RESULT,
HookManager::PROCESS_RESULT,
HookManager::POST_PROCESS_RESULT,
HookManager::PRE_ALTER_RESULT,
HookManager::ALTER_RESULT,
HookManager::POST_ALTER_RESULT,
HookManager::POST_COMMAND_HOOK,
];
$processors = $this->getHooks($hooks, $commandData->annotationData());
foreach ($processors as $processor) {
$result = $this->callProcessor($processor, $result, $commandData);
}
return $result;
}
protected function callProcessor($processor, $result, CommandData $commandData)
{
$processed = null;
if ($processor instanceof ProcessResultInterface) {
$processed = $processor->process($result, $commandData);
}
if (is_callable($processor)) {
$processed = $processor($result, $commandData);
}
if (isset($processed)) {
return $processed;
}
return $result;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
/**
* Call hooks.
*/
class ReplaceCommandHookDispatcher extends HookDispatcher implements LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @return int
*/
public function hasReplaceCommandHook()
{
return (bool) count($this->getReplaceCommandHooks());
}
/**
* @return \callable[]
*/
public function getReplaceCommandHooks()
{
$hooks = [
HookManager::REPLACE_COMMAND_HOOK,
];
$replaceCommandHooks = $this->getHooks($hooks);
return $replaceCommandHooks;
}
/**
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
*
* @return callable
*/
public function getReplacementCommand(CommandData $commandData)
{
$replaceCommandHooks = $this->getReplaceCommandHooks();
// We only take the first hook implementation of "replace-command" as the replacement. Commands shouldn't have
// more than one replacement.
$replacementCommand = reset($replaceCommandHooks);
if ($this->logger && count($replaceCommandHooks) > 1) {
$command_name = $commandData->annotationData()->get('command', 'unknown');
$message = "Multiple implementations of the \"replace - command\" hook exist for the \"$command_name\" command.\n";
foreach ($replaceCommandHooks as $replaceCommandHook) {
$class = get_class($replaceCommandHook[0]);
$method = $replaceCommandHook[1];
$hook_name = "$class->$method";
$message .= " - $hook_name\n";
}
$this->logger->warning($message);
}
return $replacementCommand;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\ExitCodeInterface;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\Hooks\StatusDeterminerInterface;
/**
* Call hooks
*/
class StatusDeterminerHookDispatcher extends HookDispatcher implements StatusDeterminerInterface
{
/**
* Call all status determiners, and see if any of them
* know how to convert to a status code.
*/
public function determineStatusCode($result)
{
// If the result (post-processing) is an object that
// implements ExitCodeInterface, then we will ask it
// to give us the status code.
if ($result instanceof ExitCodeInterface) {
return $result->getExitCode();
}
$hooks = [
HookManager::STATUS_DETERMINER,
];
// If the result does not implement ExitCodeInterface,
// then we'll see if there is a determiner that can
// extract a status code from the result.
$determiners = $this->getHooks($hooks);
foreach ($determiners as $determiner) {
$status = $this->callDeterminer($determiner, $result);
if (isset($status)) {
return $status;
}
}
}
protected function callDeterminer($determiner, $result)
{
if ($determiner instanceof StatusDeterminerInterface) {
return $determiner->determineStatusCode($result);
}
if (is_callable($determiner)) {
return $determiner($result);
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Consolidation\AnnotatedCommand\Hooks\Dispatchers;
use Consolidation\AnnotatedCommand\AnnotationData;
use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\AnnotatedCommand\CommandError;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\AnnotatedCommand\Hooks\ValidatorInterface;
/**
* Call hooks
*/
class ValidateHookDispatcher extends HookDispatcher implements ValidatorInterface
{
public function validate(CommandData $commandData)
{
$hooks = [
HookManager::PRE_ARGUMENT_VALIDATOR,
HookManager::ARGUMENT_VALIDATOR,
HookManager::POST_ARGUMENT_VALIDATOR,
HookManager::PRE_COMMAND_HOOK,
HookManager::COMMAND_HOOK,
];
$validators = $this->getHooks($hooks, $commandData->annotationData());
foreach ($validators as $validator) {
$validated = $this->callValidator($validator, $commandData);
if ($validated === false) {
return new CommandError();
}
if (is_object($validated)) {
return $validated;
}
}
}
protected function callValidator($validator, CommandData $commandData)
{
if ($validator instanceof ValidatorInterface) {
return $validator->validate($commandData);
}
if (is_callable($validator)) {
return $validator($commandData);
}
}
}

View File

@@ -8,12 +8,15 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Consolidation\AnnotatedCommand\ExitCodeInterface;
use Consolidation\AnnotatedCommand\OutputDataInterface;
use Consolidation\AnnotatedCommand\AnnotationData;
use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\AnnotatedCommand\CommandError;
use Consolidation\AnnotatedCommand\Hooks\Dispatchers\CommandEventHookDispatcher;
/**
* Manage named callback hooks
@@ -24,6 +27,7 @@ class HookManager implements EventSubscriberInterface
/** var CommandInfo[] */
protected $hookOptions = [];
const REPLACE_COMMAND_HOOK = 'replace-command';
const PRE_COMMAND_EVENT = 'pre-command-event';
const COMMAND_EVENT = 'command-event';
const POST_COMMAND_EVENT = 'post-command-event';
@@ -50,6 +54,7 @@ class HookManager implements EventSubscriberInterface
const POST_ALTER_RESULT = 'post-alter';
const STATUS_DETERMINER = 'status';
const EXTRACT_OUTPUT = 'extract';
const ON_EVENT = 'on-event';
public function __construct()
{
@@ -117,6 +122,44 @@ class HookManager implements EventSubscriberInterface
return $reflectionClass->getName();
}
/**
* Add a replace command hook
*
* @param type ReplaceCommandHookInterface $provider
* @param type string $command_name The name of the command to replace
*/
public function addReplaceCommandHook(ReplaceCommandHookInterface $replaceCommandHook, $name)
{
$this->hooks[$name][self::REPLACE_COMMAND_HOOK][] = $replaceCommandHook;
return $this;
}
public function addPreCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*')
{
$this->hooks[$name][self::PRE_COMMAND_EVENT][] = $eventDispatcher;
return $this;
}
public function addCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*')
{
$this->hooks[$name][self::COMMAND_EVENT][] = $eventDispatcher;
return $this;
}
public function addPostCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*')
{
$this->hooks[$name][self::POST_COMMAND_EVENT][] = $eventDispatcher;
return $this;
}
public function addCommandEvent(EventSubscriberInterface $eventSubscriber)
{
// Wrap the event subscriber in a dispatcher and add it
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber($eventSubscriber);
return $this->addCommandEventDispatcher($dispatcher);
}
/**
* Add an configuration provider hook
*
@@ -281,30 +324,6 @@ class HookManager implements EventSubscriberInterface
return $this;
}
public function initializeHook(
InputInterface $input,
$names,
AnnotationData $annotationData
) {
$providers = $this->getInitializeHooks($names, $annotationData);
foreach ($providers as $provider) {
$this->callInjectConfigurationHook($provider, $input, $annotationData);
}
}
public function optionsHook(
\Consolidation\AnnotatedCommand\AnnotatedCommand $command,
$names,
AnnotationData $annotationData
) {
$optionHooks = $this->getOptionHooks($names, $annotationData);
foreach ($optionHooks as $optionHook) {
$this->callOptionHook($optionHook, $command, $annotationData);
}
$commandInfoList = $this->getHookOptionsForCommand($command);
$command->optionsHookForHookAnnotations($commandInfoList);
}
public function getHookOptionsForCommand($command)
{
$names = $this->addWildcardHooksToNames($command->getNames(), $command->getAnnotationData());
@@ -325,210 +344,6 @@ class HookManager implements EventSubscriberInterface
return $result;
}
public function interact(
InputInterface $input,
OutputInterface $output,
$names,
AnnotationData $annotationData
) {
$interactors = $this->getInteractors($names, $annotationData);
foreach ($interactors as $interactor) {
$this->callInteractor($interactor, $input, $output, $annotationData);
}
}
public function validateArguments($names, CommandData $commandData)
{
$validators = $this->getValidators($names, $commandData->annotationData());
foreach ($validators as $validator) {
$validated = $this->callValidator($validator, $commandData);
if ($validated === false) {
return new CommandError();
}
if (is_object($validated)) {
return $validated;
}
}
}
/**
* Process result and decide what to do with it.
* Allow client to add transformation / interpretation
* callbacks.
*/
public function alterResult($names, $result, CommandData $commandData)
{
$processors = $this->getProcessResultHooks($names, $commandData->annotationData());
foreach ($processors as $processor) {
$result = $this->callProcessor($processor, $result, $commandData);
}
$alterers = $this->getAlterResultHooks($names, $commandData->annotationData());
foreach ($alterers as $alterer) {
$result = $this->callProcessor($alterer, $result, $commandData);
}
return $result;
}
/**
* Call all status determiners, and see if any of them
* know how to convert to a status code.
*/
public function determineStatusCode($names, $result)
{
// If the result (post-processing) is an object that
// implements ExitCodeInterface, then we will ask it
// to give us the status code.
if ($result instanceof ExitCodeInterface) {
return $result->getExitCode();
}
// If the result does not implement ExitCodeInterface,
// then we'll see if there is a determiner that can
// extract a status code from the result.
$determiners = $this->getStatusDeterminers($names);
foreach ($determiners as $determiner) {
$status = $this->callDeterminer($determiner, $result);
if (isset($status)) {
return $status;
}
}
}
/**
* Convert the result object to printable output in
* structured form.
*/
public function extractOutput($names, $result)
{
if ($result instanceof OutputDataInterface) {
return $result->getOutputData();
}
$extractors = $this->getOutputExtractors($names);
foreach ($extractors as $extractor) {
$structuredOutput = $this->callExtractor($extractor, $result);
if (isset($structuredOutput)) {
return $structuredOutput;
}
}
return $result;
}
protected function getCommandEventHooks($names)
{
return $this->getHooks(
$names,
[
self::PRE_COMMAND_EVENT,
self::COMMAND_EVENT,
self::POST_COMMAND_EVENT
]
);
}
protected function getInitializeHooks($names, AnnotationData $annotationData)
{
return $this->getHooks(
$names,
[
self::PRE_INITIALIZE,
self::INITIALIZE,
self::POST_INITIALIZE
],
$annotationData
);
}
protected function getOptionHooks($names, AnnotationData $annotationData)
{
return $this->getHooks(
$names,
[
self::PRE_OPTION_HOOK,
self::OPTION_HOOK,
self::POST_OPTION_HOOK
],
$annotationData
);
}
protected function getInteractors($names, AnnotationData $annotationData)
{
return $this->getHooks(
$names,
[
self::PRE_INTERACT,
self::INTERACT,
self::POST_INTERACT
],
$annotationData
);
}
protected function getValidators($names, AnnotationData $annotationData)
{
return $this->getHooks(
$names,
[
self::PRE_ARGUMENT_VALIDATOR,
self::ARGUMENT_VALIDATOR,
self::POST_ARGUMENT_VALIDATOR,
self::PRE_COMMAND_HOOK,
self::COMMAND_HOOK,
],
$annotationData
);
}
protected function getProcessResultHooks($names, AnnotationData $annotationData)
{
return $this->getHooks(
$names,
[
self::PRE_PROCESS_RESULT,
self::PROCESS_RESULT,
self::POST_PROCESS_RESULT
],
$annotationData
);
}
protected function getAlterResultHooks($names, AnnotationData $annotationData)
{
return $this->getHooks(
$names,
[
self::PRE_ALTER_RESULT,
self::ALTER_RESULT,
self::POST_ALTER_RESULT,
self::POST_COMMAND_HOOK,
],
$annotationData
);
}
protected function getStatusDeterminers($names)
{
return $this->getHooks(
$names,
[
self::STATUS_DETERMINER,
]
);
}
protected function getOutputExtractors($names)
{
return $this->getHooks(
$names,
[
self::EXTRACT_OUTPUT,
]
);
}
/**
* Get a set of hooks with the provided name(s). Include the
* pre- and post- hooks, and also include the global hooks ('*')
@@ -583,7 +398,7 @@ class HookManager implements EventSubscriberInterface
*
* @return callable[]
*/
protected function getHook($name, $hook)
public function getHook($name, $hook)
{
if (isset($this->hooks[$name][$hook])) {
return $this->hooks[$name][$hook];
@@ -591,103 +406,22 @@ class HookManager implements EventSubscriberInterface
return [];
}
protected function callInjectConfigurationHook($provider, $input, AnnotationData $annotationData)
{
if ($provider instanceof InitializeHookInterface) {
return $provider->applyConfiguration($input, $annotationData);
}
if (is_callable($provider)) {
return $provider($input, $annotationData);
}
}
protected function callOptionHook($optionHook, $command, AnnotationData $annotationData)
{
if ($optionHook instanceof OptionHookInterface) {
return $optionHook->getOptions($command, $annotationData);
}
if (is_callable($optionHook)) {
return $optionHook($command, $annotationData);
}
}
protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData)
{
if ($interactor instanceof InteractorInterface) {
return $interactor->interact($input, $output, $annotationData);
}
if (is_callable($interactor)) {
return $interactor($input, $output, $annotationData);
}
}
protected function callValidator($validator, CommandData $commandData)
{
if ($validator instanceof ValidatorInterface) {
return $validator->validate($commandData);
}
if (is_callable($validator)) {
return $validator($commandData);
}
}
protected function callProcessor($processor, $result, CommandData $commandData)
{
$processed = null;
if ($processor instanceof ProcessResultInterface) {
$processed = $processor->process($result, $commandData);
}
if (is_callable($processor)) {
$processed = $processor($result, $commandData);
}
if (isset($processed)) {
return $processed;
}
return $result;
}
protected function callDeterminer($determiner, $result)
{
if ($determiner instanceof StatusDeterminerInterface) {
return $determiner->determineStatusCode($result);
}
if (is_callable($determiner)) {
return $determiner($result);
}
}
protected function callExtractor($extractor, $result)
{
if ($extractor instanceof ExtractOutputInterface) {
return $extractor->extractOutput($result);
}
if (is_callable($extractor)) {
return $extractor($result);
}
}
/**
* Call the command event hooks.
*
* TODO: This should be moved to CommandEventHookDispatcher, which
* should become the class that implements EventSubscriberInterface.
* This change would break all clients, though, so postpone until next
* major release.
*
* @param ConsoleCommandEvent $event
*/
public function callCommandEventHooks(ConsoleCommandEvent $event)
{
/* @var Command $command */
$command = $event->getCommand();
$names = [$command->getName()];
$commandEventHooks = $this->getCommandEventHooks($names);
foreach ($commandEventHooks as $commandEvent) {
if (is_callable($commandEvent)) {
$commandEvent($event);
}
}
}
public function findAndAddHookOptions($command)
{
if (!$command instanceof \Consolidation\AnnotatedCommand\AnnotatedCommand) {
return;
}
$command->optionsHook();
$dispatcher = new CommandEventHookDispatcher($this, [$command->getName()]);
$dispatcher->callCommandEventHooks($event);
}
/**

View File

@@ -11,5 +11,5 @@ use Symfony\Component\Console\Input\InputInterface;
*/
interface InitializeHookInterface
{
public function initialize(InputInterface $input, Annotation $annotationData);
public function initialize(InputInterface $input, AnnotationData $annotationData);
}

View File

@@ -58,7 +58,14 @@ class AlterOptionsCommandEvent implements EventSubscriberInterface
$input->bind($command->getDefinition());
}
// Symfony Console helpfully swaps 'command_name' and 'command'
// depending on whether the user entered `help foo` or `--help foo`.
// One of these is always `help`, and the other is the command we
// are actually interested in.
$nameOfCommandToDescribe = $event->getInput()->getArgument('command_name');
if ($nameOfCommandToDescribe == 'help') {
$nameOfCommandToDescribe = $event->getInput()->getArgument('command');
}
$commandToDescribe = $this->application->find($nameOfCommandToDescribe);
$this->findAndAddHookOptions($commandToDescribe);
} else {

View File

@@ -10,6 +10,8 @@ class PrepareTerminalWidthOption implements PrepareFormatter
/** var Application */
protected $application;
protected $terminal;
/** var int */
protected $defaultWidth;
@@ -19,6 +21,9 @@ class PrepareTerminalWidthOption implements PrepareFormatter
/** var int */
protected $minWidth = 0;
/* var boolean */
protected $shouldWrap = true;
public function __construct($defaultWidth = 0)
{
$this->defaultWidth = $defaultWidth;
@@ -29,6 +34,24 @@ class PrepareTerminalWidthOption implements PrepareFormatter
$this->application = $application;
}
public function setTerminal($terminal)
{
$this->terminal = $terminal;
}
public function getTerminal()
{
if (!$this->terminal && class_exists('\Symfony\Component\Console\Terminal')) {
$this->terminal = new \Symfony\Component\Console\Terminal();
}
return $this->terminal;
}
public function enableWrap($shouldWrap)
{
$this->shouldWrap = $shouldWrap;
}
public function prepare(CommandData $commandData, FormatterOptions $options)
{
$width = $this->getTerminalWidth();
@@ -45,10 +68,24 @@ class PrepareTerminalWidthOption implements PrepareFormatter
protected function getTerminalWidth()
{
if (!$this->application) {
// Don't wrap if wrapping has been disabled.
if (!$this->shouldWrap) {
return 0;
}
$terminal = $this->getTerminal();
if ($terminal) {
return $terminal->getWidth();
}
return $this->getTerminalWidthViaApplication();
}
protected function getTerminalWidthViaApplication()
{
if (!$this->application) {
return 0;
}
$dimensions = $this->application->getTerminalDimensions();
if ($dimensions[0] == null) {
return 0;

View File

@@ -17,6 +17,11 @@ use Consolidation\AnnotatedCommand\AnnotationData;
*/
class CommandInfo
{
/**
* Serialization schema version. Incremented every time the serialization schema changes.
*/
const SERIALIZATION_SCHEMA_VERSION = 3;
/**
* @var \ReflectionMethod
*/
@@ -26,7 +31,7 @@ class CommandInfo
* @var boolean
* @var string
*/
protected $docBlockIsParsed;
protected $docBlockIsParsed = false;
/**
* @var string
@@ -68,6 +73,11 @@ class CommandInfo
*/
protected $aliases = [];
/**
* @var InputOption[]
*/
protected $inputOptions;
/**
* @var string
*/
@@ -78,33 +88,59 @@ class CommandInfo
*/
protected $returnType;
/**
* @var string
*/
protected $optionParamName;
/**
* Create a new CommandInfo class for a particular method of a class.
*
* @param string|mixed $classNameOrInstance The name of a class, or an
* instance of it.
* instance of it, or an array of cached data.
* @param string $methodName The name of the method to get info about.
* @param array $cache Cached data
* @deprecated Use CommandInfo::create() or CommandInfo::deserialize()
* instead. In the future, this constructor will be protected.
*/
public function __construct($classNameOrInstance, $methodName)
public function __construct($classNameOrInstance, $methodName, $cache = [])
{
$this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
$this->methodName = $methodName;
$this->arguments = new DefaultsWithDescriptions();
$this->options = new DefaultsWithDescriptions();
// If the cache came from a newer version, ignore it and
// regenerate the cached information.
if (!empty($cache) && CommandInfoDeserializer::isValidSerializedData($cache) && !$this->cachedFileIsModified($cache)) {
$deserializer = new CommandInfoDeserializer();
$deserializer->constructFromCache($this, $cache);
$this->docBlockIsParsed = true;
} else {
$this->constructFromClassAndMethod($classNameOrInstance, $methodName);
}
}
public static function create($classNameOrInstance, $methodName)
{
return new self($classNameOrInstance, $methodName);
}
public static function deserialize($cache)
{
$cache = (array)$cache;
return new self($cache['class'], $cache['method_name'], $cache);
}
public function cachedFileIsModified($cache)
{
$path = $this->reflection->getFileName();
return filemtime($path) != $cache['mtime'];
}
protected function constructFromClassAndMethod($classNameOrInstance, $methodName)
{
$this->otherAnnotations = new AnnotationData();
// Set up a default name for the command from the method name.
// This can be overridden via @command or @name annotations.
$this->name = $this->convertName($this->reflection->name);
$this->name = $this->convertName($methodName);
$this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false);
$this->arguments = $this->determineAgumentClassifications();
// Remember the name of the last parameter, if it holds the options.
// We will use this information to ignore @param annotations for the options.
if (!empty($this->options)) {
$this->optionParamName = $this->lastParameterName();
}
}
/**
@@ -139,6 +175,28 @@ class CommandInfo
return $this;
}
/**
* Return whether or not this method represents a valid command
* or hook.
*/
public function valid()
{
return !empty($this->name);
}
/**
* If higher-level code decides that this CommandInfo is not interesting
* or useful (if it is not a command method or a hook method), then
* we will mark it as invalid to prevent it from being created as a command.
* We still cache a placeholder record for invalid methods, so that we
* do not need to re-parse the method again later simply to determine that
* it is invalid.
*/
public function invalidate()
{
$this->name = '';
}
public function getReturnType()
{
$this->parseDocBlock();
@@ -164,6 +222,15 @@ class CommandInfo
return $this->otherAnnotations;
}
/**
* Replace the annotation data.
*/
public function replaceRawAnnotations($annotationData)
{
$this->otherAnnotations = new AnnotationData((array) $annotationData);
return $this;
}
/**
* Get any annotations included in the docblock comment,
* also including default values such as @command. We add
@@ -176,27 +243,49 @@ class CommandInfo
*/
public function getAnnotations()
{
// Also provide the path to the commandfile that these annotations
// were pulled from and the classname of that file.
$path = $this->reflection->getFileName();
$className = $this->reflection->getDeclaringClass()->getName();
return new AnnotationData(
$this->getRawAnnotations()->getArrayCopy() +
[
'command' => $this->getName(),
'_path' => $path,
'_classname' => $className,
]
);
}
/**
* Return a specific named annotation for this command.
* Return a specific named annotation for this command as a list.
*
* @param string $annotation The name of the annotation.
* @return string
* @param string $name The name of the annotation.
* @return array|null
*/
public function getAnnotation($annotation)
public function getAnnotationList($name)
{
// hasAnnotation parses the docblock
if (!$this->hasAnnotation($annotation)) {
if (!$this->hasAnnotation($name)) {
return null;
}
return $this->otherAnnotations[$annotation];
return $this->otherAnnotations->getList($name);
;
}
/**
* Return a specific named annotation for this command as a string.
*
* @param string $name The name of the annotation.
* @return string|null
*/
public function getAnnotation($name)
{
// hasAnnotation parses the docblock
if (!$this->hasAnnotation($name)) {
return null;
}
return $this->otherAnnotations->get($name);
}
/**
@@ -217,6 +306,11 @@ class CommandInfo
*/
public function addAnnotation($name, $content)
{
// Convert to an array and merge if there are multiple
// instances of the same annotation defined.
if (isset($this->otherAnnotations[$name])) {
$content = array_merge((array) $this->otherAnnotations[$name], (array)$content);
}
$this->otherAnnotations[$name] = $content;
}
@@ -246,7 +340,7 @@ class CommandInfo
*/
public function setDescription($description)
{
$this->description = $description;
$this->description = str_replace("\n", ' ', $description);
return $this;
}
@@ -293,6 +387,27 @@ class CommandInfo
return $this;
}
/**
* Get hidden status for the command.
* @return bool
*/
public function getHidden()
{
$this->parseDocBlock();
return $this->hasAnnotation('hidden');
}
/**
* Set hidden status. List command omits hidden commands.
*
* @param bool $hidden
*/
public function setHidden($hidden)
{
$this->hidden = $hidden;
return $this;
}
/**
* Return the examples for this command. This is @usage instead of
* @example because the later is defined by the phpdoc standard to
@@ -319,6 +434,29 @@ class CommandInfo
return $this;
}
/**
* Overwrite all example usages
*/
public function replaceExampleUsages($usages)
{
$this->exampleUsage = $usages;
return $this;
}
/**
* Return the topics for this command.
*
* @return string[]
*/
public function getTopics()
{
if (!$this->hasAnnotation('topics')) {
return [];
}
$topics = $this->getAnnotation('topics');
return explode(',', trim($topics));
}
/**
* Return the list of refleaction parameters.
*
@@ -349,14 +487,6 @@ class CommandInfo
return $this->options;
}
/**
* Return the name of the last parameter if it holds the options.
*/
public function optionParamName()
{
return $this->optionParamName;
}
/**
* Get the inputOptions for the options associated with this CommandInfo
* object, e.g. via @option annotations, or from
@@ -366,8 +496,31 @@ class CommandInfo
* @return InputOption[]
*/
public function inputOptions()
{
if (!isset($this->inputOptions)) {
$this->inputOptions = $this->createInputOptions();
}
return $this->inputOptions;
}
protected function addImplicitNoOptions()
{
$opts = $this->options()->getValues();
foreach ($opts as $name => $defaultValue) {
if ($defaultValue === true) {
$key = 'no-' . $name;
if (!array_key_exists($key, $opts)) {
$description = "Negate --$name option.";
$this->options()->add($key, $description, false);
}
}
}
}
protected function createInputOptions()
{
$explicitOptions = [];
$this->addImplicitNoOptions();
$opts = $this->options()->getValues();
foreach ($opts as $name => $defaultValue) {
@@ -379,10 +532,28 @@ class CommandInfo
list($fullName, $shortcut) = explode('|', $name, 2);
}
if (is_bool($defaultValue)) {
// Treat the following two cases identically:
// - 'foo' => InputOption::VALUE_OPTIONAL
// - 'foo' => null
// The first form is preferred, but we will convert the value
// to 'null' for storage as the option default value.
if ($defaultValue === InputOption::VALUE_OPTIONAL) {
$defaultValue = null;
}
if ($defaultValue === false) {
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
} elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
} elseif (is_array($defaultValue)) {
$optionality = count($defaultValue) ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
$explicitOptions[$fullName] = new InputOption(
$fullName,
$shortcut,
InputOption::VALUE_IS_ARRAY | $optionality,
$description,
count($defaultValue) ? $defaultValue : null
);
} else {
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
}
@@ -460,7 +631,7 @@ class CommandInfo
$result = new DefaultsWithDescriptions();
$params = $this->reflection->getParameters();
$optionsFromParameters = $this->determineOptionsFromParameters();
if (!empty($optionsFromParameters)) {
if ($this->lastParameterIsOptionsArray()) {
array_pop($params);
}
foreach ($params as $param) {
@@ -514,14 +685,26 @@ class CommandInfo
return $param->getDefaultValue();
}
protected function lastParameterName()
/**
* Determine if the last argument contains $options.
*
* Two forms indicate options:
* - $options = []
* - $options = ['flag' => 'default-value']
*
* Any other form, including `array $foo`, is not options.
*/
protected function lastParameterIsOptionsArray()
{
$params = $this->reflection->getParameters();
$param = end($params);
if (!$param) {
return '';
if (empty($params)) {
return [];
}
return $param->name;
$param = end($params);
if (!$param->isDefaultValueAvailable()) {
return [];
}
return is_array($param->getDefaultValue());
}
/**
@@ -529,7 +712,7 @@ class CommandInfo
* is not associative if its keys are numeric, and numbered sequentially
* from zero. All other arrays are considered to be associative.
*
* @param arrau $arr The array
* @param array $arr The array
* @return boolean
*/
protected function isAssoc($arr)

View File

@@ -0,0 +1,87 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser;
use Symfony\Component\Console\Input\InputOption;
use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParser;
use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParserFactory;
use Consolidation\AnnotatedCommand\AnnotationData;
/**
* Deserialize a CommandInfo object
*/
class CommandInfoDeserializer
{
// TODO: in a future version, move CommandInfo::deserialize here
public function deserialize($data)
{
return CommandInfo::deserialize((array)$data);
}
protected static function cachedMethodExists($cache)
{
return method_exists($cache['class'], $cache['method_name']);
}
public static function isValidSerializedData($cache)
{
return
isset($cache['schema']) &&
isset($cache['method_name']) &&
isset($cache['mtime']) &&
($cache['schema'] > 0) &&
($cache['schema'] <= CommandInfo::SERIALIZATION_SCHEMA_VERSION) &&
self::cachedMethodExists($cache);
}
public function constructFromCache(CommandInfo $commandInfo, $info_array)
{
$info_array += $this->defaultSerializationData();
$commandInfo
->setName($info_array['name'])
->replaceRawAnnotations($info_array['annotations'])
->setAliases($info_array['aliases'])
->setHelp($info_array['help'])
->setDescription($info_array['description'])
->replaceExampleUsages($info_array['example_usages'])
->setReturnType($info_array['return_type'])
;
$this->constructDefaultsWithDescriptions($commandInfo->arguments(), (array)$info_array['arguments']);
$this->constructDefaultsWithDescriptions($commandInfo->options(), (array)$info_array['options']);
}
protected function constructDefaultsWithDescriptions(DefaultsWithDescriptions $defaults, $data)
{
foreach ($data as $key => $info) {
$info = (array)$info;
$defaults->add($key, $info['description']);
if (array_key_exists('default', $info)) {
$defaults->setDefaultValue($key, $info['default']);
}
}
}
/**
* Default data. Everything should be provided during serialization;
* this is just as a fallback for unusual circumstances.
* @return array
*/
protected function defaultSerializationData()
{
return [
'name' => '',
'description' => '',
'help' => '',
'aliases' => [],
'annotations' => [],
'example_usages' => [],
'return_type' => [],
'parameters' => [],
'arguments' => [],
'options' => [],
'mtime' => 0,
];
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser;
use Symfony\Component\Console\Input\InputOption;
use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParser;
use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParserFactory;
use Consolidation\AnnotatedCommand\AnnotationData;
/**
* Serialize a CommandInfo object
*/
class CommandInfoSerializer
{
public function serialize(CommandInfo $commandInfo)
{
$allAnnotations = $commandInfo->getAnnotations();
$path = $allAnnotations['_path'];
$className = $allAnnotations['_classname'];
// Include the minimum information for command info (including placeholder records)
$info = [
'schema' => CommandInfo::SERIALIZATION_SCHEMA_VERSION,
'class' => $className,
'method_name' => $commandInfo->getMethodName(),
'mtime' => filemtime($path),
];
// If this is a valid method / hook, then add more information.
if ($commandInfo->valid()) {
$info += [
'name' => $commandInfo->getName(),
'description' => $commandInfo->getDescription(),
'help' => $commandInfo->getHelp(),
'aliases' => $commandInfo->getAliases(),
'annotations' => $commandInfo->getRawAnnotations()->getArrayCopy(),
'example_usages' => $commandInfo->getExampleUsages(),
'return_type' => $commandInfo->getReturnType(),
];
$info['arguments'] = $this->serializeDefaultsWithDescriptions($commandInfo->arguments());
$info['options'] = $this->serializeDefaultsWithDescriptions($commandInfo->options());
}
return $info;
}
protected function serializeDefaultsWithDescriptions(DefaultsWithDescriptions $defaults)
{
$result = [];
foreach ($defaults->getValues() as $key => $val) {
$result[$key] = [
'description' => $defaults->getDescription($key),
];
if ($defaults->hasDefault($key)) {
$result[$key]['default'] = $val;
}
}
return $result;
}
}

View File

@@ -32,7 +32,9 @@ class DefaultsWithDescriptions
public function __construct($values = [], $defaultDefault = null)
{
$this->values = $values;
$this->hasDefault = [];
$this->hasDefault = array_filter($this->values, function ($value) {
return isset($value);
});
$this->descriptions = [];
$this->defaultDefault = $defaultDefault;
}

View File

@@ -1,252 +0,0 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser\Internal;
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
use Consolidation\AnnotatedCommand\Parser\DefaultsWithDescriptions;
/**
* Given a class and method name, parse the annotations in the
* DocBlock comment, and provide accessor methods for all of
* the elements that are needed to create an annotated Command.
*/
abstract class AbstractCommandDocBlockParser
{
/**
* @var CommandInfo
*/
protected $commandInfo;
/**
* @var \ReflectionMethod
*/
protected $reflection;
/**
* @var array
*/
protected $tagProcessors = [
'command' => 'processCommandTag',
'name' => 'processCommandTag',
'arg' => 'processArgumentTag',
'param' => 'processParamTag',
'return' => 'processReturnTag',
'option' => 'processOptionTag',
'default' => 'processDefaultTag',
'aliases' => 'processAliases',
'usage' => 'processUsageTag',
'description' => 'processAlternateDescriptionTag',
'desc' => 'processAlternateDescriptionTag',
];
public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection)
{
$this->commandInfo = $commandInfo;
$this->reflection = $reflection;
}
protected function processAllTags($phpdoc)
{
// Iterate over all of the tags, and process them as necessary.
foreach ($phpdoc->getTags() as $tag) {
$processFn = [$this, 'processGenericTag'];
if (array_key_exists($tag->getName(), $this->tagProcessors)) {
$processFn = [$this, $this->tagProcessors[$tag->getName()]];
}
$processFn($tag);
}
}
abstract protected function getTagContents($tag);
/**
* Parse the docBlock comment for this command, and set the
* fields of this class with the data thereby obtained.
*/
abstract public function parse();
/**
* Save any tag that we do not explicitly recognize in the
* 'otherAnnotations' map.
*/
protected function processGenericTag($tag)
{
$this->commandInfo->addAnnotation($tag->getName(), $this->getTagContents($tag));
}
/**
* Set the name of the command from a @command or @name annotation.
*/
protected function processCommandTag($tag)
{
$commandName = $this->getTagContents($tag);
$this->commandInfo->setName($commandName);
// We also store the name in the 'other annotations' so that is is
// possible to determine if the method had a @command annotation.
$this->commandInfo->addAnnotation($tag->getName(), $commandName);
}
/**
* The @description and @desc annotations may be used in
* place of the synopsis (which we call 'description').
* This is discouraged.
*
* @deprecated
*/
protected function processAlternateDescriptionTag($tag)
{
$this->commandInfo->setDescription($this->getTagContents($tag));
}
/**
* Store the data from a @arg annotation in our argument descriptions.
*/
protected function processArgumentTag($tag)
{
if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
return;
}
$this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match);
}
/**
* Store the data from an @option annotation in our option descriptions.
*/
protected function processOptionTag($tag)
{
if (!$this->pregMatchOptionNameAndDescription((string)$tag->getDescription(), $match)) {
return;
}
$this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $match);
}
protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $nameAndDescription)
{
$variableName = $this->commandInfo->findMatchingOption($nameAndDescription['name']);
$desc = $nameAndDescription['description'];
$description = static::removeLineBreaks($desc);
$set->add($variableName, $description);
}
/**
* Store the data from a @default annotation in our argument or option store,
* as appropriate.
*/
protected function processDefaultTag($tag)
{
if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
return;
}
$variableName = $match['name'];
$defaultValue = $this->interpretDefaultValue($match['description']);
if ($this->commandInfo->arguments()->exists($variableName)) {
$this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
return;
}
$variableName = $this->commandInfo->findMatchingOption($variableName);
if ($this->commandInfo->options()->exists($variableName)) {
$this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
}
}
/**
* Store the data from a @usage annotation in our example usage list.
*/
protected function processUsageTag($tag)
{
$lines = explode("\n", $this->getTagContents($tag));
$usage = array_shift($lines);
$description = static::removeLineBreaks(implode("\n", $lines));
$this->commandInfo->setExampleUsage($usage, $description);
}
/**
* Process the comma-separated list of aliases
*/
protected function processAliases($tag)
{
$this->commandInfo->setAliases((string)$tag->getDescription());
}
/**
* Store the data from a @param annotation in our argument descriptions.
*/
protected function processParamTag($tag)
{
$variableName = $tag->getVariableName();
$variableName = str_replace('$', '', $variableName);
$description = static::removeLineBreaks((string)$tag->getDescription());
if ($variableName == $this->commandInfo->optionParamName()) {
return;
}
$this->commandInfo->arguments()->add($variableName, $description);
}
/**
* Store the data from a @return annotation in our argument descriptions.
*/
abstract protected function processReturnTag($tag);
protected function interpretDefaultValue($defaultValue)
{
$defaults = [
'null' => null,
'true' => true,
'false' => false,
"''" => '',
'[]' => [],
];
foreach ($defaults as $defaultName => $defaultTypedValue) {
if ($defaultValue == $defaultName) {
return $defaultTypedValue;
}
}
return $defaultValue;
}
/**
* Given a docblock description in the form "$variable description",
* return the variable name and description via the 'match' parameter.
*/
protected function pregMatchNameAndDescription($source, &$match)
{
$nameRegEx = '\\$(?P<name>[^ \t]+)[ \t]+';
$descriptionRegEx = '(?P<description>.*)';
$optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
return preg_match($optionRegEx, $source, $match);
}
/**
* Given a docblock description in the form "$variable description",
* return the variable name and description via the 'match' parameter.
*/
protected function pregMatchOptionNameAndDescription($source, &$match)
{
// Strip type and $ from the text before the @option name, if present.
$source = preg_replace('/^[a-zA-Z]* ?\\$/', '', $source);
$nameRegEx = '(?P<name>[^ \t]+)[ \t]+';
$descriptionRegEx = '(?P<description>.*)';
$optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
return preg_match($optionRegEx, $source, $match);
}
/**
* Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
* convert the data into the last of these forms.
*/
protected static function convertListToCommaSeparated($text)
{
return preg_replace('#[ \t\n\r,]+#', ',', $text);
}
/**
* Take a multiline description and convert it into a single
* long unbroken line.
*/
protected static function removeLineBreaks($text)
{
return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));
}
}

View File

@@ -0,0 +1,322 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser\Internal;
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
use Consolidation\AnnotatedCommand\Parser\DefaultsWithDescriptions;
/**
* Given a class and method name, parse the annotations in the
* DocBlock comment, and provide accessor methods for all of
* the elements that are needed to create an annotated Command.
*/
class BespokeDocBlockParser
{
protected $fqcnCache;
/**
* @var array
*/
protected $tagProcessors = [
'command' => 'processCommandTag',
'name' => 'processCommandTag',
'arg' => 'processArgumentTag',
'param' => 'processArgumentTag',
'return' => 'processReturnTag',
'option' => 'processOptionTag',
'default' => 'processDefaultTag',
'aliases' => 'processAliases',
'usage' => 'processUsageTag',
'description' => 'processAlternateDescriptionTag',
'desc' => 'processAlternateDescriptionTag',
];
public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection, $fqcnCache = null)
{
$this->commandInfo = $commandInfo;
$this->reflection = $reflection;
$this->fqcnCache = $fqcnCache ?: new FullyQualifiedClassCache();
}
/**
* Parse the docBlock comment for this command, and set the
* fields of this class with the data thereby obtained.
*/
public function parse()
{
$doc = $this->reflection->getDocComment();
$this->parseDocBlock($doc);
}
/**
* Save any tag that we do not explicitly recognize in the
* 'otherAnnotations' map.
*/
protected function processGenericTag($tag)
{
$this->commandInfo->addAnnotation($tag->getTag(), $tag->getContent());
}
/**
* Set the name of the command from a @command or @name annotation.
*/
protected function processCommandTag($tag)
{
if (!$tag->hasWordAndDescription($matches)) {
throw new \Exception('Could not determine command name from tag ' . (string)$tag);
}
$commandName = $matches['word'];
$this->commandInfo->setName($commandName);
// We also store the name in the 'other annotations' so that is is
// possible to determine if the method had a @command annotation.
$this->commandInfo->addAnnotation($tag->getTag(), $commandName);
}
/**
* The @description and @desc annotations may be used in
* place of the synopsis (which we call 'description').
* This is discouraged.
*
* @deprecated
*/
protected function processAlternateDescriptionTag($tag)
{
$this->commandInfo->setDescription($tag->getContent());
}
/**
* Store the data from a @arg annotation in our argument descriptions.
*/
protected function processArgumentTag($tag)
{
if (!$tag->hasVariable($matches)) {
throw new \Exception('Could not determine argument name from tag ' . (string)$tag);
}
if ($matches['variable'] == $this->optionParamName()) {
return;
}
$this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $matches['variable'], $matches['description']);
}
/**
* Store the data from an @option annotation in our option descriptions.
*/
protected function processOptionTag($tag)
{
if (!$tag->hasVariable($matches)) {
throw new \Exception('Could not determine option name from tag ' . (string)$tag);
}
$this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $matches['variable'], $matches['description']);
}
protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $name, $description)
{
$variableName = $this->commandInfo->findMatchingOption($name);
$description = static::removeLineBreaks($description);
$set->add($variableName, $description);
}
/**
* Store the data from a @default annotation in our argument or option store,
* as appropriate.
*/
protected function processDefaultTag($tag)
{
if (!$tag->hasVariable($matches)) {
throw new \Exception('Could not determine parameter name for default value from tag ' . (string)$tag);
}
$variableName = $matches['variable'];
$defaultValue = $this->interpretDefaultValue($matches['description']);
if ($this->commandInfo->arguments()->exists($variableName)) {
$this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
return;
}
$variableName = $this->commandInfo->findMatchingOption($variableName);
if ($this->commandInfo->options()->exists($variableName)) {
$this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
}
}
/**
* Store the data from a @usage annotation in our example usage list.
*/
protected function processUsageTag($tag)
{
$lines = explode("\n", $tag->getContent());
$usage = trim(array_shift($lines));
$description = static::removeLineBreaks(implode("\n", array_map(function ($line) {
return trim($line);
}, $lines)));
$this->commandInfo->setExampleUsage($usage, $description);
}
/**
* Process the comma-separated list of aliases
*/
protected function processAliases($tag)
{
$this->commandInfo->setAliases((string)$tag->getContent());
}
/**
* Store the data from a @return annotation in our argument descriptions.
*/
protected function processReturnTag($tag)
{
// The return type might be a variable -- '$this'. It will
// usually be a type, like RowsOfFields, or \Namespace\RowsOfFields.
if (!$tag->hasVariableAndDescription($matches)) {
throw new \Exception('Could not determine return type from tag ' . (string)$tag);
}
// Look at namespace and `use` statments to make returnType a fqdn
$returnType = $matches['variable'];
$returnType = $this->findFullyQualifiedClass($returnType);
$this->commandInfo->setReturnType($returnType);
}
protected function findFullyQualifiedClass($className)
{
if (strpos($className, '\\') !== false) {
return $className;
}
return $this->fqcnCache->qualify($this->reflection->getFileName(), $className);
}
private function parseDocBlock($doc)
{
// Remove the leading /** and the trailing */
$doc = preg_replace('#^\s*/\*+\s*#', '', $doc);
$doc = preg_replace('#\s*\*+/\s*#', '', $doc);
// Nothing left? Exit.
if (empty($doc)) {
return;
}
$tagFactory = new TagFactory();
$lines = [];
foreach (explode("\n", $doc) as $row) {
// Remove trailing whitespace and leading space + '*'s
$row = rtrim($row);
$row = preg_replace('#^[ \t]*\**#', '', $row);
if (!$tagFactory->parseLine($row)) {
$lines[] = $row;
}
}
$this->processDescriptionAndHelp($lines);
$this->processAllTags($tagFactory->getTags());
}
protected function processDescriptionAndHelp($lines)
{
// Trim all of the lines individually.
$lines =
array_map(
function ($line) {
return trim($line);
},
$lines
);
// Everything up to the first blank line goes in the description.
$description = array_shift($lines);
while ($this->nextLineIsNotEmpty($lines)) {
$description .= ' ' . array_shift($lines);
}
// Everything else goes in the help.
$help = trim(implode("\n", $lines));
$this->commandInfo->setDescription($description);
$this->commandInfo->setHelp($help);
}
protected function nextLineIsNotEmpty($lines)
{
if (empty($lines)) {
return false;
}
$nextLine = trim($lines[0]);
return !empty($nextLine);
}
protected function processAllTags($tags)
{
// Iterate over all of the tags, and process them as necessary.
foreach ($tags as $tag) {
$processFn = [$this, 'processGenericTag'];
if (array_key_exists($tag->getTag(), $this->tagProcessors)) {
$processFn = [$this, $this->tagProcessors[$tag->getTag()]];
}
$processFn($tag);
}
}
protected function lastParameterName()
{
$params = $this->commandInfo->getParameters();
$param = end($params);
if (!$param) {
return '';
}
return $param->name;
}
/**
* Return the name of the last parameter if it holds the options.
*/
public function optionParamName()
{
// Remember the name of the last parameter, if it holds the options.
// We will use this information to ignore @param annotations for the options.
if (!isset($this->optionParamName)) {
$this->optionParamName = '';
$options = $this->commandInfo->options();
if (!$options->isEmpty()) {
$this->optionParamName = $this->lastParameterName();
}
}
return $this->optionParamName;
}
protected function interpretDefaultValue($defaultValue)
{
$defaults = [
'null' => null,
'true' => true,
'false' => false,
"''" => '',
'[]' => [],
];
foreach ($defaults as $defaultName => $defaultTypedValue) {
if ($defaultValue == $defaultName) {
return $defaultTypedValue;
}
}
return $defaultValue;
}
/**
* Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
* convert the data into the last of these forms.
*/
protected static function convertListToCommaSeparated($text)
{
return preg_replace('#[ \t\n\r,]+#', ',', $text);
}
/**
* Take a multiline description and convert it into a single
* long unbroken line.
*/
protected static function removeLineBreaks($text)
{
return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser\Internal;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tag\ParamTag;
use phpDocumentor\Reflection\DocBlock\Tag\ReturnTag;
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
use Consolidation\AnnotatedCommand\Parser\DefaultsWithDescriptions;
/**
* Given a class and method name, parse the annotations in the
* DocBlock comment, and provide accessor methods for all of
* the elements that are needed to create an annotated Command.
*/
class CommandDocBlockParser2 extends AbstractCommandDocBlockParser
{
/**
* Parse the docBlock comment for this command, and set the
* fields of this class with the data thereby obtained.
*/
public function parse()
{
$docblockComment = $this->reflection->getDocComment();
$phpdoc = new DocBlock($docblockComment);
// First set the description (synopsis) and help.
$this->commandInfo->setDescription((string)$phpdoc->getShortDescription());
$this->commandInfo->setHelp((string)$phpdoc->getLongDescription());
$this->processAllTags($phpdoc);
}
protected function getTagContents($tag)
{
return $tag->getContent();
}
/**
* Store the data from a @arg annotation in our argument descriptions.
*/
protected function processArgumentTag($tag)
{
if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
return;
}
$this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match);
}
/**
* Store the data from a @param annotation in our argument descriptions.
*/
protected function processParamTag($tag)
{
if (!$tag instanceof ParamTag) {
return;
}
return parent::processParamTag($tag);
}
/**
* Store the data from a @return annotation in our argument descriptions.
*/
protected function processReturnTag($tag)
{
if (!$tag instanceof ReturnTag) {
return;
}
$this->commandInfo->setReturnType($tag->getType());
}
}

View File

@@ -1,74 +0,0 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser\Internal;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
use Consolidation\AnnotatedCommand\Parser\DefaultsWithDescriptions;
/**
* Given a class and method name, parse the annotations in the
* DocBlock comment, and provide accessor methods for all of
* the elements that are needed to create an annotated Command.
*/
class CommandDocBlockParser3 extends AbstractCommandDocBlockParser
{
/**
* Parse the docBlock comment for this command, and set the
* fields of this class with the data thereby obtained.
*/
public function parse()
{
// DocBlockFactory::create fails if the comment is empty.
$docComment = $this->reflection->getDocComment();
if (empty($docComment)) {
return;
}
$phpdoc = $this->createDocBlock();
// First set the description (synopsis) and help.
$this->commandInfo->setDescription((string)$phpdoc->getSummary());
$this->commandInfo->setHelp((string)$phpdoc->getDescription());
$this->processAllTags($phpdoc);
}
public function createDocBlock()
{
$docBlockFactory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
$contextFactory = new \phpDocumentor\Reflection\Types\ContextFactory();
return $docBlockFactory->create(
$this->reflection,
$contextFactory->createFromReflector($this->reflection)
);
}
protected function getTagContents($tag)
{
return (string)$tag;
}
/**
* Store the data from a @param annotation in our argument descriptions.
*/
protected function processParamTag($tag)
{
if (!$tag instanceof Param) {
return;
}
return parent::processParamTag($tag);
}
/**
* Store the data from a @return annotation in our argument descriptions.
*/
protected function processReturnTag($tag)
{
if (!$tag instanceof Return_) {
return;
}
// If there is a spurrious trailing space on the return type, remove it.
$this->commandInfo->setReturnType(trim($this->getTagContents($tag)));
}
}

View File

@@ -15,14 +15,6 @@ class CommandDocBlockParserFactory
private static function create(CommandInfo $commandInfo, \ReflectionMethod $reflection)
{
if (static::hasReflectionDocBlock3()) {
return new CommandDocBlockParser3($commandInfo, $reflection);
}
return new CommandDocBlockParser2($commandInfo, $reflection);
}
private static function hasReflectionDocBlock3()
{
return class_exists('phpDocumentor\Reflection\DocBlockFactory') && class_exists('phpDocumentor\Reflection\Types\ContextFactory');
return new BespokeDocBlockParser($commandInfo, $reflection);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser\Internal;
/**
* Methods to convert to / from a csv string.
*/
class CsvUtils
{
/**
* Ensure that the provided data is a string.
*
* @param string|array $data The data to convert to a string.
* @return string
*/
public static function toString($data)
{
if (is_array($data)) {
return static::csvEscape($data);
}
return $data;
}
/**
* Convert a string to a csv.
*/
public static function csvEscape(array $data, $delimiter = ',')
{
$buffer = fopen('php://temp', 'r+');
fputcsv($buffer, $data, $delimiter);
rewind($buffer);
$csv = fgets($buffer);
fclose($buffer);
return rtrim($csv);
}
/**
* Return a specific named annotation for this command.
*
* @param string|array $data The data to convert to an array.
* @return array
*/
public static function toList($data)
{
if (!is_array($data)) {
return str_getcsv($data);
}
return $data;
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser\Internal;
/**
* Hold the tag definition for one tag in a DocBlock.
*
* The tag can be sliced into the following forms:
* - "@tag content"
* - "@tag word description"
* - "@tag $variable description"
* - "@tag word $variable description"
*/
class DocblockTag
{
/** @var string Name of the tag */
protected $tag;
/** @var string|null Contents of the tag. */
protected $content;
const TAG_REGEX = '@(?P<tag>[^\s$]+)[\s]*';
const VARIABLE_REGEX = '\\$(?P<variable>[^\s$]+)[\s]*';
const VARIABLE_OR_WORD_REGEX = '\\$?(?P<variable>[^\s$]+)[\s]*';
const TYPE_REGEX = '(?P<type>[^\s$]+)[\s]*';
const WORD_REGEX = '(?P<word>[^\s$]+)[\s]*';
const DESCRIPTION_REGEX = '(?P<description>.*)';
const IS_TAG_REGEX = '/^[*\s]*@/';
/**
* Check if the provided string begins with a tag
* @param string $subject
* @return bool
*/
public static function isTag($subject)
{
return preg_match(self::IS_TAG_REGEX, $subject);
}
/**
* Use a regular expression to separate the tag from the content.
*
* @param string $subject
* @param string[] &$matches Sets $matches['tag'] and $matches['description']
* @return bool
*/
public static function splitTagAndContent($subject, &$matches)
{
$regex = '/' . self::TAG_REGEX . self::DESCRIPTION_REGEX . '/s';
return preg_match($regex, $subject, $matches);
}
/**
* DockblockTag constructor
*/
public function __construct($tag, $content = null)
{
$this->tag = $tag;
$this->content = $content;
}
/**
* Add more content onto a tag during parsing.
*/
public function appendContent($line)
{
$this->content .= "\n$line";
}
/**
* Return the tag - e.g. "@foo description" returns 'foo'
*
* @return string
*/
public function getTag()
{
return $this->tag;
}
/**
* Return the content portion of the tag - e.g. "@foo bar baz boz" returns
* "bar baz boz"
*
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Convert tag back into a string.
*/
public function __toString()
{
return '@' . $this->getTag() . ' ' . $this->getContent();
}
/**
* Determine if tag is one of:
* - "@tag variable description"
* - "@tag $variable description"
* - "@tag type $variable description"
*
* @param string $subject
* @param string[] &$matches Sets $matches['variable'] and
* $matches['description']; might set $matches['type'].
* @return bool
*/
public function hasVariable(&$matches)
{
return
$this->hasTypeVariableAndDescription($matches) ||
$this->hasVariableAndDescription($matches);
}
/**
* Determine if tag is "@tag $variable description"
* @param string $subject
* @param string[] &$matches Sets $matches['variable'] and
* $matches['description']
* @return bool
*/
public function hasVariableAndDescription(&$matches)
{
$regex = '/^\s*' . self::VARIABLE_OR_WORD_REGEX . self::DESCRIPTION_REGEX . '/s';
return preg_match($regex, $this->getContent(), $matches);
}
/**
* Determine if tag is "@tag type $variable description"
*
* @param string $subject
* @param string[] &$matches Sets $matches['variable'],
* $matches['description'] and $matches['type'].
* @return bool
*/
public function hasTypeVariableAndDescription(&$matches)
{
$regex = '/^\s*' . self::TYPE_REGEX . self::VARIABLE_REGEX . self::DESCRIPTION_REGEX . '/s';
return preg_match($regex, $this->getContent(), $matches);
}
/**
* Determine if tag is "@tag word description"
* @param string $subject
* @param string[] &$matches Sets $matches['word'] and
* $matches['description']
* @return bool
*/
public function hasWordAndDescription(&$matches)
{
$regex = '/^\s*' . self::WORD_REGEX . self::DESCRIPTION_REGEX . '/s';
return preg_match($regex, $this->getContent(), $matches);
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser\Internal;
class FullyQualifiedClassCache
{
protected $classCache = [];
protected $namespaceCache = [];
public function qualify($filename, $className)
{
$this->primeCache($filename, $className);
return $this->cached($filename, $className);
}
protected function cached($filename, $className)
{
return isset($this->classCache[$filename][$className]) ? $this->classCache[$filename][$className] : $className;
}
protected function primeCache($filename, $className)
{
// If the cache has already been primed, do no further work
if (isset($this->namespaceCache[$filename])) {
return false;
}
$handle = fopen($filename, "r");
if (!$handle) {
return false;
}
$namespaceName = $this->primeNamespaceCache($filename, $handle);
$this->primeUseCache($filename, $handle);
// If there is no 'use' statement for the className, then
// generate an effective classname from the namespace
if (!isset($this->classCache[$filename][$className])) {
$this->classCache[$filename][$className] = $namespaceName . '\\' . $className;
}
fclose($handle);
}
protected function primeNamespaceCache($filename, $handle)
{
$namespaceName = $this->readNamespace($handle);
if (!$namespaceName) {
return false;
}
$this->namespaceCache[$filename] = $namespaceName;
return $namespaceName;
}
protected function primeUseCache($filename, $handle)
{
$usedClasses = $this->readUseStatements($handle);
if (empty($usedClasses)) {
return false;
}
$this->classCache[$filename] = $usedClasses;
}
protected function readNamespace($handle)
{
$namespaceRegex = '#^\s*namespace\s+#';
$line = $this->readNextRelevantLine($handle);
if (!$line || !preg_match($namespaceRegex, $line)) {
return false;
}
$namespaceName = preg_replace($namespaceRegex, '', $line);
$namespaceName = rtrim($namespaceName, ';');
return $namespaceName;
}
protected function readUseStatements($handle)
{
$useRegex = '#^\s*use\s+#';
$result = [];
while (true) {
$line = $this->readNextRelevantLine($handle);
if (!$line || !preg_match($useRegex, $line)) {
return $result;
}
$usedClass = preg_replace($useRegex, '', $line);
$usedClass = rtrim($usedClass, ';');
$unqualifiedClass = preg_replace('#.*\\\\#', '', $usedClass);
// If this is an aliased class, 'use \Foo\Bar as Baz', then adjust
if (strpos($usedClass, ' as ')) {
$unqualifiedClass = preg_replace('#.*\sas\s+#', '', $usedClass);
$usedClass = preg_replace('#\s+as\s+#', '', $usedClass);
}
$result[$unqualifiedClass] = $usedClass;
}
}
protected function readNextRelevantLine($handle)
{
while (($line = fgets($handle)) !== false) {
if (preg_match('#^\s*\w#', $line)) {
return trim($line);
}
}
return false;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Consolidation\AnnotatedCommand\Parser\Internal;
/**
* Hold some state. Collect tags.
*/
class TagFactory
{
/** @var DocblockTag|null Current tag */
protected $current;
/** @var DocblockTag[] All tag */
protected $tags;
/**
* DocblockTag constructor
*/
public function __construct()
{
$this->current = null;
$this->tags = [];
}
public function parseLine($line)
{
if (DocblockTag::isTag($line)) {
return $this->createTag($line);
}
if (empty($line)) {
return $this->storeCurrentTag();
}
return $this->accumulateContent($line);
}
public function getTags()
{
$this->storeCurrentTag();
return $this->tags;
}
protected function createTag($line)
{
DocblockTag::splitTagAndContent($line, $matches);
$this->storeCurrentTag();
$this->current = new DocblockTag($matches['tag'], $matches['description']);
return true;
}
protected function storeCurrentTag()
{
if (!$this->current) {
return false;
}
$this->tags[] = $this->current;
$this->current = false;
return true;
}
protected function accumulateContent($line)
{
if (!$this->current) {
return false;
}
$this->current->appendContent($line);
return true;
}
}