Complex classes like DNParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use DNParser, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
16 | class DNParser |
||
17 | { |
||
18 | /** |
||
19 | * RFC 2253 special characters. |
||
20 | * |
||
21 | * @var string |
||
22 | */ |
||
23 | const SPECIAL_CHARS = ',=+<>#;'; |
||
24 | |||
25 | /** |
||
26 | * DN string. |
||
27 | * |
||
28 | * @var string |
||
29 | */ |
||
30 | private $_dn; |
||
31 | |||
32 | /** |
||
33 | * DN string length. |
||
34 | * |
||
35 | * @var int |
||
36 | */ |
||
37 | private $_len; |
||
38 | |||
39 | /** |
||
40 | * Constructor. |
||
41 | * |
||
42 | * @param string $dn Distinguised name |
||
43 | */ |
||
44 | 70 | protected function __construct(string $dn) |
|
45 | { |
||
46 | 70 | $this->_dn = $dn; |
|
47 | 70 | $this->_len = strlen($dn); |
|
48 | 70 | } |
|
49 | |||
50 | /** |
||
51 | * Parse distinguished name string to name-components. |
||
52 | * |
||
53 | * @param string $dn |
||
54 | * |
||
55 | * @return array |
||
56 | */ |
||
57 | 70 | public static function parseString(string $dn): array |
|
58 | { |
||
59 | 70 | $parser = new self($dn); |
|
60 | 70 | return $parser->parse(); |
|
61 | } |
||
62 | |||
63 | /** |
||
64 | * Escape a AttributeValue string conforming to RFC 2253. |
||
65 | * |
||
66 | * @see https://tools.ietf.org/html/rfc2253#section-2.4 |
||
67 | * |
||
68 | * @param string $str |
||
69 | * |
||
70 | * @return string |
||
71 | */ |
||
72 | 30 | public static function escapeString(string $str): string |
|
73 | { |
||
74 | // one of the characters ",", "+", """, "\", "<", ">" or ";" |
||
75 | 30 | $str = preg_replace('/([,\+"\\\<\>;])/u', '\\\\$1', $str); |
|
76 | // a space character occurring at the end of the string |
||
77 | 30 | $str = preg_replace('/( )$/u', '\\\\$1', $str); |
|
78 | // a space or "#" character occurring at the beginning of the string |
||
79 | 30 | $str = preg_replace('/^([ #])/u', '\\\\$1', $str); |
|
80 | // implementation specific special characters |
||
81 | 30 | $str = preg_replace_callback('/([\pC])/u', |
|
82 | function ($m) { |
||
83 | 2 | $octets = str_split(bin2hex($m[1]), 2); |
|
84 | 2 | return implode('', |
|
85 | 2 | array_map( |
|
86 | function ($octet) { |
||
87 | 2 | return '\\' . strtoupper($octet); |
|
88 | 2 | }, $octets)); |
|
89 | 30 | }, $str); |
|
90 | 30 | return $str; |
|
91 | } |
||
92 | |||
93 | /** |
||
94 | * Parse DN to name-components. |
||
95 | * |
||
96 | * @throws \RuntimeException |
||
97 | * |
||
98 | * @return array |
||
99 | */ |
||
100 | 70 | protected function parse(): array |
|
111 | } |
||
112 | |||
113 | /** |
||
114 | * Parse 'name'. |
||
115 | * |
||
116 | * name-component *("," name-component) |
||
117 | * |
||
118 | * @param int $offset |
||
119 | * |
||
120 | * @return array Array of name-components |
||
121 | */ |
||
122 | 70 | private function _parseName(int &$offset): array |
|
123 | { |
||
124 | 70 | $idx = $offset; |
|
125 | 70 | $names = []; |
|
126 | 70 | while ($idx < $this->_len) { |
|
127 | 70 | $names[] = $this->_parseNameComponent($idx); |
|
128 | 62 | if ($idx >= $this->_len) { |
|
129 | 61 | break; |
|
130 | } |
||
131 | 20 | $this->_skipWs($idx); |
|
132 | 20 | if (',' != $this->_dn[$idx] && ';' != $this->_dn[$idx]) { |
|
133 | 1 | break; |
|
134 | } |
||
135 | 19 | ++$idx; |
|
136 | 19 | $this->_skipWs($idx); |
|
137 | } |
||
138 | 62 | $offset = $idx; |
|
139 | 62 | return array_reverse($names); |
|
140 | } |
||
141 | |||
142 | /** |
||
143 | * Parse 'name-component'. |
||
144 | * |
||
145 | * attributeTypeAndValue *("+" attributeTypeAndValue) |
||
146 | * |
||
147 | * @param int $offset |
||
148 | * |
||
149 | * @return array Array of [type, value] tuples |
||
150 | */ |
||
151 | 70 | private function _parseNameComponent(int &$offset): array |
|
152 | { |
||
153 | 70 | $idx = $offset; |
|
154 | 70 | $tvpairs = []; |
|
155 | 70 | while ($idx < $this->_len) { |
|
156 | 70 | $tvpairs[] = $this->_parseAttrTypeAndValue($idx); |
|
157 | 62 | $this->_skipWs($idx); |
|
158 | 62 | if ($idx >= $this->_len || '+' != $this->_dn[$idx]) { |
|
159 | 62 | break; |
|
160 | } |
||
161 | 10 | ++$idx; |
|
162 | 10 | $this->_skipWs($idx); |
|
163 | } |
||
164 | 62 | $offset = $idx; |
|
165 | 62 | return $tvpairs; |
|
166 | } |
||
167 | |||
168 | /** |
||
169 | * Parse 'attributeTypeAndValue'. |
||
170 | * |
||
171 | * attributeType "=" attributeValue |
||
172 | * |
||
173 | * @param int $offset |
||
174 | * |
||
175 | * @throws \UnexpectedValueException |
||
176 | * |
||
177 | * @return array A tuple of [type, value]. Value may be either a string or |
||
178 | * an Element, if it's encoded as hexstring. |
||
179 | */ |
||
180 | 70 | private function _parseAttrTypeAndValue(int &$offset): array |
|
181 | { |
||
182 | 70 | $idx = $offset; |
|
183 | 70 | $type = $this->_parseAttrType($idx); |
|
184 | 69 | $this->_skipWs($idx); |
|
185 | 69 | if ($idx >= $this->_len || '=' != $this->_dn[$idx++]) { |
|
186 | 1 | throw new \UnexpectedValueException('Invalid type and value pair.'); |
|
187 | } |
||
188 | 68 | $this->_skipWs($idx); |
|
189 | // hexstring |
||
190 | 68 | if ($idx < $this->_len && '#' == $this->_dn[$idx]) { |
|
191 | 8 | ++$idx; |
|
192 | 8 | $data = $this->_parseAttrHexValue($idx); |
|
193 | try { |
||
194 | 7 | $value = Element::fromDER($data); |
|
195 | 1 | } catch (DecodeException $e) { |
|
196 | 1 | throw new \UnexpectedValueException( |
|
197 | 7 | 'Invalid DER encoding from hexstring.', 0, $e); |
|
198 | } |
||
199 | } else { |
||
200 | 60 | $value = $this->_parseAttrStringValue($idx); |
|
201 | } |
||
202 | 62 | $offset = $idx; |
|
203 | 62 | return [$type, $value]; |
|
204 | } |
||
205 | |||
206 | /** |
||
207 | * Parse 'attributeType'. |
||
208 | * |
||
209 | * (ALPHA 1*keychar) / oid |
||
210 | * |
||
211 | * @param int $offset |
||
212 | * |
||
213 | * @throws \UnexpectedValueException |
||
214 | * |
||
215 | * @return string |
||
216 | */ |
||
217 | 70 | private function _parseAttrType(int &$offset): string |
|
231 | } |
||
232 | |||
233 | /** |
||
234 | * Parse 'attributeValue' of string type. |
||
235 | * |
||
236 | * @param int $offset |
||
237 | * |
||
238 | * @throws \UnexpectedValueException |
||
239 | * |
||
240 | * @return string |
||
241 | */ |
||
242 | 60 | private function _parseAttrStringValue(int &$offset): string |
|
243 | { |
||
244 | 60 | $idx = $offset; |
|
245 | 60 | if ($idx >= $this->_len) { |
|
246 | 2 | return ''; |
|
247 | } |
||
248 | 58 | if ('"' == $this->_dn[$idx]) { // quoted string |
|
249 | 4 | $val = $this->_parseQuotedAttrString($idx); |
|
250 | } else { // string |
||
251 | 54 | $val = $this->_parseAttrString($idx); |
|
252 | } |
||
253 | 54 | $offset = $idx; |
|
254 | 54 | return $val; |
|
255 | } |
||
256 | |||
257 | /** |
||
258 | * Parse plain 'attributeValue' string. |
||
259 | * |
||
260 | * @param int $offset |
||
261 | * |
||
262 | * @throws \UnexpectedValueException |
||
263 | * |
||
264 | * @return string |
||
265 | */ |
||
266 | 54 | private function _parseAttrString(int &$offset): string |
|
267 | { |
||
268 | 54 | $idx = $offset; |
|
269 | 54 | $val = ''; |
|
270 | 54 | $wsidx = null; |
|
271 | 54 | while ($idx < $this->_len) { |
|
272 | 54 | $c = $this->_dn[$idx]; |
|
273 | // pair (escape sequence) |
||
274 | 54 | if ('\\' == $c) { |
|
275 | 10 | ++$idx; |
|
276 | 10 | $val .= $this->_parsePairAfterSlash($idx); |
|
277 | 7 | $wsidx = null; |
|
278 | 7 | continue; |
|
279 | } |
||
280 | 49 | if ('"' == $c) { |
|
281 | 1 | throw new \UnexpectedValueException('Unexpected quotation.'); |
|
282 | } |
||
283 | 49 | if (false !== strpos(self::SPECIAL_CHARS, $c)) { |
|
284 | 27 | break; |
|
285 | } |
||
286 | // keep track of the first consecutive whitespace |
||
287 | 49 | if (' ' == $c) { |
|
288 | 8 | if (null === $wsidx) { |
|
289 | 8 | $wsidx = $idx; |
|
290 | } |
||
291 | } else { |
||
292 | 49 | $wsidx = null; |
|
293 | } |
||
294 | // stringchar |
||
295 | 49 | $val .= $c; |
|
296 | 49 | ++$idx; |
|
297 | } |
||
298 | // if there was non-escaped whitespace in the end of the value |
||
299 | 50 | if (null !== $wsidx) { |
|
300 | 5 | $val = substr($val, 0, -($idx - $wsidx)); |
|
301 | } |
||
302 | 50 | $offset = $idx; |
|
303 | 50 | return $val; |
|
304 | } |
||
305 | |||
306 | /** |
||
307 | * Parse quoted 'attributeValue' string. |
||
308 | * |
||
309 | * @param int $offset Offset to starting quote |
||
310 | * |
||
311 | * @throws \UnexpectedValueException |
||
312 | * |
||
313 | * @return string |
||
314 | */ |
||
315 | 4 | private function _parseQuotedAttrString(int &$offset): string |
|
316 | { |
||
317 | 4 | $idx = $offset + 1; |
|
318 | 4 | $val = ''; |
|
319 | 4 | while ($idx < $this->_len) { |
|
320 | 4 | $c = $this->_dn[$idx]; |
|
321 | 4 | if ('\\' == $c) { // pair |
|
322 | 1 | ++$idx; |
|
323 | 1 | $val .= $this->_parsePairAfterSlash($idx); |
|
324 | 1 | continue; |
|
325 | } |
||
326 | 4 | if ('"' == $c) { |
|
327 | 4 | ++$idx; |
|
328 | 4 | break; |
|
329 | } |
||
330 | 4 | $val .= $c; |
|
331 | 4 | ++$idx; |
|
332 | } |
||
333 | 4 | $offset = $idx; |
|
334 | 4 | return $val; |
|
335 | } |
||
336 | |||
337 | /** |
||
338 | * Parse 'attributeValue' of binary type. |
||
339 | * |
||
340 | * @param int $offset |
||
341 | * |
||
342 | * @throws \UnexpectedValueException |
||
343 | * |
||
344 | * @return string |
||
345 | */ |
||
346 | 8 | private function _parseAttrHexValue(int &$offset): string |
|
347 | { |
||
348 | 8 | $idx = $offset; |
|
349 | 8 | $hexstr = $this->_regexMatch('/^(?:[0-9a-f]{2})+/i', $idx); |
|
350 | 8 | if (null === $hexstr) { |
|
351 | 1 | throw new \UnexpectedValueException('Invalid hexstring.'); |
|
352 | } |
||
353 | 7 | $data = hex2bin($hexstr); |
|
354 | 7 | $offset = $idx; |
|
355 | 7 | return $data; |
|
356 | } |
||
357 | |||
358 | /** |
||
359 | * Parse 'pair' after leading slash. |
||
360 | * |
||
361 | * @param int $offset |
||
362 | * |
||
363 | * @throws \UnexpectedValueException |
||
364 | * |
||
365 | * @return string |
||
366 | */ |
||
367 | 11 | private function _parsePairAfterSlash(int &$offset): string |
|
389 | } |
||
390 | |||
391 | /** |
||
392 | * Match DN to pattern and extract the last capture group. |
||
393 | * |
||
394 | * Updates offset to fully matched pattern. |
||
395 | * |
||
396 | * @param string $pattern |
||
397 | * @param int $offset |
||
398 | * |
||
399 | * @return null|string Null if pattern doesn't match |
||
400 | */ |
||
401 | 70 | private function _regexMatch(string $pattern, int &$offset): ?string |
|
402 | { |
||
403 | 70 | $idx = $offset; |
|
404 | 70 | if (!preg_match($pattern, substr($this->_dn, $idx), $match)) { |
|
405 | 63 | return null; |
|
429 |