Completed
Pull Request — master (#19)
by Greg
02:19
created

processAlternateDescriptionTag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
namespace Consolidation\AnnotatedCommand\Parser;
3
4
use phpDocumentor\Reflection\DocBlock\Tag\ParamTag;
5
use phpDocumentor\Reflection\DocBlock;
6
7
/**
8
 * Given a class and method name, parse the annotations in the
9
 * DocBlock comment, and provide accessor methods for all of
10
 * the elements that are needed to create an annotated Command.
11
 */
12
class CommandDocBlockParser
13
{
14
    /**
15
     * @var CommandInfo
16
     */
17
    protected $commandInfo;
18
19
    /**
20
     * @var array
21
     */
22
    protected $tagProcessors = [
23
        'command' => 'processCommandTag',
24
        'name' => 'processCommandTag',
25
        'param' => 'processArgumentTag',
26
        'option' => 'processOptionTag',
27
        'default' => 'processDefaultTag',
28
        'aliases' => 'processAliases',
29
        'usage' => 'processUsageTag',
30
        'description' => 'processAlternateDescriptionTag',
31
        'desc' => 'processAlternateDescriptionTag',
32
    ];
33
34
    public function __construct(CommandInfo $commandInfo)
35
    {
36
        $this->commandInfo = $commandInfo;
37
    }
38
39
    /**
40
     * Parse the docBlock comment for this command, and set the
41
     * fields of this class with the data thereby obtained.
42
     */
43
    public function parse($docblock)
44
    {
45
        $phpdoc = new DocBlock($docblock);
46
47
        // First set the description (synopsis) and help.
48
        $this->commandInfo->setDescription((string)$phpdoc->getShortDescription());
49
        $this->commandInfo->setHelp((string)$phpdoc->getLongDescription());
50
51
        // Iterate over all of the tags, and process them as necessary.
52
        foreach ($phpdoc->getTags() as $tag) {
53
            $processFn = [$this, 'processGenericTag'];
54
            if (array_key_exists($tag->getName(), $this->tagProcessors)) {
55
                $processFn = [$this, $this->tagProcessors[$tag->getName()]];
56
            }
57
            $processFn($tag);
58
        }
59
    }
60
61
    /**
62
     * Save any tag that we do not explicitly recognize in the
63
     * 'otherAnnotations' map.
64
     */
65
    protected function processGenericTag($tag)
66
    {
67
        $this->commandInfo->addOtherAnnotation($tag->getName(), $tag->getContent());
68
    }
69
70
    /**
71
     * Set the name of the command from a @command or @name annotation.
72
     */
73
    protected function processCommandTag($tag)
74
    {
75
        $this->commandInfo->setName($tag->getContent());
76
        // We also store the name in the 'other annotations' so that is is
77
        // possible to determine if the method had a @command annotation.
78
        $this->processGenericTag($tag);
79
    }
80
81
    /**
82
     * The @description and @desc annotations may be used in
83
     * place of the synopsis (which we call 'description').
84
     * This is discouraged.
85
     *
86
     * @deprecated
87
     */
88
    protected function processAlternateDescriptionTag($tag)
89
    {
90
        $this->commandInfo->setDescription($tag->getContent());
91
    }
92
93
    /**
94
     * Store the data from a @param annotation in our argument descriptions.
95
     */
96
    protected function processArgumentTag($tag)
97
    {
98
        if (!$tag instanceof ParamTag) {
99
            return;
100
        }
101
        $variableName = $tag->getVariableName();
102
        $variableName = str_replace('$', '', $variableName);
103
        $description = static::removeLineBreaks($tag->getDescription());
104
        $this->commandInfo->arguments()->add($variableName, $description);
105
    }
106
107
    /**
108
     * Given a docblock description in the form "$variable description",
109
     * return the variable name and description via the 'match' parameter.
110
     */
111
    protected function pregMatchNameAndDescription($source, &$match)
112
    {
113
        $nameRegEx = '\\$(?P<name>[^ \t]+)[ \t]+';
114
        $descriptionRegEx = '(?P<description>.*)';
115
        $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
116
117
        return preg_match($optionRegEx, $source, $match);
118
    }
119
120
    /**
121
     * Store the data from an @option annotation in our option descriptions.
122
     */
123
    protected function processOptionTag($tag)
124
    {
125
        if (!$this->pregMatchNameAndDescription($tag->getDescription(), $match)) {
126
            return;
127
        }
128
        $variableName = $this->commandInfo->findMatchingOption($match['name']);
129
        $desc = $match['description'];
130
        $description = static::removeLineBreaks($desc);
131
        $this->commandInfo->options()->add($variableName, $description);
132
    }
133
134
    /**
135
     * Store the data from a @default annotation in our argument or option store,
136
     * as appropriate.
137
     */
138
    protected function processDefaultTag($tag)
139
    {
140
        if (!$this->pregMatchNameAndDescription($tag->getDescription(), $match)) {
141
            return;
142
        }
143
        $variableName = $match['name'];
144
        $defaultValue = $this->interpretDefaultValue($match['description']);
145
        if ($this->commandInfo->arguments()->exists($variableName)) {
146
            $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
147
            return;
148
        }
149
        $variableName = $this->commandInfo->findMatchingOption($variableName);
150
        if ($this->commandInfo->options()->exists($variableName)) {
151
            $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
152
        }
153
    }
154
155
    protected function interpretDefaultValue($defaultValue)
156
    {
157
        $defaults = [
158
            'null' => null,
159
            'true' => true,
160
            'false' => false,
161
            "''" => '',
162
            '[]' => [],
163
        ];
164
        foreach ($defaults as $defaultName => $defaultTypedValue) {
165
            if ($defaultValue == $defaultName) {
166
                return $defaultTypedValue;
167
            }
168
        }
169
        return $defaultValue;
170
    }
171
172
    /**
173
     * Process the comma-separated list of aliases
174
     */
175
    protected function processAliases($tag)
176
    {
177
        $this->commandInfo->setAliases($tag->getDescription());
178
    }
179
180
    /**
181
     * Store the data from a @usage annotation in our example usage list.
182
     */
183
    protected function processUsageTag($tag)
184
    {
185
        $lines = explode("\n", $tag->getContent());
186
        $usage = array_shift($lines);
187
        $description = static::removeLineBreaks(implode("\n", $lines));
188
189
        $this->commandInfo->setExampleUsage($usage, $description);
190
    }
191
192
    /**
193
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
194
     * convert the data into the last of these forms.
195
     */
196
    protected static function convertListToCommaSeparated($text)
197
    {
198
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
199
    }
200
201
    /**
202
     * Take a multiline description and convert it into a single
203
     * long unbroken line.
204
     */
205
    protected static function removeLineBreaks($text)
206
    {
207
        return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));
208
    }
209
}
210