Version 1
[yaffs-website] / vendor / consolidation / annotated-command / src / Parser / CommandInfo.php
diff --git a/vendor/consolidation/annotated-command/src/Parser/CommandInfo.php b/vendor/consolidation/annotated-command/src/Parser/CommandInfo.php
new file mode 100644 (file)
index 0000000..1780573
--- /dev/null
@@ -0,0 +1,697 @@
+<?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
+{
+    /**
+     * Serialization schema version. Incremented every time the serialization schema changes.
+     */
+    const SERIALIZATION_SCHEMA_VERSION = 3;
+
+    /**
+     * @var \ReflectionMethod
+     */
+    protected $reflection;
+
+    /**
+     * @var boolean
+     * @var string
+    */
+    protected $docBlockIsParsed = false;
+
+    /**
+     * @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 InputOption[]
+     */
+    protected $inputOptions;
+
+    /**
+     * @var string
+     */
+    protected $methodName;
+
+    /**
+     * @var string
+     */
+    protected $returnType;
+
+    /**
+     * 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, 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, $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($methodName);
+        $this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false);
+        $this->arguments = $this->determineAgumentClassifications();
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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();
+        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;
+    }
+
+    /**
+     * 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
+     * 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()
+    {
+        // 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 as a list.
+     *
+     * @param string $name The name of the annotation.
+     * @return array|null
+     */
+    public function getAnnotationList($name)
+    {
+        // hasAnnotation parses the docblock
+        if (!$this->hasAnnotation($name)) {
+            return null;
+        }
+        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);
+    }
+
+    /**
+     * 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)
+    {
+        // 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;
+    }
+
+    /**
+     * 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 = str_replace("\n", ' ', $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;
+    }
+
+    /**
+     * 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.
+     *
+     * @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;
+    }
+
+    /**
+     * 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()
+    {
+        if (!isset($this->inputOptions)) {
+            $this->inputOptions = $this->createInputOptions();
+        }
+        return $this->inputOptions;
+    }
+
+    protected function createInputOptions()
+    {
+        $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);
+            } 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);
+            }
+        }
+
+        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();
+    }
+
+    /**
+     * 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 array $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);
+    }
+}