Completed
Push — master ( d582b4...acdf9f )
by Greg
02:27
created

AbstractCommandDocBlockParser::processCommandTag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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