1 | <?php |
||
17 | final class VersionTokenizer |
||
18 | { |
||
19 | private $comparatorMap = [ |
||
20 | '>' => Token::GREATER_THAN, |
||
21 | '>=' => Token::GREATER_THAN_EQUAL, |
||
22 | '<' => Token::LESS_THAN, |
||
23 | '<=' => Token::LESS_THAN_EQUAL, |
||
24 | '=' => Token::EQUAL_TO |
||
25 | ]; |
||
26 | |||
27 | /** |
||
28 | * Accepts a version string & returns an array of tokenized values. |
||
29 | * |
||
30 | * @param string $versionString |
||
31 | * |
||
32 | * @return Token[] |
||
33 | */ |
||
34 | 31 | public function tokenize($versionString) |
|
35 | { |
||
36 | /** @var Token[] $tokenList */ |
||
37 | 31 | $tokenList = []; |
|
38 | 31 | $digitAccumulator = ''; |
|
39 | 31 | $stringAccumulator = ''; |
|
40 | |||
41 | // Iterate through string, character-by-character |
||
42 | 31 | for ($i = 0; $i < strlen($versionString); $i++) { |
|
43 | 31 | $chr = $this->getCharacter($i, $versionString); |
|
44 | |||
45 | switch (true) { |
||
46 | |||
47 | // Simple token types - separators and range indicators |
||
48 | 31 | case !is_null($this->getSimpleToken($chr)): |
|
|
|||
49 | |||
50 | // Handle preceding digits or labels |
||
51 | 29 | $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList); |
|
52 | 29 | $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList); |
|
53 | |||
54 | 29 | $tokenList[] = $this->getSimpleToken($chr); |
|
55 | 29 | break; |
|
56 | |||
57 | // Comparator token types |
||
58 | 31 | case !is_null($token = $this->getComparatorToken($i, $versionString)): |
|
59 | |||
60 | // Handle preceding digits or labels |
||
61 | 11 | $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList); |
|
62 | 11 | $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList); |
|
63 | |||
64 | 11 | $this->addImplicitAnd($tokenList); |
|
65 | |||
66 | 11 | $tokenList[] = $token; |
|
67 | 11 | break; |
|
68 | |||
69 | // Skip the 'v' character if immediately preceding a digit |
||
70 | 31 | case $this->isPrefixedV($i, $versionString): |
|
71 | // Do nothing |
||
72 | 1 | break; |
|
73 | |||
74 | // Spaces, pipes, ampersands and commas may contextually be logical AND/OR |
||
75 | 31 | case $this->isPossibleLogicalOperator($chr): |
|
76 | |||
77 | // Handle preceding digits or labels |
||
78 | 6 | $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList); |
|
79 | 6 | $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList); |
|
80 | |||
81 | $operatorTokenList = [ |
||
82 | 6 | Token::GREATER_THAN, |
|
83 | Token::GREATER_THAN_EQUAL, |
||
84 | Token::LESS_THAN, |
||
85 | Token::LESS_THAN_EQUAL, |
||
86 | Token::EQUAL_TO |
||
87 | ]; |
||
88 | |||
89 | // No previous tokens, or previous token was not a comparator |
||
90 | if ( |
||
91 | 6 | !count($tokenList) |
|
92 | 6 | || !in_array($tokenList[count($tokenList)-1]->getType(), $operatorTokenList) |
|
93 | ) { |
||
94 | 5 | $possibleOperator = trim($chr); |
|
95 | |||
96 | 5 | for ($j = $i + 1; $j < strlen($versionString); $j++) { |
|
97 | 5 | $operatorChr = $this->getCharacter($j, $versionString); |
|
98 | |||
99 | 5 | if ($this->isPossibleLogicalOperator($operatorChr)) { |
|
100 | 3 | $possibleOperator .= trim($operatorChr); |
|
101 | } else { |
||
102 | 5 | if (!strlen($possibleOperator) || ',' === $possibleOperator) { |
|
103 | 4 | $tokenList[] = new Token(Token::LOGICAL_AND, $possibleOperator); |
|
104 | } else { |
||
105 | 2 | $tokenList[] = new Token(Token::LOGICAL_OR, $possibleOperator); |
|
106 | } |
||
107 | 5 | $i = $j - 1; |
|
108 | 5 | break; |
|
109 | } |
||
110 | } |
||
111 | } |
||
112 | |||
113 | 6 | break; |
|
114 | |||
115 | // Start accumulating on the first non-digit & continue until we reach a separator |
||
116 | 31 | case !is_numeric($chr) || strlen($stringAccumulator): |
|
117 | 5 | $stringAccumulator .= $chr; |
|
118 | 5 | break; |
|
119 | |||
120 | // Simple digits |
||
121 | default: |
||
122 | 31 | $digitAccumulator .= $chr; |
|
123 | 31 | break; |
|
124 | } |
||
125 | } |
||
126 | |||
127 | // Handle any remaining digits or labels |
||
128 | 31 | $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList); |
|
129 | 31 | $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList); |
|
130 | |||
131 | 31 | return $tokenList; |
|
132 | } |
||
133 | |||
134 | /** |
||
135 | * Add an implicit AND between a version & the comparator of a subsequent version. |
||
136 | * |
||
137 | * @param Token[] $tokenList |
||
138 | */ |
||
139 | 11 | private function addImplicitAnd(array &$tokenList) |
|
140 | { |
||
141 | $digitTokenList = [ |
||
142 | 11 | Token::DIGITS, |
|
143 | Token::WILDCARD_DIGITS |
||
144 | ]; |
||
145 | |||
146 | 11 | if (count($tokenList) && in_array($tokenList[count($tokenList) -1]->getType(), $digitTokenList)) { |
|
147 | 1 | $tokenList[] = new Token(Token::LOGICAL_AND, ''); |
|
148 | } |
||
149 | 11 | } |
|
150 | |||
151 | /** |
||
152 | * Returns true if the character is an AND comparator |
||
153 | * |
||
154 | * @param string $chr |
||
155 | * |
||
156 | * @return bool |
||
157 | */ |
||
158 | 31 | private function isPossibleLogicalOperator($chr) |
|
159 | { |
||
160 | 31 | return in_array($chr, array(',', '|')) || ctype_space($chr); |
|
161 | } |
||
162 | |||
163 | /** |
||
164 | * Tries to find a comparator token beginning at the specified index. |
||
165 | * |
||
166 | * @param int $index |
||
167 | * @param string $versionString |
||
168 | * |
||
169 | * @return Token|null |
||
170 | */ |
||
171 | 31 | private function getComparatorToken(&$index, $versionString) |
|
172 | { |
||
173 | 31 | $token = null; |
|
174 | |||
175 | // See if the first character matches a token |
||
176 | 31 | $comparator = substr($versionString, $index, 1); |
|
177 | 31 | if (array_key_exists($comparator, $this->comparatorMap)) { |
|
178 | |||
179 | // Check for second character in comparator ('<=' or '=>') |
||
180 | 11 | $nextChr = $this->getCharacter($index + 1, $versionString); |
|
181 | 11 | if (array_key_exists($comparator . $nextChr, $this->comparatorMap)) { |
|
182 | 6 | $comparator .= $nextChr; |
|
183 | 6 | $index++; |
|
184 | } |
||
185 | |||
186 | 11 | $token = new Token($this->comparatorMap[$comparator], $comparator); |
|
187 | } |
||
188 | |||
189 | 31 | return $token; |
|
190 | } |
||
191 | |||
192 | /** |
||
193 | * Get a token from a single-character. |
||
194 | * |
||
195 | * @param string $chr |
||
196 | * |
||
197 | * @return Token|null |
||
198 | */ |
||
199 | 31 | private function getSimpleToken($chr) |
|
217 | |||
218 | /** |
||
219 | * Add a token to the list if the string length is greater than 0, empties string. |
||
220 | * |
||
221 | * @param string $type One of Token class constants |
||
222 | * @param string $value |
||
223 | * @param Token[] $tokenList |
||
224 | */ |
||
225 | 31 | public function conditionallyAddToken($type, &$value, &$tokenList) |
|
236 | |||
237 | /** |
||
238 | * Returns true if the character is a 'v' prefix to a version number (e.g. v1.0.7). |
||
239 | * |
||
240 | * @param int $index |
||
241 | * @param string $versionString |
||
242 | * |
||
243 | * @return bool |
||
244 | */ |
||
245 | 31 | private function isPrefixedV($index, $versionString) |
|
253 | |||
254 | /** |
||
255 | * Get the next character after the specified index or an empty string if not present. |
||
256 | * |
||
257 | * @param int $index |
||
258 | * @param string $versionString |
||
259 | * @return string |
||
260 | */ |
||
261 | 31 | private function getCharacter($index, $versionString) |
|
270 | } |
||
271 |
According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.
}
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.