Complex classes like MixinVisitor 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 MixinVisitor, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 7 | abstract class MixinVisitor extends CodeVisitor |
||
| 8 | { |
||
| 9 | protected function getMixinArgumentAssign($argument) |
||
| 10 | { |
||
| 11 | $argument = trim($argument); |
||
| 12 | |||
| 13 | if (preg_match('`^[a-zA-Z][a-zA-Z0-9:_-]*\s*=`', $argument)) { |
||
| 14 | return explode('=', $argument, 2); |
||
| 15 | } |
||
| 16 | } |
||
| 17 | |||
| 18 | protected function parseMixinArguments(&$arguments, &$containsOnlyArrays, &$defaultAttributes) |
||
| 19 | { |
||
| 20 | $newArrayKey = null; |
||
| 21 | $arguments = is_null($arguments) ? array() : explode(',', $arguments); |
||
| 22 | foreach ($arguments as $key => &$argument) { |
||
| 23 | if ($tab = $this->getMixinArgumentAssign($argument)) { |
||
| 24 | if (is_null($newArrayKey)) { |
||
| 25 | $newArrayKey = $key; |
||
| 26 | $argument = array(); |
||
| 27 | } else { |
||
| 28 | unset($arguments[$key]); |
||
| 29 | } |
||
| 30 | |||
| 31 | $defaultAttributes[] = var_export($tab[0], true) . ' => ' . $tab[1]; |
||
| 32 | $arguments[$newArrayKey][$tab[0]] = static::decodeValue($tab[1]); |
||
| 33 | continue; |
||
| 34 | } |
||
| 35 | |||
| 36 | $containsOnlyArrays = false; |
||
| 37 | $newArrayKey = null; |
||
| 38 | } |
||
| 39 | |||
| 40 | return array_map(function ($argument) { |
||
| 41 | if (is_array($argument)) { |
||
| 42 | $argument = var_export($argument, true); |
||
| 43 | } |
||
| 44 | |||
| 45 | return $argument; |
||
| 46 | }, $arguments); |
||
| 47 | } |
||
| 48 | |||
| 49 | protected function parseMixinStringAttribute($data) |
||
| 50 | { |
||
| 51 | $value = is_array($data['value']) |
||
| 52 | ? preg_split('`\s+`', trim(implode(' ', $data['value']))) |
||
| 53 | : trim($data['value']); |
||
| 54 | |||
| 55 | return $data['escaped'] === true |
||
| 56 | ? is_array($value) |
||
| 57 | ? array_map('htmlspecialchars', $value) |
||
| 58 | : htmlspecialchars($value) |
||
| 59 | : $value; |
||
| 60 | } |
||
| 61 | |||
| 62 | protected function parseMixinAttribute($data) |
||
| 63 | { |
||
| 64 | if ($data['value'] === 'null' || $data['value'] === 'undefined' || is_null($data['value'])) { |
||
| 65 | return; |
||
| 66 | } |
||
| 67 | |||
| 68 | if (is_bool($data['value'])) { |
||
| 69 | return $data['value']; |
||
| 70 | } |
||
| 71 | |||
| 72 | return $this->parseMixinStringAttribute($data); |
||
| 73 | } |
||
| 74 | |||
| 75 | protected function parseMixinAttributes($attributes, $defaultAttributes, $mixinAttributes) |
||
| 76 | { |
||
| 77 | if (!count($attributes)) { |
||
| 78 | return "(isset(\$attributes)) ? \$attributes : array($defaultAttributes)"; |
||
| 79 | } |
||
| 80 | |||
| 81 | $parsedAttributes = array(); |
||
| 82 | foreach ($attributes as $data) { |
||
| 83 | $parsedAttributes[$data['name']] = $this->parseMixinAttribute($data); |
||
| 84 | } |
||
| 85 | |||
| 86 | $attributes = var_export($parsedAttributes, true); |
||
| 87 | $mixinAttributes = var_export(static::decodeAttributes($mixinAttributes), true); |
||
| 88 | |||
| 89 | return "array_merge(\\Jade\\Compiler::withMixinAttributes($attributes, $mixinAttributes), (isset(\$attributes)) ? \$attributes : array($defaultAttributes))"; |
||
| 90 | } |
||
| 91 | |||
| 92 | protected function renderClosureOpenning() |
||
| 93 | { |
||
| 94 | $arguments = func_get_args(); |
||
| 95 | $begin = array_shift($arguments); |
||
| 96 | $begin = is_array($begin) |
||
| 97 | ? $begin[0] . 'function ' . $begin[1] |
||
| 98 | : $begin . 'function '; |
||
| 99 | $params = implode(', ', array_map(function ($name) { |
||
| 100 | return (substr($name, 0, 1) === '$' ? '' : '$') . $name; |
||
| 101 | }, $arguments)); |
||
| 102 | |||
| 103 | if ($this->restrictedScope) { |
||
| 104 | return $this->buffer($this->createCode($begin . '(' . $params . ') {')); |
||
| 105 | } |
||
| 106 | |||
| 107 | $params = '&$__varHandler, ' . $params; |
||
| 108 | |||
| 109 | $this->buffer( |
||
| 110 | $this->createCode($begin . '(' . $params . ') {') . |
||
| 111 | $this->createCode($this->indent() . 'extract($__varHandler, EXTR_SKIP);') |
||
| 112 | ); |
||
| 113 | } |
||
| 114 | |||
| 115 | protected function renderClosureClosing($code, $arguments = array()) |
||
| 116 | { |
||
| 117 | if (!$this->restrictedScope) { |
||
| 118 | $arguments = array_filter(array_map(function ($argument) { |
||
| 119 | $argument = explode('=', $argument); |
||
| 120 | $argument = trim($argument[0]); |
||
| 121 | |||
| 122 | return substr($argument, 0, 1) === '$' |
||
| 123 | ? substr($argument, 1) |
||
| 124 | : false; |
||
| 125 | }, array_slice($arguments, 1))); |
||
| 126 | $exception = count($arguments) |
||
| 127 | ? ' && !in_array($key, ' . var_export($arguments, true) . ')' |
||
| 128 | : ''; |
||
| 129 | $this->buffer($this->createCode( |
||
| 130 | 'foreach ($__varHandler as $key => &$val) {' . |
||
| 131 | 'if ($key !== \'__varHandler\'' . $exception . ') {' . |
||
| 132 | '$val = ${$key};' . |
||
| 133 | '}' . |
||
| 134 | '}' |
||
| 135 | )); |
||
| 136 | } |
||
| 137 | |||
| 138 | $this->buffer($this->createCode($code)); |
||
| 139 | } |
||
| 140 | |||
| 141 | /** |
||
| 142 | * @param Nodes\Mixin $mixin |
||
| 143 | */ |
||
| 144 | protected function visitMixinCall(Mixin $mixin, $name, $blockName, $attributes) |
||
| 145 | { |
||
| 146 | $arguments = $mixin->arguments; |
||
| 147 | $block = $mixin->block; |
||
| 148 | $defaultAttributes = array(); |
||
| 149 | $containsOnlyArrays = true; |
||
| 150 | $arguments = $this->parseMixinArguments($mixin->arguments, $containsOnlyArrays, $defaultAttributes); |
||
| 151 | |||
| 152 | $defaultAttributes = implode(', ', $defaultAttributes); |
||
| 153 | $attributes = $this->parseMixinAttributes($attributes, $defaultAttributes, $mixin->attributes); |
||
| 154 | |||
| 155 | if ($block) { |
||
| 156 | $this->renderClosureOpenning("\\Jade\\Compiler::recordMixinBlock($blockName, ", 'attributes'); |
||
| 157 | $this->visit($block); |
||
| 158 | $this->renderClosureClosing('});'); |
||
| 159 | } |
||
| 160 | |||
| 161 | $strings = array(); |
||
| 162 | $arguments = preg_replace_callback( |
||
| 163 | '#([\'"])(.*(?!<\\\\)(?:\\\\{2})*)\\1#U', |
||
| 164 | function ($match) use (&$strings) { |
||
| 165 | $nextIndex = count($strings); |
||
| 166 | $strings[] = $match[0]; |
||
| 167 | |||
| 168 | return 'stringToReplaceBy' . $nextIndex . 'ThCapture'; |
||
| 169 | }, |
||
| 170 | $arguments |
||
| 171 | ); |
||
| 172 | $arguments = array_map( |
||
| 173 | function ($arg) use ($strings) { |
||
| 174 | return preg_replace_callback( |
||
| 175 | '#stringToReplaceBy([0-9]+)ThCapture#', |
||
| 176 | function ($match) use ($strings) { |
||
| 177 | return $strings[intval($match[1])]; |
||
| 178 | }, |
||
| 179 | $arg |
||
| 180 | ); |
||
| 181 | }, |
||
| 182 | $arguments |
||
| 183 | ); |
||
| 184 | |||
| 185 | array_unshift($arguments, $attributes); |
||
| 186 | $arguments = array_filter($arguments, 'strlen'); |
||
| 187 | $statements = $this->apply('createStatements', $arguments); |
||
| 188 | |||
| 189 | $variables = array_pop($statements); |
||
| 190 | if ($mixin->call && $containsOnlyArrays) { |
||
| 191 | array_splice($variables, 1, 0, array('null')); |
||
| 192 | } |
||
| 193 | $variables = implode(', ', $variables); |
||
| 194 | array_push($statements, $variables); |
||
| 195 | |||
| 196 | $arguments = $statements; |
||
| 197 | |||
| 198 | $paramsPrefix = ''; |
||
| 199 | if (!$this->restrictedScope) { |
||
| 200 | $this->buffer($this->createCode('$__varHandler = get_defined_vars();')); |
||
| 201 | $paramsPrefix = '$__varHandler, '; |
||
| 202 | } |
||
| 203 | $codeFormat = str_repeat('%s;', count($arguments) - 1) . "{$name}({$paramsPrefix}%s)"; |
||
| 204 | |||
| 205 | array_unshift($arguments, $codeFormat); |
||
| 206 | |||
| 207 | $this->buffer($this->apply('createCode', $arguments)); |
||
| 208 | if (!$this->restrictedScope) { |
||
| 209 | $this->buffer( |
||
| 210 | $this->createCode( |
||
| 211 | 'extract(array_diff_key($__varHandler, array(\'__varHandler\' => 1, \'attributes\' => 1)));' |
||
| 212 | ) |
||
| 213 | ); |
||
| 214 | } |
||
| 215 | |||
| 216 | if ($block) { |
||
| 217 | $code = $this->createCode("\\Jade\\Compiler::terminateMixinBlock($blockName);"); |
||
| 218 | $this->buffer($code); |
||
| 219 | } |
||
| 220 | } |
||
| 221 | |||
| 222 | protected function visitMixinCodeAndBlock($name, $block, $arguments) |
||
| 223 | { |
||
| 224 | $this->renderClosureOpenning( |
||
| 225 | $this->allowMixinOverride |
||
| 226 | ? "{$name} = " |
||
| 227 | : array("if(!function_exists('{$name}')) { ", $name), |
||
| 228 | implode(',', $arguments) |
||
| 229 | ); |
||
| 230 | $this->indents++; |
||
| 231 | $this->visit($block); |
||
| 232 | $this->indents--; |
||
| 233 | $this->renderClosureClosing($this->allowMixinOverride ? '};' : '} }', $arguments); |
||
| 234 | } |
||
| 235 | |||
| 236 | /** |
||
| 237 | * @param Nodes\Mixin $mixin |
||
| 238 | */ |
||
| 239 | protected function visitMixinDeclaration(Mixin $mixin, $name) |
||
| 240 | { |
||
| 241 | $arguments = $mixin->arguments; |
||
| 242 | $block = $mixin->block; |
||
| 243 | $previousVisitedMixin = isset($this->visitedMixin) ? $this->visitedMixin : null; |
||
| 244 | $this->visitedMixin = $mixin; |
||
| 245 | if ($arguments === null || empty($arguments)) { |
||
| 246 | $arguments = array(); |
||
| 247 | } elseif (!is_array($arguments)) { |
||
| 248 | $arguments = array($arguments); |
||
| 249 | } |
||
| 250 | |||
| 251 | array_unshift($arguments, 'attributes'); |
||
| 252 | $arguments = implode(',', $arguments); |
||
| 253 | $arguments = explode(',', $arguments); |
||
| 254 | array_walk($arguments, array(get_class(), 'initArgToNull')); |
||
| 255 | $this->visitMixinCodeAndBlock($name, $block, $arguments); |
||
| 256 | |||
| 257 | if (is_null($previousVisitedMixin)) { |
||
| 258 | unset($this->visitedMixin); |
||
| 259 | |||
| 260 | return; |
||
| 261 | } |
||
| 262 | |||
| 263 | $this->visitedMixin = $previousVisitedMixin; |
||
| 264 | } |
||
| 265 | |||
| 266 | /** |
||
| 267 | * @param Nodes\Mixin $mixin |
||
| 268 | */ |
||
| 269 | protected function visitMixin(Mixin $mixin) |
||
| 270 | { |
||
| 271 | $name = strtr($mixin->name, '-', '_') . '_mixin'; |
||
| 272 | $blockName = var_export($mixin->name, true); |
||
| 273 | if ($this->allowMixinOverride) { |
||
| 274 | $name = '$GLOBALS[\'' . $name . '\']'; |
||
| 275 | } |
||
| 276 | $attributes = static::decodeAttributes($mixin->attributes); |
||
| 277 | |||
| 278 | if ($mixin->call) { |
||
| 279 | $this->visitMixinCall($mixin, $name, $blockName, $attributes); |
||
| 280 | |||
| 281 | return; |
||
| 282 | } |
||
| 283 | |||
| 284 | $this->visitMixinDeclaration($mixin, $name); |
||
| 285 | } |
||
| 286 | } |
||
| 287 |