Complex classes like PhpDocController 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 PhpDocController, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
23 | class PhpDocController extends Controller |
||
24 | { |
||
25 | /** |
||
26 | * {@inheritdoc} |
||
27 | */ |
||
28 | public $defaultAction = 'property'; |
||
29 | /** |
||
30 | * @var bool whether to update class docs directly. Setting this to false will just output docs |
||
31 | * for copy and paste. |
||
32 | */ |
||
33 | public $updateFiles = true; |
||
34 | /** |
||
35 | * @var bool whether to add copyright header to php files. This should be skipped in application code. |
||
36 | */ |
||
37 | public $skipFrameworkRequirements = false; |
||
38 | |||
39 | |||
40 | /** |
||
41 | * Generates `@property` annotations in class files from getters and setters. |
||
42 | * |
||
43 | * Property description will be taken from getter or setter or from an `@property` annotation |
||
44 | * in the getters docblock if there is one defined. |
||
45 | * |
||
46 | * See https://github.com/yiisoft/yii2/wiki/Core-framework-code-style#documentation for details. |
||
47 | * |
||
48 | * @param string $root the directory to parse files from. Defaults to YII2_PATH. |
||
49 | */ |
||
50 | public function actionProperty($root = null) |
||
51 | { |
||
52 | $files = $this->findFiles($root); |
||
53 | |||
54 | $nFilesTotal = 0; |
||
55 | $nFilesUpdated = 0; |
||
56 | foreach ($files as $file) { |
||
57 | $result = $this->generateClassPropertyDocs($file); |
||
58 | if ($result !== false) { |
||
59 | list($className, $phpdoc) = $result; |
||
60 | if ($this->updateFiles) { |
||
61 | if ($this->updateClassPropertyDocs($file, $className, $phpdoc)) { |
||
62 | $nFilesUpdated++; |
||
63 | } |
||
64 | } elseif (!empty($phpdoc)) { |
||
65 | $this->stdout("\n[ " . $file . " ]\n\n", Console::BOLD); |
||
66 | $this->stdout($phpdoc); |
||
67 | } |
||
68 | } |
||
69 | $nFilesTotal++; |
||
70 | } |
||
71 | |||
72 | $this->stdout("\nParsed $nFilesTotal files.\n"); |
||
73 | $this->stdout("Updated $nFilesUpdated files.\n"); |
||
74 | } |
||
75 | |||
76 | /** |
||
77 | * Fix some issues with PHPDoc in files. |
||
78 | * |
||
79 | * @param string $root the directory to parse files from. Defaults to YII2_PATH. |
||
80 | */ |
||
81 | public function actionFix($root = null) |
||
82 | { |
||
83 | $files = $this->findFiles($root, false); |
||
84 | |||
85 | $nFilesTotal = 0; |
||
86 | $nFilesUpdated = 0; |
||
87 | foreach ($files as $file) { |
||
88 | $contents = file_get_contents($file); |
||
89 | $hash = $this->hash($contents); |
||
90 | |||
91 | // fix line endings |
||
92 | $lines = preg_split('/(\r\n|\n|\r)/', $contents); |
||
93 | |||
94 | if (!$this->skipFrameworkRequirements) { |
||
95 | $this->fixFileDoc($lines); |
||
96 | } |
||
97 | $this->fixDocBlockIndentation($lines); |
||
98 | $lines = array_values($this->fixLineSpacing($lines)); |
||
99 | |||
100 | $newContent = implode("\n", $lines); |
||
101 | if ($hash !== $this->hash($newContent)) { |
||
102 | file_put_contents($file, $newContent); |
||
103 | $nFilesUpdated++; |
||
104 | } |
||
105 | $nFilesTotal++; |
||
106 | } |
||
107 | |||
108 | $this->stdout("\nParsed $nFilesTotal files.\n"); |
||
109 | $this->stdout("Updated $nFilesUpdated files.\n"); |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * {@inheritdoc} |
||
114 | */ |
||
115 | public function options($actionID) |
||
116 | { |
||
117 | return array_merge(parent::options($actionID), ['updateFiles', 'skipFrameworkRequirements']); |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * @param string $root |
||
122 | * @param bool $needsInclude |
||
123 | * @return array list of files. |
||
124 | */ |
||
125 | protected function findFiles($root, $needsInclude = true) |
||
126 | { |
||
127 | $except = []; |
||
128 | if ($needsInclude) { |
||
129 | $extensionExcept = [ |
||
130 | 'apidoc' => [ |
||
131 | '/helpers/PrettyPrinter.php', |
||
132 | '/extensions/apidoc/helpers/ApiIndexer.php', |
||
133 | '/extensions/apidoc/helpers/ApiMarkdownLaTeX.php', |
||
134 | ], |
||
135 | 'codeception' => [ |
||
136 | '/TestCase.php', |
||
137 | '/DbTestCase.php', |
||
138 | ], |
||
139 | 'gii' => [ |
||
140 | '/components/DiffRendererHtmlInline.php', |
||
141 | '/generators/extension/default/AutoloadExample.php', |
||
142 | ], |
||
143 | 'swiftmailer' => [ |
||
144 | 'src/Logger.php', |
||
145 | ], |
||
146 | 'twig' => [ |
||
147 | '/Extension.php', |
||
148 | '/Optimizer.php', |
||
149 | '/Template.php', |
||
150 | '/TwigSimpleFileLoader.php', |
||
151 | '/ViewRendererStaticClassProxy.php', |
||
152 | ], |
||
153 | ]; |
||
154 | } else { |
||
155 | $extensionExcept = []; |
||
156 | } |
||
157 | |||
158 | if ($root === null) { |
||
159 | $root = \dirname(YII2_PATH); |
||
160 | $extensionPath = "$root/extensions"; |
||
161 | $this->setUpExtensionAliases($extensionPath); |
||
162 | |||
163 | $except = [ |
||
164 | '/apps/', |
||
165 | '/build/', |
||
166 | '/docs/', |
||
167 | '/extensions/composer/', |
||
168 | '/framework/BaseYii.php', |
||
169 | '/framework/Yii.php', |
||
170 | 'assets/', |
||
171 | 'tests/', |
||
172 | 'vendor/', |
||
173 | ]; |
||
174 | foreach ($extensionExcept as $ext => $paths) { |
||
175 | foreach ($paths as $path) { |
||
176 | $except[] = "/extensions/$ext$path"; |
||
177 | } |
||
178 | } |
||
179 | } elseif (preg_match('~extensions/([\w-]+)[\\\\/]?$~', $root, $matches)) { |
||
180 | $extensionPath = \dirname(rtrim($root, '\\/')); |
||
181 | $this->setUpExtensionAliases($extensionPath); |
||
182 | |||
183 | list(, $extension) = $matches; |
||
184 | Yii::setAlias("@yii/$extension", (string)$root); |
||
185 | if (is_file($autoloadFile = Yii::getAlias("@yii/$extension/vendor/autoload.php"))) { |
||
186 | include $autoloadFile; |
||
187 | } |
||
188 | |||
189 | if (isset($extensionExcept[$extension])) { |
||
190 | foreach ($extensionExcept[$extension] as $path) { |
||
191 | $except[] = $path; |
||
192 | } |
||
193 | } |
||
194 | $except[] = '/vendor/'; |
||
195 | $except[] = '/tests/'; |
||
196 | $except[] = '/docs/'; |
||
197 | |||
198 | // // composer extension does not contain yii code |
||
199 | // if ($extension === 'composer') { |
||
200 | // return []; |
||
201 | // } |
||
202 | } elseif (preg_match('~apps/([\w-]+)[\\\\/]?$~', $root, $matches)) { |
||
203 | $extensionPath = \dirname(\dirname(rtrim($root, '\\/'))) . '/extensions'; |
||
204 | $this->setUpExtensionAliases($extensionPath); |
||
205 | |||
206 | list(, $appName) = $matches; |
||
207 | Yii::setAlias("@app-$appName", (string)$root); |
||
208 | if (is_file($autoloadFile = Yii::getAlias("@app-$appName/vendor/autoload.php"))) { |
||
209 | include $autoloadFile; |
||
210 | } |
||
211 | |||
212 | $except[] = '/runtime/'; |
||
213 | $except[] = '/vendor/'; |
||
214 | $except[] = '/tests/'; |
||
215 | $except[] = '/docs/'; |
||
216 | } |
||
217 | $root = FileHelper::normalizePath($root); |
||
218 | $options = [ |
||
219 | 'filter' => function ($path) { |
||
220 | if (is_file($path)) { |
||
221 | $file = basename($path); |
||
222 | if ($file[0] < 'A' || $file[0] > 'Z') { |
||
223 | return false; |
||
224 | } |
||
225 | } |
||
226 | |||
227 | return null; |
||
228 | }, |
||
229 | 'only' => ['*.php'], |
||
230 | 'except' => array_merge($except, [ |
||
231 | '.git/', |
||
232 | 'views/', |
||
233 | 'requirements/', |
||
234 | 'gii/generators/', |
||
235 | 'vendor/', |
||
236 | ]), |
||
237 | ]; |
||
238 | |||
239 | return FileHelper::findFiles($root, $options); |
||
240 | } |
||
241 | |||
242 | /** |
||
243 | * @param string $extensionPath root path containing extension repositories. |
||
244 | */ |
||
245 | private function setUpExtensionAliases($extensionPath) |
||
246 | { |
||
247 | foreach (scandir($extensionPath) as $extension) { |
||
248 | if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) { |
||
249 | Yii::setAlias("@yii/$extension", "$extensionPath/$extension"); |
||
250 | |||
251 | $composerConfigFile = $extensionPath . '/' . $extension . '/composer.json'; |
||
252 | if (file_exists($composerConfigFile)) { |
||
253 | $composerConfig = Json::decode(file_get_contents($composerConfigFile)); |
||
254 | if (isset($composerConfig['autoload']['psr-4'])) { |
||
255 | foreach ($composerConfig['autoload']['psr-4'] as $namespace => $subPath) { |
||
256 | $alias = '@' . str_replace('\\', '/', $namespace); |
||
257 | $path = rtrim("$extensionPath/$extension/$subPath", '/'); |
||
258 | Yii::setAlias($alias, $path); |
||
259 | } |
||
260 | } |
||
261 | } |
||
262 | } |
||
263 | } |
||
264 | } |
||
265 | |||
266 | /** |
||
267 | * Fix file PHPDoc. |
||
268 | */ |
||
269 | protected function fixFileDoc(&$lines) |
||
270 | { |
||
271 | // find namespace |
||
272 | $namespace = false; |
||
273 | $namespaceLine = ''; |
||
274 | $contentAfterNamespace = false; |
||
275 | foreach ($lines as $i => $line) { |
||
276 | $line = trim($line); |
||
277 | if (!empty($line)) { |
||
278 | if (strncmp($line, 'namespace', 9) === 0) { |
||
279 | $namespace = $i; |
||
280 | $namespaceLine = $line; |
||
281 | } elseif ($namespace !== false) { |
||
282 | $contentAfterNamespace = $i; |
||
283 | break; |
||
284 | } |
||
285 | } |
||
286 | } |
||
287 | |||
288 | if ($namespace !== false && $contentAfterNamespace !== false) { |
||
289 | while ($contentAfterNamespace > 0) { |
||
290 | array_shift($lines); |
||
291 | $contentAfterNamespace--; |
||
292 | } |
||
293 | $lines = array_merge([ |
||
294 | '<?php', |
||
295 | '/**', |
||
296 | ' * @link http://www.yiiframework.com/', |
||
297 | ' * @copyright Copyright (c) 2008 Yii Software LLC', |
||
298 | ' * @license http://www.yiiframework.com/license/', |
||
299 | ' */', |
||
300 | '', |
||
301 | $namespaceLine, |
||
302 | '', |
||
303 | ], $lines); |
||
304 | } |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Markdown aware fix of whitespace issues in doc comments. |
||
309 | * @param array $lines |
||
310 | */ |
||
311 | protected function fixDocBlockIndentation(&$lines) |
||
312 | { |
||
313 | $docBlock = false; |
||
314 | $codeBlock = false; |
||
315 | $listIndent = ''; |
||
316 | $tag = false; |
||
317 | $indent = ''; |
||
318 | foreach ($lines as $i => $line) { |
||
319 | if (preg_match('~^(\s*)/\*\*$~', $line, $matches)) { |
||
320 | $docBlock = true; |
||
321 | $indent = $matches[1]; |
||
322 | } elseif (preg_match('~^(\s*)\*+/~', $line)) { |
||
323 | if ($docBlock) { // could be the end of normal comment |
||
324 | $lines[$i] = $indent . ' */'; |
||
325 | } |
||
326 | $docBlock = false; |
||
327 | $codeBlock = false; |
||
328 | $listIndent = ''; |
||
329 | $tag = false; |
||
330 | } elseif ($docBlock) { |
||
331 | $line = ltrim($line); |
||
332 | if (isset($line[0]) && $line[0] === '*') { |
||
333 | $line = substr($line, 1); |
||
334 | } |
||
335 | if (isset($line[0]) && $line[0] === ' ') { |
||
336 | $line = substr($line, 1); |
||
337 | } |
||
338 | $docLine = str_replace("\t", ' ', rtrim($line)); |
||
339 | if (empty($docLine)) { |
||
340 | $listIndent = ''; |
||
341 | } elseif ($docLine[0] === '@') { |
||
342 | $listIndent = ''; |
||
343 | $codeBlock = false; |
||
344 | $tag = true; |
||
345 | $docLine = preg_replace('/\s+/', ' ', $docLine); |
||
346 | $docLine = $this->fixParamTypes($docLine); |
||
347 | } elseif (preg_match('/^(~~~|```)/', $docLine)) { |
||
348 | $codeBlock = !$codeBlock; |
||
349 | $listIndent = ''; |
||
350 | } elseif (preg_match('/^(\s*)([0-9]+\.|-|\*|\+) /', $docLine, $matches)) { |
||
351 | $listIndent = str_repeat(' ', \strlen($matches[0])); |
||
352 | $tag = false; |
||
353 | $lines[$i] = $indent . ' * ' . $docLine; |
||
354 | continue; |
||
355 | } |
||
356 | if ($codeBlock) { |
||
357 | $lines[$i] = rtrim($indent . ' * ' . $docLine); |
||
358 | } else { |
||
359 | $lines[$i] = rtrim($indent . ' * ' . (empty($listIndent) && !$tag ? $docLine : ($listIndent . ltrim($docLine)))); |
||
360 | } |
||
361 | } |
||
362 | } |
||
363 | } |
||
364 | |||
365 | /** |
||
366 | * @param string $line |
||
367 | * @return string |
||
368 | */ |
||
369 | protected function fixParamTypes($line) |
||
370 | { |
||
371 | return preg_replace_callback('~@(param|return) ([\w\\|]+)~i', function ($matches) { |
||
372 | $types = explode('|', $matches[2]); |
||
373 | foreach ($types as $i => $type) { |
||
374 | switch ($type) { |
||
375 | case 'integer': $types[$i] = 'int'; break; |
||
376 | case 'boolean': $types[$i] = 'bool'; break; |
||
377 | } |
||
378 | } |
||
379 | |||
380 | return '@' . $matches[1] . ' ' . implode('|', $types); |
||
381 | }, $line); |
||
382 | } |
||
383 | |||
384 | /** |
||
385 | * Fixes line spacing code style for properties and constants. |
||
386 | * @param string[] $lines |
||
387 | * @return string[] |
||
388 | */ |
||
389 | protected function fixLineSpacing($lines) |
||
390 | { |
||
391 | $propertiesOnly = false; |
||
392 | // remove blank lines between properties |
||
393 | $skip = true; |
||
394 | $level = 0; |
||
395 | foreach ($lines as $i => $line) { |
||
396 | if (strpos($line, 'class ') !== false) { |
||
397 | $skip = false; |
||
398 | } |
||
399 | if ($skip) { |
||
400 | continue; |
||
401 | } |
||
402 | |||
403 | // keep spaces in multi line arrays |
||
404 | if (strpos($line, '*') === false && strncmp(trim($line), "'SQLSTATE[", 10) !== 0) { |
||
405 | $level += substr_count($line, '[') - substr_count($line, ']'); |
||
406 | } |
||
407 | |||
408 | if (trim($line) === '') { |
||
409 | if ($level == 0) { |
||
410 | unset($lines[$i]); |
||
411 | } |
||
412 | } elseif (ltrim($line)[0] !== '*' && strpos($line, 'function ') !== false) { |
||
413 | break; |
||
414 | } elseif (trim($line) === '}') { |
||
415 | $propertiesOnly = true; |
||
416 | break; |
||
417 | } |
||
418 | } |
||
419 | $lines = array_values($lines); |
||
420 | |||
421 | // add back some |
||
422 | $endofUse = false; |
||
423 | $endofConst = false; |
||
424 | $endofPublic = false; |
||
425 | $endofProtected = false; |
||
426 | $endofPrivate = false; |
||
427 | $skip = true; |
||
428 | $level = 0; // track array properties |
||
429 | $property = ''; |
||
430 | foreach ($lines as $i => $line) { |
||
431 | if (strpos($line, 'class ') !== false) { |
||
432 | $skip = false; |
||
433 | } |
||
434 | if ($skip) { |
||
435 | continue; |
||
436 | } |
||
437 | |||
438 | // check for multi line array |
||
439 | if ($level > 0) { |
||
440 | ${'endof' . $property} = $i; |
||
441 | } |
||
442 | |||
443 | $line = trim($line); |
||
444 | if (strncmp($line, 'public $', 8) === 0 || strncmp($line, 'public static $', 15) === 0) { |
||
445 | $endofPublic = $i; |
||
446 | $property = 'Public'; |
||
447 | $level = 0; |
||
448 | } elseif (strncmp($line, 'protected $', 11) === 0 || strncmp($line, 'protected static $', 18) === 0) { |
||
449 | $endofProtected = $i; |
||
450 | $property = 'Protected'; |
||
451 | $level = 0; |
||
452 | } elseif (strncmp($line, 'private $', 9) === 0 || strncmp($line, 'private static $', 16) === 0) { |
||
453 | $endofPrivate = $i; |
||
454 | $property = 'Private'; |
||
455 | $level = 0; |
||
456 | } elseif (substr($line, 0, 6) === 'const ') { |
||
457 | $endofConst = $i; |
||
458 | $property = false; |
||
459 | } elseif (substr($line, 0, 4) === 'use ') { |
||
460 | $endofUse = $i; |
||
461 | $property = false; |
||
462 | } elseif (!empty($line) && $line[0] === '*') { |
||
463 | $property = false; |
||
464 | } elseif (!empty($line) && $line[0] !== '*' && strpos($line, 'function ') !== false || $line === '}') { |
||
465 | break; |
||
466 | } |
||
467 | |||
468 | // check for multi line array |
||
469 | if ($property !== false && strncmp($line, "'SQLSTATE[", 10) !== 0) { |
||
470 | $level += substr_count($line, '[') - substr_count($line, ']'); |
||
471 | } |
||
472 | } |
||
473 | |||
474 | $endofAll = false; |
||
475 | foreach (['Private', 'Protected', 'Public', 'Const', 'Use'] as $var) { |
||
476 | if (${'endof' . $var} !== false) { |
||
477 | $endofAll = ${'endof' . $var}; |
||
478 | break; |
||
479 | } |
||
480 | } |
||
481 | |||
482 | // $this->checkPropertyOrder($lineInfo); |
||
483 | $result = []; |
||
484 | foreach ($lines as $i => $line) { |
||
485 | $result[] = $line; |
||
486 | if (!($propertiesOnly && $i === $endofAll)) { |
||
487 | if ($i === $endofUse || $i === $endofConst || $i === $endofPublic || |
||
488 | $i === $endofProtected || $i === $endofPrivate) { |
||
489 | $result[] = ''; |
||
490 | } |
||
491 | if ($i === $endofAll) { |
||
492 | $result[] = ''; |
||
493 | } |
||
494 | } |
||
495 | } |
||
496 | |||
497 | return $result; |
||
498 | } |
||
499 | |||
500 | protected function checkPropertyOrder($lineInfo) |
||
501 | { |
||
502 | // TODO |
||
503 | } |
||
504 | |||
505 | protected function updateClassPropertyDocs($file, $className, $propertyDoc) |
||
506 | { |
||
507 | try { |
||
508 | $ref = new \ReflectionClass($className); |
||
509 | } catch (\Exception $e) { |
||
510 | $this->stderr("[ERR] Unable to create ReflectionClass for class '$className': " . $e->getMessage() . "\n", Console::FG_RED); |
||
511 | return false; |
||
512 | } |
||
513 | if ($ref->getFileName() != $file) { |
||
514 | $this->stderr("[ERR] Unable to create ReflectionClass for class: $className loaded class is not from file: $file\n", Console::FG_RED); |
||
515 | return false; |
||
516 | } |
||
517 | |||
518 | if (!$ref->isSubclassOf('yii\base\Object') && $className != 'yii\base\Object' && !$ref->isSubclassOf('yii\base\BaseObject') && $className != 'yii\base\BaseObject') { |
||
519 | $this->stderr("[INFO] Skipping class $className as it is not a subclass of yii\\base\\BaseObject.\n", Console::FG_BLUE, Console::BOLD); |
||
520 | return false; |
||
521 | } |
||
522 | |||
523 | if ($ref->isSubclassOf('yii\db\BaseActiveRecord')) { |
||
524 | $this->stderr("[INFO] Skipping class $className as it is an ActiveRecord class, property handling is not supported yet.\n", Console::FG_BLUE, Console::BOLD); |
||
525 | return false; |
||
526 | } |
||
527 | |||
528 | $oldDoc = $ref->getDocComment(); |
||
529 | $newDoc = $this->cleanDocComment($this->updateDocComment($oldDoc, $propertyDoc)); |
||
530 | |||
531 | $seenSince = false; |
||
532 | $seenAuthor = false; |
||
533 | |||
534 | // TODO move these checks to different action |
||
535 | $lines = explode("\n", $newDoc); |
||
536 | $firstLine = trim($lines[1]); |
||
537 | if ($firstLine === '*' || strncmp($firstLine, '* @', 3) === 0) { |
||
538 | $this->stderr("[WARN] Class $className has no short description.\n", Console::FG_YELLOW, Console::BOLD); |
||
539 | } |
||
540 | foreach ($lines as $line) { |
||
541 | $line = trim($line); |
||
542 | if (strncmp($line, '* @since ', 9) === 0) { |
||
543 | $seenSince = true; |
||
544 | } elseif (strncmp($line, '* @author ', 10) === 0) { |
||
545 | $seenAuthor = true; |
||
546 | } |
||
547 | } |
||
548 | |||
549 | if (!$this->skipFrameworkRequirements) { |
||
550 | if (!$seenSince) { |
||
551 | $this->stderr("[ERR] No @since found in class doc in file: $file\n", Console::FG_RED); |
||
552 | } |
||
553 | if (!$seenAuthor) { |
||
554 | $this->stderr("[ERR] No @author found in class doc in file: $file\n", Console::FG_RED); |
||
555 | } |
||
556 | } |
||
557 | |||
558 | if (trim($oldDoc) != trim($newDoc)) { |
||
559 | $fileContent = explode("\n", file_get_contents($file)); |
||
560 | $start = $ref->getStartLine() - 2; |
||
561 | $docStart = $start - \count(explode("\n", $oldDoc)) + 1; |
||
562 | |||
563 | $newFileContent = []; |
||
564 | $n = \count($fileContent); |
||
565 | for ($i = 0; $i < $n; $i++) { |
||
566 | if ($i > $start || $i < $docStart) { |
||
567 | $newFileContent[] = $fileContent[$i]; |
||
568 | } else { |
||
569 | $newFileContent[] = trim($newDoc); |
||
570 | $i = $start; |
||
571 | } |
||
572 | } |
||
573 | |||
574 | file_put_contents($file, implode("\n", $newFileContent)); |
||
575 | |||
576 | return true; |
||
577 | } |
||
578 | |||
579 | return false; |
||
580 | } |
||
581 | |||
582 | /** |
||
583 | * remove multi empty lines and trim trailing whitespace. |
||
584 | * |
||
585 | * @param $doc |
||
586 | * @return string |
||
587 | */ |
||
588 | protected function cleanDocComment($doc) |
||
589 | { |
||
590 | $lines = explode("\n", $doc); |
||
591 | $n = \count($lines); |
||
592 | for ($i = 0; $i < $n; $i++) { |
||
593 | $lines[$i] = rtrim($lines[$i]); |
||
594 | if (trim($lines[$i]) == '*' && trim($lines[$i + 1]) == '*') { |
||
595 | unset($lines[$i]); |
||
596 | } |
||
597 | } |
||
598 | |||
599 | return implode("\n", $lines); |
||
600 | } |
||
601 | |||
602 | /** |
||
603 | * Replace property annotations in doc comment. |
||
604 | * @param $doc |
||
605 | * @param $properties |
||
606 | * @return string |
||
607 | */ |
||
608 | protected function updateDocComment($doc, $properties) |
||
609 | { |
||
610 | $lines = explode("\n", $doc); |
||
611 | $propertyPart = false; |
||
612 | $propertyPosition = false; |
||
613 | foreach ($lines as $i => $line) { |
||
614 | $line = trim($line); |
||
615 | if (strncmp($line, '* @property ', 12) === 0) { |
||
616 | $propertyPart = true; |
||
617 | } elseif ($propertyPart && $line == '*') { |
||
618 | $propertyPosition = $i; |
||
619 | $propertyPart = false; |
||
620 | } |
||
621 | if (strncmp($line, '* @author ', 10) === 0 && $propertyPosition === false) { |
||
622 | $propertyPosition = $i - 1; |
||
623 | $propertyPart = false; |
||
624 | } |
||
625 | if ($propertyPart) { |
||
626 | unset($lines[$i]); |
||
627 | } |
||
628 | } |
||
629 | |||
630 | // if no properties or other tags where present add properties at the end |
||
631 | if ($propertyPosition === false) { |
||
632 | $propertyPosition = \count($lines) - 2; |
||
633 | } |
||
634 | |||
635 | $finalDoc = ''; |
||
636 | foreach ($lines as $i => $line) { |
||
637 | $finalDoc .= $line . "\n"; |
||
638 | if ($i == $propertyPosition) { |
||
639 | $finalDoc .= $properties; |
||
640 | } |
||
641 | } |
||
642 | |||
643 | return $finalDoc; |
||
644 | } |
||
645 | |||
646 | protected function generateClassPropertyDocs($fileName) |
||
647 | { |
||
648 | $phpdoc = ''; |
||
649 | $file = str_replace("\r", '', str_replace("\t", ' ', file_get_contents($fileName, true))); |
||
650 | $ns = $this->match('#\nnamespace (?<name>[\w\\\\]+);\n#', $file); |
||
651 | $namespace = reset($ns); |
||
652 | $namespace = $namespace['name']; |
||
653 | $classes = $this->match('#\n(?:abstract )(?:final )?class (?<name>\w+)( extends .+)?( implements .+)?\n\{(?<content>.*)\n\}(\n|$)#', $file); |
||
654 | |||
655 | if (\count($classes) > 1) { |
||
656 | $this->stderr("[ERR] There should be only one class in a file: $fileName\n", Console::FG_RED); |
||
657 | |||
658 | return false; |
||
659 | } |
||
660 | if (\count($classes) < 1) { |
||
661 | $interfaces = $this->match('#\ninterface (?<name>\w+)( extends .+)?\n\{(?<content>.*)\n\}(\n|$)#', $file); |
||
662 | if (\count($interfaces) == 1) { |
||
663 | return false; |
||
664 | } elseif (\count($interfaces) > 1) { |
||
665 | $this->stderr("[ERR] There should be only one interface in a file: $fileName\n", Console::FG_RED); |
||
666 | } else { |
||
667 | $traits = $this->match('#\ntrait (?<name>\w+)\n\{(?<content>.*)\n\}(\n|$)#', $file); |
||
668 | if (\count($traits) == 1) { |
||
669 | return false; |
||
670 | } elseif (\count($traits) > 1) { |
||
671 | $this->stderr("[ERR] There should be only one class/trait/interface in a file: $fileName\n", Console::FG_RED); |
||
672 | } else { |
||
673 | $this->stderr("[ERR] No class in file: $fileName\n", Console::FG_RED); |
||
674 | } |
||
675 | } |
||
676 | |||
677 | return false; |
||
678 | } |
||
679 | |||
680 | $className = null; |
||
681 | foreach ($classes as &$class) { |
||
682 | $className = $namespace . '\\' . $class['name']; |
||
683 | |||
684 | $gets = $this->match( |
||
685 | '#\* @return (?<type>[\w\\|\\\\\\[\\]]+)(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' . |
||
686 | '[\s\n]{2,}public function (?<kind>get)(?<name>\w+)\((?:,? ?\$\w+ ?= ?[^,]+)*\)#', |
||
687 | $class['content'], true); |
||
688 | $sets = $this->match( |
||
689 | '#\* @param (?<type>[\w\\|\\\\\\[\\]]+) \$\w+(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' . |
||
690 | '[\s\n]{2,}public function (?<kind>set)(?<name>\w+)\(\$\w+(?:, ?\$\w+ ?= ?[^,]+)*\)#', |
||
691 | $class['content'], true); |
||
692 | // check for @property annotations in getter and setter |
||
693 | $properties = $this->match( |
||
694 | '#\* @(?<kind>property) (?<type>[\w\\|\\\\\\[\\]]+)(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' . |
||
695 | '[\s\n]{2,}public function [g|s]et(?<name>\w+)\(((?:,? ?\$\w+ ?= ?[^,]+)*|\$\w+(?:, ?\$\w+ ?= ?[^,]+)*)\)#', |
||
696 | $class['content']); |
||
697 | $acrs = array_merge($properties, $gets, $sets); |
||
698 | |||
699 | $props = []; |
||
700 | foreach ($acrs as &$acr) { |
||
701 | $acr['name'] = lcfirst($acr['name']); |
||
702 | $acr['comment'] = trim(preg_replace('#(^|\n)\s+\*\s?#', '$1 * ', $acr['comment'])); |
||
703 | $props[$acr['name']][$acr['kind']] = [ |
||
704 | 'type' => $acr['type'], |
||
705 | 'comment' => $this->fixSentence($acr['comment']), |
||
706 | ]; |
||
707 | } |
||
708 | |||
709 | ksort($props); |
||
710 | |||
711 | if (\count($props) > 0) { |
||
712 | $phpdoc .= " *\n"; |
||
713 | foreach ($props as $propName => &$prop) { |
||
714 | $docline = ' * @'; |
||
715 | $docline .= 'property'; // Do not use property-read and property-write as few IDEs support complex syntax. |
||
716 | $note = ''; |
||
717 | if (isset($prop['get'], $prop['set'])) { |
||
718 | if ($prop['get']['type'] != $prop['set']['type']) { |
||
719 | $note = ' Note that the type of this property differs in getter and setter.' |
||
720 | . ' See [[get' . ucfirst($propName) . '()]] and [[set' . ucfirst($propName) . '()]] for details.'; |
||
721 | } |
||
722 | } elseif (isset($prop['get'])) { |
||
723 | // check if parent class has setter defined |
||
724 | $c = $className; |
||
725 | $parentSetter = false; |
||
726 | while ($parent = get_parent_class($c)) { |
||
727 | if (method_exists($parent, 'set' . ucfirst($propName))) { |
||
728 | $parentSetter = true; |
||
729 | break; |
||
730 | } |
||
731 | $c = $parent; |
||
732 | } |
||
733 | if (!$parentSetter) { |
||
734 | $note = ' This property is read-only.'; |
||
735 | //$docline .= '-read'; |
||
736 | } |
||
737 | } elseif (isset($prop['set'])) { |
||
738 | // check if parent class has getter defined |
||
739 | $c = $className; |
||
740 | $parentGetter = false; |
||
741 | while ($parent = get_parent_class($c)) { |
||
742 | if (method_exists($parent, 'set' . ucfirst($propName))) { |
||
743 | $parentGetter = true; |
||
744 | break; |
||
745 | } |
||
746 | $c = $parent; |
||
747 | } |
||
748 | if (!$parentGetter) { |
||
749 | $note = ' This property is write-only.'; |
||
750 | //$docline .= '-write'; |
||
751 | } |
||
752 | } else { |
||
753 | continue; |
||
754 | } |
||
755 | $docline .= ' ' . $this->getPropParam($prop, 'type') . " $$propName "; |
||
756 | $comment = explode("\n", $this->getPropParam($prop, 'comment') . $note); |
||
757 | foreach ($comment as &$cline) { |
||
758 | $cline = ltrim($cline, '* '); |
||
759 | } |
||
760 | $docline = wordwrap($docline . implode(' ', $comment), 110, "\n * ") . "\n"; |
||
761 | |||
762 | $phpdoc .= $docline; |
||
763 | } |
||
764 | $phpdoc .= " *\n"; |
||
765 | } |
||
766 | } |
||
767 | |||
768 | return [$className, $phpdoc]; |
||
769 | } |
||
770 | |||
771 | protected function match($pattern, $subject, $split = false) |
||
772 | { |
||
773 | $sets = []; |
||
774 | // split subject by double newlines because regex sometimes has problems with matching |
||
775 | // in the complete set of methods |
||
776 | // example: yii\di\ServiceLocator setComponents() is not recognized in the whole but in |
||
777 | // a part of the class. |
||
778 | $parts = $split ? explode("\n\n", $subject) : [$subject]; |
||
779 | foreach ($parts as $part) { |
||
780 | preg_match_all($pattern . 'suU', $part, $matches, PREG_SET_ORDER); |
||
781 | foreach ($matches as &$set) { |
||
782 | foreach ($set as $i => $match) { |
||
783 | if (is_numeric($i) /*&& $i != 0*/) { |
||
784 | unset($set[$i]); |
||
785 | } |
||
786 | } |
||
787 | |||
788 | $sets[] = $set; |
||
789 | } |
||
790 | } |
||
791 | |||
792 | return $sets; |
||
793 | } |
||
794 | |||
795 | protected function fixSentence($str) |
||
796 | { |
||
797 | // TODO fix word wrap |
||
798 | if ($str == '') { |
||
799 | return ''; |
||
800 | } |
||
801 | |||
802 | return strtoupper(substr($str, 0, 1)) . substr($str, 1) . ($str[\strlen($str) - 1] != '.' ? '.' : ''); |
||
803 | } |
||
804 | |||
805 | protected function getPropParam($prop, $param) |
||
806 | { |
||
807 | return isset($prop['property']) ? $prop['property'][$param] : (isset($prop['get']) ? $prop['get'][$param] : $prop['set'][$param]); |
||
808 | } |
||
809 | |||
810 | /** |
||
811 | * Generate a hash value (message digest) |
||
812 | * @param string $string message to be hashed. |
||
813 | * @return string calculated message digest. |
||
814 | */ |
||
815 | private function hash($string) |
||
816 | { |
||
817 | if (!function_exists('hash')) { |
||
818 | return sha1($string); |
||
819 | } |
||
820 | return hash('sha256', $string); |
||
821 | } |
||
822 | } |
||
823 |