Complex classes like Attributes 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 Attributes, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 10 | class Attributes |
||
| 11 | { |
||
| 12 | protected $token; |
||
| 13 | |||
| 14 | public function __construct($token = null) |
||
| 15 | { |
||
| 16 | $this->token = $token; |
||
| 17 | } |
||
| 18 | |||
| 19 | protected function parseSpace($states, $escapedAttribute, &$key, &$val, $char, $previousNonBlankChar, $nextChar) |
||
| 20 | { |
||
| 21 | if ( |
||
| 22 | in_array($states->current(), array('expr', 'array', 'string', 'object')) || |
||
| 23 | ( |
||
| 24 | ($char === ' ' || $char === "\t") && |
||
| 25 | ( |
||
| 26 | !preg_match('/^[a-zA-Z0-9_\\x7f-\\xff"\'\\]\\)\\}]$/', $previousNonBlankChar) || |
||
| 27 | !preg_match('/^[a-zA-Z0-9_]$/', $nextChar) |
||
| 28 | ) |
||
| 29 | ) |
||
| 30 | ) { |
||
| 31 | $val .= $char; |
||
| 32 | |||
| 33 | return; |
||
| 34 | } |
||
| 35 | |||
| 36 | $states->push('key'); |
||
| 37 | $val = trim($val); |
||
| 38 | $key = trim($key); |
||
| 39 | |||
| 40 | if (empty($key)) { |
||
| 41 | return; |
||
| 42 | } |
||
| 43 | |||
| 44 | $key = preg_replace( |
||
| 45 | array('/^[\'\"]|[\'\"]$/', '/\!/'), '', $key |
||
| 46 | ); |
||
| 47 | $this->token->escaped[$key] = $escapedAttribute; |
||
| 48 | |||
| 49 | $this->token->attributes[$key] = ('' === $val) ? true : $this->interpolate($val); |
||
| 50 | |||
| 51 | $key = ''; |
||
| 52 | $val = ''; |
||
| 53 | } |
||
| 54 | |||
| 55 | protected function replaceInterpolationsInStrings($match) |
||
| 56 | { |
||
| 57 | $quote = $match[1]; |
||
| 58 | |||
| 59 | return str_replace('\\#{', '#{', preg_replace_callback('/(?<!\\\\)#{([^}]+)}/', function ($match) use ($quote) { |
||
| 60 | return $quote . ' . ' . CommonUtils::addDollarIfNeeded(preg_replace_callback( |
||
| 61 | '/(?<![a-zA-Z0-9_\$])(\$?[a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_])/', |
||
| 62 | function ($match) { |
||
| 63 | return CommonUtils::getGetter($match[1], $match[2]); |
||
| 64 | }, |
||
| 65 | $match[1] |
||
| 66 | )) . ' . ' . $quote; |
||
| 67 | }, $match[0])); |
||
| 68 | } |
||
| 69 | |||
| 70 | protected function interpolate($attr) |
||
| 71 | { |
||
| 72 | return preg_replace_callback('/([\'"]).*?(?<!\\\\)(?:\\\\\\\\)*\1/', array($this, 'replaceInterpolationsInStrings'), $attr); |
||
| 73 | } |
||
| 74 | |||
| 75 | protected function parseEqual($states, &$escapedAttribute, &$key, &$val, $char, $previousChar) |
||
| 76 | { |
||
| 77 | switch ($states->current()) { |
||
| 78 | case 'key char': |
||
| 79 | $key .= $char; |
||
| 80 | break; |
||
| 81 | |||
| 82 | case 'val': |
||
| 83 | case 'expr': |
||
| 84 | case 'array': |
||
| 85 | case 'string': |
||
| 86 | case 'object': |
||
| 87 | $val .= $char; |
||
| 88 | break; |
||
| 89 | |||
| 90 | default: |
||
| 91 | $escapedAttribute = '!' !== $previousChar; |
||
| 92 | $states->push('val'); |
||
| 93 | } |
||
| 94 | } |
||
| 95 | |||
| 96 | protected function parsePairs($states, $char, &$val) |
||
| 97 | { |
||
| 98 | switch ($char) { |
||
| 99 | case '(': |
||
| 100 | $states->pushFor('expr', 'val', 'expr'); |
||
| 101 | break; |
||
| 102 | |||
| 103 | case ')': |
||
| 104 | $states->popFor('val', 'expr'); |
||
| 105 | break; |
||
| 106 | |||
| 107 | case '{': |
||
| 108 | $states->pushFor('object', 'val'); |
||
| 109 | break; |
||
| 110 | |||
| 111 | case '}': |
||
| 112 | $states->popFor('object'); |
||
| 113 | break; |
||
| 114 | |||
| 115 | case '[': |
||
| 116 | $states->pushFor('array', 'val'); |
||
| 117 | break; |
||
| 118 | |||
| 119 | case ']': |
||
| 120 | $states->popFor('array'); |
||
| 121 | break; |
||
| 122 | |||
| 123 | default: |
||
| 124 | return false; |
||
| 125 | } |
||
| 126 | $val .= $char; |
||
| 127 | |||
| 128 | return true; |
||
| 129 | } |
||
| 130 | |||
| 131 | protected function parseString(&$states, &$key, &$val, &$quote, $char) |
||
| 132 | { |
||
| 133 | if (($char === '"' || $char === "'") && !CommonUtils::escapedEnd($val)) { |
||
| 134 | $stringParser = new StringAttribute($char); |
||
| 135 | $stringParser->parse($states, $val, $quote); |
||
| 136 | |||
| 137 | return; |
||
| 138 | } |
||
| 139 | ${in_array($states->current(), array('key', 'key char')) ? 'key' : 'val'} .= $char; |
||
| 140 | } |
||
| 141 | |||
| 142 | public function parseChar($char, &$nextChar, &$key, &$val, &$quote, $states, &$escapedAttribute, &$previousChar, &$previousNonBlankChar) |
||
| 143 | { |
||
| 144 | if ($this->parsePairs($states, $char, $val)) { |
||
| 145 | return; |
||
| 146 | } |
||
| 147 | |||
| 148 | switch ($char) { |
||
| 149 | case ',': |
||
| 150 | case "\n": |
||
| 151 | case "\t": |
||
| 152 | case ' ': |
||
| 153 | $this->parseSpace($states, $escapedAttribute, $key, $val, $char, $previousNonBlankChar, $nextChar); |
||
| 154 | break; |
||
| 155 | |||
| 156 | case '=': |
||
| 157 | $this->parseEqual($states, $escapedAttribute, $key, $val, $char, $previousChar); |
||
| 158 | break; |
||
| 159 | |||
| 160 | default: |
||
| 161 | $this->parseString($states, $key, $val, $quote, $char); |
||
| 162 | } |
||
| 163 | } |
||
| 164 | |||
| 165 | protected function getParseFunction(&$key, &$val, &$quote, $states, &$escapedAttribute, &$previousChar, &$previousNonBlankChar, $parser) |
||
| 166 | { |
||
| 167 | return function ($char, $nextChar = '') use (&$key, &$val, &$quote, $states, &$escapedAttribute, &$previousChar, &$previousNonBlankChar, $parser) { |
||
| 168 | $parser->parseChar($char, $nextChar, $key, $val, $quote, $states, $escapedAttribute, $previousChar, $previousNonBlankChar); |
||
| 169 | $previousChar = $char; |
||
| 170 | if (trim($char) !== '') { |
||
| 171 | $previousNonBlankChar = $char; |
||
| 172 | } |
||
| 173 | }; |
||
| 174 | } |
||
| 175 | |||
| 176 | /** |
||
| 177 | * @return object |
||
| 178 | */ |
||
| 179 | public function parseWith($str) |
||
| 180 | { |
||
| 181 | $parser = $this; |
||
| 182 | |||
| 183 | $key = ''; |
||
| 184 | $val = ''; |
||
| 185 | $quote = ''; |
||
| 186 | $states = new AttributesState(); |
||
| 187 | $escapedAttribute = ''; |
||
| 188 | $previousChar = ''; |
||
| 189 | $previousNonBlankChar = ''; |
||
| 190 | |||
| 191 | $parse = $this->getParseFunction($key, $val, $quote, $states, $escapedAttribute, $previousChar, $previousNonBlankChar, $parser); |
||
| 192 | |||
| 193 | for ($i = 0; $i < strlen($str); $i++) { |
||
| 194 | $parse(substr($str, $i, 1), substr($str, $i + 1, 1)); |
||
| 195 | } |
||
| 196 | |||
| 197 | $parse(','); |
||
| 198 | } |
||
| 199 | } |
||
| 200 |