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 Generator 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 Generator, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 13 | class Generator |
||
| 14 | { |
||
| 15 | use ParamHelpers; |
||
| 16 | |||
| 17 | /** |
||
| 18 | * @var DocumentationConfig |
||
| 19 | */ |
||
| 20 | private $config; |
||
| 21 | |||
| 22 | public function __construct(DocumentationConfig $config = null) |
||
| 23 | { |
||
| 24 | // If no config is injected, pull from global |
||
| 25 | $this->config = $config ?: new DocumentationConfig(config('apidoc')); |
||
| 26 | } |
||
| 27 | |||
| 28 | /** |
||
| 29 | * @param Route $route |
||
| 30 | * |
||
| 31 | * @return mixed |
||
| 32 | */ |
||
| 33 | public function getUri(Route $route) |
||
| 37 | |||
| 38 | /** |
||
| 39 | * @param Route $route |
||
| 40 | * |
||
| 41 | * @return mixed |
||
| 42 | */ |
||
| 43 | public function getMethods(Route $route) |
||
| 47 | |||
| 48 | /** |
||
| 49 | * @param \Illuminate\Routing\Route $route |
||
| 50 | * @param array $rulesToApply Rules to apply when generating documentation for this route |
||
| 51 | * |
||
| 52 | * @return array |
||
| 53 | */ |
||
| 54 | public function processRoute(Route $route, array $rulesToApply = []) |
||
| 55 | { |
||
| 56 | list($class, $method) = Utils::getRouteActionUses($route->getAction()); |
||
| 57 | $controller = new ReflectionClass($class); |
||
| 58 | $method = $controller->getMethod($method); |
||
| 59 | |||
| 60 | list($routeGroupName, $routeGroupDescription) = $this->getRouteGroup($controller, $method); |
||
| 61 | |||
| 62 | $docBlock = $this->parseDocBlock($method); |
||
| 63 | $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']); |
||
| 64 | $queryParameters = $this->getQueryParameters($method, $docBlock['tags']); |
||
| 65 | $content = ResponseResolver::getResponse($route, $docBlock['tags'], [ |
||
| 66 | 'rules' => $rulesToApply, |
||
| 67 | 'body' => $bodyParameters, |
||
| 68 | 'query' => $queryParameters, |
||
| 69 | ]); |
||
| 70 | |||
| 71 | $parsedRoute = [ |
||
| 72 | 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), |
||
| 73 | 'groupName' => $routeGroupName, |
||
| 74 | 'groupDescription' => $routeGroupDescription, |
||
| 75 | 'title' => $docBlock['short'], |
||
| 76 | 'description' => $docBlock['long'], |
||
| 77 | 'methods' => $this->getMethods($route), |
||
| 78 | 'uri' => $this->getUri($route), |
||
| 79 | 'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])), |
||
| 80 | 'queryParameters' => $queryParameters, |
||
| 81 | 'bodyParameters' => $bodyParameters, |
||
| 82 | 'cleanBodyParameters' => $this->cleanParams($bodyParameters), |
||
| 83 | 'cleanQueryParameters' => $this->cleanParams($queryParameters), |
||
| 84 | 'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']), |
||
| 85 | 'response' => $content, |
||
| 86 | 'showresponse' => ! empty($content), |
||
| 87 | ]; |
||
| 88 | $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; |
||
| 89 | |||
| 90 | return $parsedRoute; |
||
| 91 | } |
||
| 92 | |||
| 93 | View Code Duplication | protected function getBodyParameters(ReflectionMethod $method, array $tags) |
|
|
|
|||
| 94 | { |
||
| 95 | foreach ($method->getParameters() as $param) { |
||
| 96 | $paramType = $param->getType(); |
||
| 97 | if ($paramType === null) { |
||
| 98 | continue; |
||
| 99 | } |
||
| 100 | |||
| 101 | $parameterClassName = version_compare(phpversion(), '7.1.0', '<') |
||
| 102 | ? $paramType->__toString() |
||
| 103 | : $paramType->getName(); |
||
| 104 | |||
| 105 | try { |
||
| 106 | $parameterClass = new ReflectionClass($parameterClassName); |
||
| 107 | } catch (\ReflectionException $e) { |
||
| 108 | continue; |
||
| 109 | } |
||
| 110 | |||
| 111 | if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { |
||
| 112 | $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); |
||
| 113 | $bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags()); |
||
| 114 | |||
| 115 | if (count($bodyParametersFromDocBlock)) { |
||
| 116 | return $bodyParametersFromDocBlock; |
||
| 117 | } |
||
| 118 | } |
||
| 119 | } |
||
| 120 | |||
| 121 | return $this->getBodyParametersFromDocBlock($tags); |
||
| 122 | } |
||
| 123 | |||
| 124 | /** |
||
| 125 | * @param array $tags |
||
| 126 | * |
||
| 127 | * @return array |
||
| 128 | */ |
||
| 129 | protected function getBodyParametersFromDocBlock(array $tags) |
||
| 130 | { |
||
| 131 | $parameters = collect($tags) |
||
| 132 | ->filter(function ($tag) { |
||
| 133 | return $tag instanceof Tag && $tag->getName() === 'bodyParam'; |
||
| 134 | }) |
||
| 135 | ->mapWithKeys(function ($tag) { |
||
| 136 | preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); |
||
| 137 | $content = preg_replace('/\s?No-example.?/', '', $content); |
||
| 138 | View Code Duplication | if (empty($content)) { |
|
| 139 | // this means only name and type were supplied |
||
| 140 | list($name, $type) = preg_split('/\s+/', $tag->getContent()); |
||
| 141 | $required = false; |
||
| 142 | $description = ''; |
||
| 143 | } else { |
||
| 144 | list($_, $name, $type, $required, $description) = $content; |
||
| 145 | $description = trim($description); |
||
| 146 | if ($description == 'required' && empty(trim($required))) { |
||
| 147 | $required = $description; |
||
| 148 | $description = ''; |
||
| 149 | } |
||
| 150 | $required = trim($required) == 'required' ? true : false; |
||
| 151 | } |
||
| 152 | |||
| 153 | $type = $this->normalizeParameterType($type); |
||
| 154 | list($description, $example) = $this->parseDescription($description, $type); |
||
| 155 | $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; |
||
| 156 | |||
| 157 | return [$name => compact('type', 'description', 'required', 'value')]; |
||
| 158 | })->toArray(); |
||
| 159 | |||
| 160 | return $parameters; |
||
| 161 | } |
||
| 162 | |||
| 163 | /** |
||
| 164 | * @param ReflectionMethod $method |
||
| 165 | * @param array $tags |
||
| 166 | * |
||
| 167 | * @return array |
||
| 168 | */ |
||
| 169 | View Code Duplication | protected function getQueryParameters(ReflectionMethod $method, array $tags) |
|
| 170 | { |
||
| 171 | foreach ($method->getParameters() as $param) { |
||
| 172 | $paramType = $param->getType(); |
||
| 173 | if ($paramType === null) { |
||
| 174 | continue; |
||
| 175 | } |
||
| 176 | |||
| 177 | $parameterClassName = version_compare(phpversion(), '7.1.0', '<') |
||
| 178 | ? $paramType->__toString() |
||
| 179 | : $paramType->getName(); |
||
| 180 | |||
| 181 | try { |
||
| 182 | $parameterClass = new ReflectionClass($parameterClassName); |
||
| 183 | } catch (\ReflectionException $e) { |
||
| 184 | continue; |
||
| 185 | } |
||
| 186 | |||
| 187 | if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { |
||
| 188 | $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); |
||
| 189 | $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags()); |
||
| 190 | |||
| 191 | if (count($queryParametersFromDocBlock)) { |
||
| 192 | return $queryParametersFromDocBlock; |
||
| 193 | } |
||
| 194 | } |
||
| 195 | } |
||
| 196 | |||
| 197 | return $this->getQueryParametersFromDocBlock($tags); |
||
| 198 | } |
||
| 199 | |||
| 200 | /** |
||
| 201 | * @param array $tags |
||
| 202 | * |
||
| 203 | * @return array |
||
| 204 | */ |
||
| 205 | protected function getQueryParametersFromDocBlock(array $tags) |
||
| 206 | { |
||
| 207 | $parameters = collect($tags) |
||
| 208 | ->filter(function ($tag) { |
||
| 209 | return $tag instanceof Tag && $tag->getName() === 'queryParam'; |
||
| 210 | }) |
||
| 211 | ->mapWithKeys(function ($tag) { |
||
| 212 | preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); |
||
| 213 | $content = preg_replace('/\s?No-example.?/', '', $content); |
||
| 214 | View Code Duplication | if (empty($content)) { |
|
| 215 | // this means only name was supplied |
||
| 216 | list($name) = preg_split('/\s+/', $tag->getContent()); |
||
| 217 | $required = false; |
||
| 218 | $description = ''; |
||
| 219 | } else { |
||
| 220 | list($_, $name, $required, $description) = $content; |
||
| 221 | $description = trim($description); |
||
| 222 | if ($description == 'required' && empty(trim($required))) { |
||
| 223 | $required = $description; |
||
| 224 | $description = ''; |
||
| 225 | } |
||
| 226 | $required = trim($required) == 'required' ? true : false; |
||
| 227 | } |
||
| 228 | |||
| 229 | list($description, $value) = $this->parseDescription($description, 'string'); |
||
| 230 | if (is_null($value) && ! $this->shouldExcludeExample($tag)) { |
||
| 231 | $value = str_contains($description, ['number', 'count', 'page']) |
||
| 232 | ? $this->generateDummyValue('integer') |
||
| 233 | : $this->generateDummyValue('string'); |
||
| 234 | } |
||
| 235 | |||
| 236 | return [$name => compact('description', 'required', 'value')]; |
||
| 237 | })->toArray(); |
||
| 238 | |||
| 239 | return $parameters; |
||
| 240 | } |
||
| 241 | |||
| 242 | /** |
||
| 243 | * @param array $tags |
||
| 244 | * |
||
| 245 | * @return bool |
||
| 246 | */ |
||
| 247 | protected function getAuthStatusFromDocBlock(array $tags) |
||
| 256 | |||
| 257 | /** |
||
| 258 | * @param ReflectionMethod $method |
||
| 259 | * |
||
| 260 | * @return array |
||
| 261 | */ |
||
| 262 | protected function parseDocBlock(ReflectionMethod $method) |
||
| 273 | |||
| 274 | /** |
||
| 275 | * @param ReflectionClass $controller |
||
| 276 | * @param ReflectionMethod $method |
||
| 277 | * |
||
| 278 | * @return array The route group name and description |
||
| 279 | */ |
||
| 280 | protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method) |
||
| 314 | |||
| 315 | private function normalizeParameterType($type) |
||
| 325 | |||
| 326 | private function generateDummyValue(string $type) |
||
| 360 | |||
| 361 | /** |
||
| 362 | * Allows users to specify an example for the parameter by writing 'Example: the-example', |
||
| 363 | * to be used in example requests and response calls. |
||
| 364 | * |
||
| 365 | * @param string $description |
||
| 366 | * @param string $type The type of the parameter. Used to cast the example provided, if any. |
||
| 367 | * |
||
| 368 | * @return array The description and included example. |
||
| 369 | */ |
||
| 370 | private function parseDescription(string $description, string $type) |
||
| 382 | |||
| 383 | /** |
||
| 384 | * Allows users to specify that we shouldn't generate an example for the parameter |
||
| 385 | * by writing 'No-example'. |
||
| 386 | * |
||
| 387 | * @param Tag $tag |
||
| 388 | * |
||
| 389 | * @return bool Whether no example should be generated |
||
| 390 | */ |
||
| 391 | private function shouldExcludeExample(Tag $tag) |
||
| 395 | |||
| 396 | /** |
||
| 397 | * Cast a value from a string to a specified type. |
||
| 398 | * |
||
| 399 | * @param string $value |
||
| 400 | * @param string $type |
||
| 401 | * |
||
| 402 | * @return mixed |
||
| 403 | */ |
||
| 404 | private function castToType(string $value, string $type) |
||
| 425 | } |
||
| 426 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.