Refactoring
This commit is contained in:
581
lib/composer/vendor/consolidation/annotated-command/src/Parser/CommandInfo.php
vendored
Normal file
581
lib/composer/vendor/consolidation/annotated-command/src/Parser/CommandInfo.php
vendored
Normal file
@@ -0,0 +1,581 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 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 a Symfony Console Command.
|
||||
*
|
||||
* Note that the name of this class is now somewhat of a misnomer,
|
||||
* as we now use it to hold annotation data for hooks as well as commands.
|
||||
* It would probably be better to rename this to MethodInfo at some point.
|
||||
*/
|
||||
class CommandInfo
|
||||
{
|
||||
/**
|
||||
* @var \ReflectionMethod
|
||||
*/
|
||||
protected $reflection;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
* @var string
|
||||
*/
|
||||
protected $docBlockIsParsed;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $help = '';
|
||||
|
||||
/**
|
||||
* @var DefaultsWithDescriptions
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* @var DefaultsWithDescriptions
|
||||
*/
|
||||
protected $arguments;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $exampleUsage = [];
|
||||
|
||||
/**
|
||||
* @var AnnotationData
|
||||
*/
|
||||
protected $otherAnnotations;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $aliases = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $methodName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
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.
|
||||
* @param string $methodName The name of the method to get info about.
|
||||
*/
|
||||
public function __construct($classNameOrInstance, $methodName)
|
||||
{
|
||||
$this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
|
||||
$this->methodName = $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->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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the method name provided to the constructor.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMethodName()
|
||||
{
|
||||
return $this->methodName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the primary name for this command.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
$this->parseDocBlock();
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary name for this command.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReturnType()
|
||||
{
|
||||
$this->parseDocBlock();
|
||||
return $this->returnType;
|
||||
}
|
||||
|
||||
public function setReturnType($returnType)
|
||||
{
|
||||
$this->returnType = $returnType;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any annotations included in the docblock comment for the
|
||||
* implementation method of this command that are not already
|
||||
* handled by the primary methods of this class.
|
||||
*
|
||||
* @return AnnotationData
|
||||
*/
|
||||
public function getRawAnnotations()
|
||||
{
|
||||
$this->parseDocBlock();
|
||||
return $this->otherAnnotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any annotations included in the docblock comment,
|
||||
* also including default values such as @command. We add
|
||||
* in the default @command annotation late, and only in a
|
||||
* copy of the annotation data because we use the existance
|
||||
* of a @command to indicate that this CommandInfo is
|
||||
* a command, and not a hook or anything else.
|
||||
*
|
||||
* @return AnnotationData
|
||||
*/
|
||||
public function getAnnotations()
|
||||
{
|
||||
return new AnnotationData(
|
||||
$this->getRawAnnotations()->getArrayCopy() +
|
||||
[
|
||||
'command' => $this->getName(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a specific named annotation for this command.
|
||||
*
|
||||
* @param string $annotation The name of the annotation.
|
||||
* @return string
|
||||
*/
|
||||
public function getAnnotation($annotation)
|
||||
{
|
||||
// hasAnnotation parses the docblock
|
||||
if (!$this->hasAnnotation($annotation)) {
|
||||
return null;
|
||||
}
|
||||
return $this->otherAnnotations[$annotation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the specified annotation exists for this command.
|
||||
*
|
||||
* @param string $annotation The name of the annotation.
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasAnnotation($annotation)
|
||||
{
|
||||
$this->parseDocBlock();
|
||||
return isset($this->otherAnnotations[$annotation]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save any tag that we do not explicitly recognize in the
|
||||
* 'otherAnnotations' map.
|
||||
*/
|
||||
public function addAnnotation($name, $content)
|
||||
{
|
||||
$this->otherAnnotations[$name] = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an annotation that was previoudly set.
|
||||
*/
|
||||
public function removeAnnotation($name)
|
||||
{
|
||||
unset($this->otherAnnotations[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the synopsis of the command (~first line).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
$this->parseDocBlock();
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the command description.
|
||||
*
|
||||
* @param string $description The description to set.
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the help text of the command (the description)
|
||||
*/
|
||||
public function getHelp()
|
||||
{
|
||||
$this->parseDocBlock();
|
||||
return $this->help;
|
||||
}
|
||||
/**
|
||||
* Set the help text for this command.
|
||||
*
|
||||
* @param string $help The help text.
|
||||
*/
|
||||
public function setHelp($help)
|
||||
{
|
||||
$this->help = $help;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of aliases for this command.
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAliases()
|
||||
{
|
||||
$this->parseDocBlock();
|
||||
return $this->aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set aliases that can be used in place of the command's primary name.
|
||||
*
|
||||
* @param string|string[] $aliases
|
||||
*/
|
||||
public function setAliases($aliases)
|
||||
{
|
||||
if (is_string($aliases)) {
|
||||
$aliases = explode(',', static::convertListToCommaSeparated($aliases));
|
||||
}
|
||||
$this->aliases = array_filter($aliases);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the examples for this command. This is @usage instead of
|
||||
* @example because the later is defined by the phpdoc standard to
|
||||
* be example method calls.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getExampleUsages()
|
||||
{
|
||||
$this->parseDocBlock();
|
||||
return $this->exampleUsage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an example usage for this command.
|
||||
*
|
||||
* @param string $usage An example of the command, including the command
|
||||
* name and all of its example arguments and options.
|
||||
* @param string $description An explanation of what the example does.
|
||||
*/
|
||||
public function setExampleUsage($usage, $description)
|
||||
{
|
||||
$this->exampleUsage[$usage] = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of refleaction parameters.
|
||||
*
|
||||
* @return ReflectionParameter[]
|
||||
*/
|
||||
public function getParameters()
|
||||
{
|
||||
return $this->reflection->getParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptions of commandline arguements for this command.
|
||||
*
|
||||
* @return DefaultsWithDescriptions
|
||||
*/
|
||||
public function arguments()
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptions of commandline options for this command.
|
||||
*
|
||||
* @return DefaultsWithDescriptions
|
||||
*/
|
||||
public function options()
|
||||
{
|
||||
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
|
||||
* $options = ['someoption' => 'defaultvalue'] in the command method
|
||||
* parameter list.
|
||||
*
|
||||
* @return InputOption[]
|
||||
*/
|
||||
public function inputOptions()
|
||||
{
|
||||
$explicitOptions = [];
|
||||
|
||||
$opts = $this->options()->getValues();
|
||||
foreach ($opts as $name => $defaultValue) {
|
||||
$description = $this->options()->getDescription($name);
|
||||
|
||||
$fullName = $name;
|
||||
$shortcut = '';
|
||||
if (strpos($name, '|')) {
|
||||
list($fullName, $shortcut) = explode('|', $name, 2);
|
||||
}
|
||||
|
||||
if (is_bool($defaultValue)) {
|
||||
$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);
|
||||
} else {
|
||||
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
return $explicitOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* An option might have a name such as 'silent|s'. In this
|
||||
* instance, we will allow the @option or @default tag to
|
||||
* reference the option only by name (e.g. 'silent' or 's'
|
||||
* instead of 'silent|s').
|
||||
*
|
||||
* @param string $optionName
|
||||
* @return string
|
||||
*/
|
||||
public function findMatchingOption($optionName)
|
||||
{
|
||||
// Exit fast if there's an exact match
|
||||
if ($this->options->exists($optionName)) {
|
||||
return $optionName;
|
||||
}
|
||||
$existingOptionName = $this->findExistingOption($optionName);
|
||||
if (isset($existingOptionName)) {
|
||||
return $existingOptionName;
|
||||
}
|
||||
return $this->findOptionAmongAlternatives($optionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $optionName
|
||||
* @return string
|
||||
*/
|
||||
protected function findOptionAmongAlternatives($optionName)
|
||||
{
|
||||
// Check the other direction: if the annotation contains @silent|s
|
||||
// and the options array has 'silent|s'.
|
||||
$checkMatching = explode('|', $optionName);
|
||||
if (count($checkMatching) > 1) {
|
||||
foreach ($checkMatching as $checkName) {
|
||||
if ($this->options->exists($checkName)) {
|
||||
$this->options->rename($checkName, $optionName);
|
||||
return $optionName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $optionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $optionName
|
||||
* @return string|null
|
||||
*/
|
||||
protected function findExistingOption($optionName)
|
||||
{
|
||||
// Check to see if we can find the option name in an existing option,
|
||||
// e.g. if the options array has 'silent|s' => false, and the annotation
|
||||
// is @silent.
|
||||
foreach ($this->options()->getValues() as $name => $default) {
|
||||
if (in_array($optionName, explode('|', $name))) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine the parameters of the method for this command, and
|
||||
* build a list of commandline arguements for them.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function determineAgumentClassifications()
|
||||
{
|
||||
$result = new DefaultsWithDescriptions();
|
||||
$params = $this->reflection->getParameters();
|
||||
$optionsFromParameters = $this->determineOptionsFromParameters();
|
||||
if (!empty($optionsFromParameters)) {
|
||||
array_pop($params);
|
||||
}
|
||||
foreach ($params as $param) {
|
||||
$this->addParameterToResult($result, $param);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine the provided parameter, and determine whether it
|
||||
* is a parameter that will be filled in with a positional
|
||||
* commandline argument.
|
||||
*/
|
||||
protected function addParameterToResult($result, $param)
|
||||
{
|
||||
// Commandline arguments must be strings, so ignore any
|
||||
// parameter that is typehinted to any non-primative class.
|
||||
if ($param->getClass() != null) {
|
||||
return;
|
||||
}
|
||||
$result->add($param->name);
|
||||
if ($param->isDefaultValueAvailable()) {
|
||||
$defaultValue = $param->getDefaultValue();
|
||||
if (!$this->isAssoc($defaultValue)) {
|
||||
$result->setDefaultValue($param->name, $defaultValue);
|
||||
}
|
||||
} elseif ($param->isArray()) {
|
||||
$result->setDefaultValue($param->name, []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine the parameters of the method for this command, and determine
|
||||
* the disposition of the options from them.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function determineOptionsFromParameters()
|
||||
{
|
||||
$params = $this->reflection->getParameters();
|
||||
if (empty($params)) {
|
||||
return [];
|
||||
}
|
||||
$param = end($params);
|
||||
if (!$param->isDefaultValueAvailable()) {
|
||||
return [];
|
||||
}
|
||||
if (!$this->isAssoc($param->getDefaultValue())) {
|
||||
return [];
|
||||
}
|
||||
return $param->getDefaultValue();
|
||||
}
|
||||
|
||||
protected function lastParameterName()
|
||||
{
|
||||
$params = $this->reflection->getParameters();
|
||||
$param = end($params);
|
||||
if (!$param) {
|
||||
return '';
|
||||
}
|
||||
return $param->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper; determine if an array is associative or not. An array
|
||||
* 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
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isAssoc($arr)
|
||||
{
|
||||
if (!is_array($arr)) {
|
||||
return false;
|
||||
}
|
||||
return array_keys($arr) !== range(0, count($arr) - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from a method name to the corresponding command name. A
|
||||
* method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
|
||||
* become 'foo:bar-baz-boz'.
|
||||
*
|
||||
* @param string $camel method name.
|
||||
* @return string
|
||||
*/
|
||||
protected function convertName($camel)
|
||||
{
|
||||
$splitter="-";
|
||||
$camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
|
||||
$camel = preg_replace("/$splitter/", ':', $camel, 1);
|
||||
return strtolower($camel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the docBlock comment for this command, and set the
|
||||
* fields of this class with the data thereby obtained.
|
||||
*/
|
||||
protected function parseDocBlock()
|
||||
{
|
||||
if (!$this->docBlockIsParsed) {
|
||||
// The parse function will insert data from the provided method
|
||||
// into this object, using our accessors.
|
||||
CommandDocBlockParserFactory::parse($this, $this->reflection);
|
||||
$this->docBlockIsParsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
160
lib/composer/vendor/consolidation/annotated-command/src/Parser/DefaultsWithDescriptions.php
vendored
Normal file
160
lib/composer/vendor/consolidation/annotated-command/src/Parser/DefaultsWithDescriptions.php
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
namespace Consolidation\AnnotatedCommand\Parser;
|
||||
|
||||
/**
|
||||
* An associative array that maps from key to default value;
|
||||
* each entry can also have a description.
|
||||
*/
|
||||
class DefaultsWithDescriptions
|
||||
{
|
||||
/**
|
||||
* @var array Associative array of key : default mappings
|
||||
*/
|
||||
protected $values;
|
||||
|
||||
/**
|
||||
* @var array Associative array used like a set to indicate default value
|
||||
* exists for the key.
|
||||
*/
|
||||
protected $hasDefault;
|
||||
|
||||
/**
|
||||
* @var array Associative array of key : description mappings
|
||||
*/
|
||||
protected $descriptions;
|
||||
|
||||
/**
|
||||
* @var mixed Default value that the default value of items in
|
||||
* the collection should take when not specified in the 'add' method.
|
||||
*/
|
||||
protected $defaultDefault;
|
||||
|
||||
public function __construct($values = [], $defaultDefault = null)
|
||||
{
|
||||
$this->values = $values;
|
||||
$this->hasDefault = [];
|
||||
$this->descriptions = [];
|
||||
$this->defaultDefault = $defaultDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return just the key : default values mapping
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getValues()
|
||||
{
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this set of options is empty
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return empty($this->values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see whether the speicifed key exists in the collection.
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists($key)
|
||||
{
|
||||
return array_key_exists($key, $this->values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of one entry.
|
||||
*
|
||||
* @param string $key The key of the item.
|
||||
* @return string
|
||||
*/
|
||||
public function get($key)
|
||||
{
|
||||
if (array_key_exists($key, $this->values)) {
|
||||
return $this->values[$key];
|
||||
}
|
||||
return $this->defaultDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description of one entry.
|
||||
*
|
||||
* @param string $key The key of the item.
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription($key)
|
||||
{
|
||||
if (array_key_exists($key, $this->descriptions)) {
|
||||
return $this->descriptions[$key];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add another argument to this command.
|
||||
*
|
||||
* @param string $key Name of the argument.
|
||||
* @param string $description Help text for the argument.
|
||||
* @param mixed $defaultValue The default value for the argument.
|
||||
*/
|
||||
public function add($key, $description = '', $defaultValue = null)
|
||||
{
|
||||
if (!$this->exists($key) || isset($defaultValue)) {
|
||||
$this->values[$key] = isset($defaultValue) ? $defaultValue : $this->defaultDefault;
|
||||
}
|
||||
unset($this->descriptions[$key]);
|
||||
if (!empty($description)) {
|
||||
$this->descriptions[$key] = $description;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the default value of an entry.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $defaultValue
|
||||
*/
|
||||
public function setDefaultValue($key, $defaultValue)
|
||||
{
|
||||
$this->values[$key] = $defaultValue;
|
||||
$this->hasDefault[$key] = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the named argument definitively has a default value.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDefault($key)
|
||||
{
|
||||
return array_key_exists($key, $this->hasDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entry
|
||||
*
|
||||
* @param string $key The entry to remove
|
||||
*/
|
||||
public function clear($key)
|
||||
{
|
||||
unset($this->values[$key]);
|
||||
unset($this->descriptions[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename an existing option to something else.
|
||||
*/
|
||||
public function rename($oldName, $newName)
|
||||
{
|
||||
$this->add($newName, $this->getDescription($oldName), $this->get($oldName));
|
||||
$this->clear($oldName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Consolidation\AnnotatedCommand\Parser\Internal;
|
||||
|
||||
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
|
||||
|
||||
/**
|
||||
* Create an appropriate CommandDocBlockParser.
|
||||
*/
|
||||
class CommandDocBlockParserFactory
|
||||
{
|
||||
public static function parse(CommandInfo $commandInfo, \ReflectionMethod $reflection)
|
||||
{
|
||||
return static::create($commandInfo, $reflection)->parse();
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user