Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like Parser 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 Parser, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 28 | class Parser extends Tokenizer |
||
| 29 | { |
||
| 30 | |||
| 31 | /** @var array */ |
||
| 32 | protected $variablesInfo = []; |
||
| 33 | |||
| 34 | /** @var array */ |
||
| 35 | protected $variableTypeUsage = []; |
||
| 36 | |||
| 37 | 68 | public function parse($source = null) |
|
| 38 | { |
||
| 39 | 68 | $this->initTokenizer($source); |
|
| 40 | |||
| 41 | 68 | $data = ['queries' => [], 'mutations' => [], 'fragments' => []]; |
|
| 42 | |||
| 43 | 68 | while (!$this->end()) { |
|
| 44 | 67 | $tokenType = $this->peek()->getType(); |
|
| 45 | |||
| 46 | switch ($tokenType) { |
||
| 47 | 67 | case Token::TYPE_LBRACE: |
|
| 48 | 67 | case Token::TYPE_QUERY: |
|
| 49 | 54 | $data = array_merge($data, ['queries' => $this->parseBody()]); |
|
| 50 | 47 | break; |
|
| 51 | |||
| 52 | 19 | case Token::TYPE_MUTATION: |
|
| 53 | 12 | $data = array_merge($data, ['mutations' => $this->parseBody(Token::TYPE_MUTATION)]); |
|
| 54 | 6 | break; |
|
| 55 | |||
| 56 | 7 | case Token::TYPE_FRAGMENT: |
|
| 57 | 6 | $data['fragments'][] = $this->parseFragment(); |
|
| 58 | 6 | break; |
|
| 59 | |||
| 60 | 1 | default: |
|
| 61 | 1 | throw new SyntaxErrorException('Incorrect request syntax'); |
|
| 62 | 1 | } |
|
| 63 | 53 | } |
|
| 64 | |||
| 65 | 54 | $this->checkVariableUsage(); |
|
| 66 | |||
| 67 | 53 | return $data; |
|
| 68 | } |
||
| 69 | |||
| 70 | 66 | protected function parseBody($token = Token::TYPE_QUERY, $highLevel = true) |
|
| 110 | |||
| 111 | 54 | protected function checkVariableUsage() |
|
| 117 | |||
| 118 | 8 | protected function parseVariableTypes() |
|
| 119 | { |
||
| 120 | 8 | $types = []; |
|
| 121 | 8 | $first = true; |
|
| 122 | |||
| 123 | 8 | $this->eat(Token::TYPE_LPAREN); |
|
| 124 | |||
| 125 | 8 | while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) { |
|
| 126 | 8 | if ($first) { |
|
| 127 | 8 | $first = false; |
|
| 128 | 8 | } else { |
|
| 129 | 3 | $this->expect(Token::TYPE_COMMA); |
|
| 130 | } |
||
| 131 | |||
| 132 | 8 | $this->eat(Token::TYPE_VARIABLE); |
|
| 133 | 8 | $name = $this->parseIdentifier(); |
|
| 134 | 8 | $this->eat(Token::TYPE_COLON); |
|
| 135 | |||
| 136 | 8 | if ($this->match(Token::TYPE_LSQUARE_BRACE)) { |
|
| 137 | 1 | $isArray = true; |
|
| 138 | |||
| 139 | 1 | $this->eat(Token::TYPE_LSQUARE_BRACE); |
|
| 140 | 1 | $type = $this->parseIdentifier(); |
|
| 141 | 1 | $this->eat(Token::TYPE_RSQUARE_BRACE); |
|
| 142 | 1 | } else { |
|
| 143 | 7 | $isArray = false; |
|
| 144 | 7 | $type = $this->parseIdentifier(); |
|
| 145 | } |
||
| 146 | |||
| 147 | 8 | $required = false; |
|
| 148 | 8 | if ($this->match(Token::TYPE_REQUIRED)) { |
|
| 149 | 2 | $required = true; |
|
| 150 | 2 | $this->eat(Token::TYPE_REQUIRED); |
|
| 151 | 2 | } |
|
| 152 | |||
| 153 | |||
| 154 | 8 | if (array_key_exists($name, $types)) { |
|
| 155 | 1 | throw new DuplicationVariableException(sprintf('"%s" variable duplication', $name)); |
|
| 156 | } |
||
| 157 | |||
| 158 | 8 | $types[$name] = [ |
|
| 159 | 8 | 'name' => $name, |
|
| 160 | 8 | 'isArray' => $isArray, |
|
| 161 | 8 | 'required' => $required, |
|
| 162 | 'type' => $type |
||
| 163 | 8 | ]; |
|
| 164 | 8 | } |
|
| 165 | |||
| 166 | 7 | $this->expect(Token::TYPE_RPAREN); |
|
| 167 | |||
| 168 | 7 | return $types; |
|
| 169 | } |
||
| 170 | |||
| 171 | 63 | protected function expectMulti($types) |
|
| 172 | { |
||
| 173 | 63 | if ($this->matchMulti($types)) { |
|
| 174 | 63 | return $this->lex(); |
|
| 175 | } |
||
| 176 | |||
| 177 | 3 | throw $this->createUnexpectedException($this->peek()); |
|
| 178 | } |
||
| 179 | |||
| 180 | 8 | protected function parseReference() |
|
| 181 | { |
||
| 182 | 8 | $this->expectMulti([Token::TYPE_VARIABLE]); |
|
| 183 | |||
| 184 | 8 | if ($this->match(Token::TYPE_NUMBER) || $this->match(Token::TYPE_IDENTIFIER)) { |
|
| 185 | 7 | $name = $this->lex()->getData(); |
|
| 186 | |||
| 187 | 7 | if (!array_key_exists($name, $this->variablesInfo)) { |
|
| 188 | 1 | throw new VariableTypeNotDefined(sprintf('Type for variable "%s" not defined', $name)); |
|
| 189 | } |
||
| 190 | |||
| 191 | 7 | $this->variableTypeUsage[$name] = true; |
|
| 192 | |||
| 193 | 7 | $variableInfo = $this->variablesInfo[$name]; |
|
| 194 | |||
| 195 | 7 | return new Variable($variableInfo['name'], $variableInfo['type'], $variableInfo['required'], $variableInfo['isArray']); |
|
| 196 | } |
||
| 197 | |||
| 198 | 1 | throw $this->createUnexpectedException($this->peek()); |
|
| 199 | } |
||
| 200 | |||
| 201 | 5 | protected function parseFragmentReference() |
|
| 207 | |||
| 208 | 63 | protected function parseIdentifier() |
|
| 209 | { |
||
| 210 | 63 | return $this->expectMulti([ |
|
| 211 | 63 | Token::TYPE_IDENTIFIER, |
|
| 212 | 63 | Token::TYPE_MUTATION, |
|
| 213 | 63 | Token::TYPE_QUERY, |
|
| 214 | 63 | Token::TYPE_FRAGMENT, |
|
| 215 | 63 | ])->getData(); |
|
| 216 | } |
||
| 217 | |||
| 218 | 62 | protected function parseBodyItem($type = Token::TYPE_QUERY, $highLevel = true) |
|
| 219 | { |
||
| 220 | 62 | $name = $this->parseIdentifier(); |
|
| 221 | 62 | $alias = null; |
|
| 222 | |||
| 223 | 62 | if ($this->eat(Token::TYPE_COLON)) { |
|
| 224 | 12 | $alias = $name; |
|
| 225 | 12 | $name = $this->parseIdentifier(); |
|
| 226 | 12 | } |
|
| 227 | |||
| 228 | 62 | $arguments = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : []; |
|
| 229 | |||
| 230 | 54 | if ($this->match(Token::TYPE_LBRACE)) { |
|
| 231 | 46 | $fields = $this->parseBody($type == Token::TYPE_TYPED_FRAGMENT ? Token::TYPE_QUERY : $type, false); |
|
| 232 | |||
| 233 | 42 | View Code Duplication | if ($type == Token::TYPE_QUERY) { |
| 234 | 41 | return new Query($name, $alias, $arguments, $fields); |
|
| 235 | 4 | } elseif ($type == Token::TYPE_TYPED_FRAGMENT) { |
|
| 236 | 2 | return new TypedFragmentReference($name, $fields); |
|
| 237 | } else { |
||
| 238 | 2 | return new Mutation($name, $alias, $arguments, $fields); |
|
| 239 | } |
||
| 240 | View Code Duplication | } else { |
|
| 241 | 52 | if ($highLevel && $type == Token::TYPE_MUTATION) { |
|
| 242 | 5 | return new Mutation($name, $alias, $arguments); |
|
| 243 | 48 | } elseif ($highLevel && $type == Token::TYPE_QUERY) { |
|
| 244 | 7 | return new Query($name, $alias, $arguments, []); |
|
| 245 | } |
||
| 246 | |||
| 247 | 44 | return new Field($name, $alias, $arguments); |
|
| 248 | } |
||
| 249 | } |
||
| 250 | |||
| 251 | 37 | protected function parseArgumentList() |
|
| 272 | |||
| 273 | 37 | protected function parseArgument() |
|
| 281 | |||
| 282 | /** |
||
| 283 | * @return array|InputList|InputObject|Literal|Variable |
||
| 284 | * |
||
| 285 | * @throws VariableTypeNotDefined |
||
| 286 | */ |
||
| 287 | 36 | protected function parseValue() |
|
| 312 | |||
| 313 | 6 | protected function parseList($createType = true) |
|
| 314 | { |
||
| 330 | |||
| 331 | 9 | protected function parseListValue() |
|
| 332 | { |
||
| 333 | 9 | switch ($this->lookAhead->getType()) { |
|
| 334 | 9 | case Token::TYPE_NUMBER: |
|
| 354 | |||
| 355 | 6 | protected function parseObject($createType = true) |
|
| 376 | |||
| 377 | 6 | protected function parseFragment() |
|
| 389 | |||
| 390 | 65 | protected function eat($type) |
|
| 398 | |||
| 399 | 23 | protected function eatMulti($types) |
|
| 407 | |||
| 408 | 63 | protected function matchMulti($types) |
|
| 418 | } |
||
| 419 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)or! empty(...)instead.