| Total Complexity | 44 |
| Total Lines | 385 |
| Duplicated Lines | 0 % |
| Changes | 13 | ||
| Bugs | 4 | Features | 2 |
Complex classes like ViewReview 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.
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 ViewReview, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 86 | class ViewReview extends ViewAbstract |
||
| 87 | { |
||
| 88 | /** |
||
| 89 | * Highlight mapping. |
||
| 90 | * |
||
| 91 | * @var array |
||
| 92 | */ |
||
| 93 | protected $phpHighlightColorMap; |
||
| 94 | |||
| 95 | /** |
||
| 96 | * Suffixes for php files. |
||
| 97 | * |
||
| 98 | * @var array |
||
| 99 | */ |
||
| 100 | protected $phpSuffixes; |
||
| 101 | |||
| 102 | /** |
||
| 103 | * Default constructor |
||
| 104 | * |
||
| 105 | * Highlighting strings are set. |
||
| 106 | * |
||
| 107 | * @param string $templateDir The directory containing the templates. |
||
| 108 | * @param string $outputDir The directory where the reviews should be. |
||
| 109 | * @param IOHelper $ioHelper The IOHelper object to use for I/O. |
||
| 110 | * @param array $phpSuffixes The array with extensions of php files. |
||
| 111 | */ |
||
| 112 | public function __construct(string $templateDir, string $outputDir, IOHelper $ioHelper, array $phpSuffixes = ['php']) |
||
| 113 | { |
||
| 114 | parent::__construct($templateDir, $outputDir, $ioHelper); |
||
| 115 | |||
| 116 | $this->phpHighlightColorMap = [ |
||
| 117 | \ini_get('highlight.string') => 'string', |
||
| 118 | \ini_get('highlight.comment') => 'comment', |
||
| 119 | \ini_get('highlight.keyword') => 'keyword', |
||
| 120 | \ini_get('highlight.default') => 'default', |
||
| 121 | \ini_get('highlight.html') => 'html', |
||
| 122 | ]; |
||
| 123 | |||
| 124 | $this->phpSuffixes = $phpSuffixes; |
||
| 125 | } |
||
| 126 | |||
| 127 | /** |
||
| 128 | * Generating the Html code browser view for a given file. |
||
| 129 | * |
||
| 130 | * Issue list for each file will be marked in source code. |
||
| 131 | * Source code is highlighted. |
||
| 132 | * Generated Html source code is be saved as Html. |
||
| 133 | * |
||
| 134 | * @param array $issueList The issue list for given file |
||
| 135 | * @param string $fileName |
||
| 136 | * @param string $commonPathPrefix The prefix path all given files have |
||
| 137 | * in common |
||
| 138 | * @param bool $excludeOK |
||
| 139 | * |
||
| 140 | * @return void |
||
| 141 | * |
||
| 142 | * @see self::_formatIssues |
||
| 143 | * @see self::_formatSourceCode |
||
| 144 | * @see self::_generateJSCode |
||
| 145 | */ |
||
| 146 | public function generate(array $issueList, string $fileName, string $commonPathPrefix, bool $excludeOK = false): void |
||
| 147 | { |
||
| 148 | $issues = $this->formatIssues($issueList); |
||
| 149 | $shortFilename = \substr($fileName, \strlen($commonPathPrefix)); |
||
| 150 | |||
| 151 | $data = []; |
||
| 152 | |||
| 153 | $data['issues'] = $issueList; |
||
| 154 | $data['filepath'] = $shortFilename; |
||
| 155 | $data['source'] = $this->formatSourceCode($fileName, $issues); |
||
| 156 | |||
| 157 | $depth = \substr_count($shortFilename, DIRECTORY_SEPARATOR); |
||
| 158 | $data['csspath'] = \str_repeat('../', \max($depth - 1, 0)); |
||
| 159 | |||
| 160 | //we want to exclude files without issues and there are no issues in this one |
||
| 161 | if ($excludeOK && !$data['issues']) { |
||
| 162 | return; |
||
| 163 | } |
||
| 164 | |||
| 165 | $this->ioHelper->createFile( |
||
| 166 | $this->outputDir.$shortFilename.'.html', |
||
| 167 | $this->render('review', $data) |
||
| 168 | ); |
||
| 169 | } |
||
| 170 | |||
| 171 | /** |
||
| 172 | * Highlighter method for PHP source code |
||
| 173 | * |
||
| 174 | * The source code is highlighted by PHP native method. |
||
| 175 | * Afterwords a DOMDocument will be generated with each |
||
| 176 | * line in a separate node. |
||
| 177 | * |
||
| 178 | * @param string $sourceCode The PHP source code |
||
| 179 | * |
||
| 180 | * @return DOMDocument |
||
| 181 | */ |
||
| 182 | protected function highlightPhpCode(string $sourceCode): DOMDocument |
||
| 183 | { |
||
| 184 | $code = \highlight_string($sourceCode, true); |
||
| 185 | |||
| 186 | if (\extension_loaded('mbstring') && !\mb_check_encoding($code, 'UTF-8')) { |
||
|
|
|||
| 187 | $detectOrder = (array) \mb_detect_order(); |
||
| 188 | $detectOrder[] = 'iso-8859-1'; |
||
| 189 | |||
| 190 | $encoding = \mb_detect_encoding($code, $detectOrder, true); |
||
| 191 | |||
| 192 | if (false === $encoding) { |
||
| 193 | \error_log('Error detecting file encoding'); |
||
| 194 | } |
||
| 195 | |||
| 196 | $code = \mb_convert_encoding( |
||
| 197 | $code, |
||
| 198 | 'UTF-8', |
||
| 199 | $encoding |
||
| 200 | ); |
||
| 201 | } |
||
| 202 | |||
| 203 | $sourceDom = new DOMDocument(); |
||
| 204 | $sourceDom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'.$code); |
||
| 205 | |||
| 206 | //fetch <code>-><span>->children from php generated html |
||
| 207 | $sourceElements = $sourceDom->getElementsByTagName('code')->item(0) |
||
| 208 | ->childNodes->item(0)->childNodes; |
||
| 209 | |||
| 210 | //create target dom |
||
| 211 | $targetDom = new DOMDocument(); |
||
| 212 | $targetNode = $targetDom->createElement('ol'); |
||
| 213 | $targetNode->setAttribute('class', 'code'); |
||
| 214 | $targetDom->appendChild($targetNode); |
||
| 215 | |||
| 216 | $liElement = $targetDom->createElement('li'); |
||
| 217 | $targetNode->appendChild($liElement); |
||
| 218 | |||
| 219 | // iterate through all <span> elements |
||
| 220 | foreach ($sourceElements as $sourceElement) { |
||
| 221 | if ($sourceElement instanceof DOMText) { |
||
| 222 | $span = $targetDom->createElement('span'); |
||
| 223 | $span->nodeValue = \htmlspecialchars($sourceElement->wholeText, ENT_COMPAT); |
||
| 224 | $liElement->appendChild($span); |
||
| 225 | } |
||
| 226 | |||
| 227 | if (!$sourceElement instanceof DOMElement) { |
||
| 228 | continue; |
||
| 229 | } |
||
| 230 | |||
| 231 | if ('br' === $sourceElement->tagName) { |
||
| 232 | // create new li and new line |
||
| 233 | $liElement = $targetDom->createElement('li'); |
||
| 234 | $targetNode->appendChild($liElement); |
||
| 235 | |||
| 236 | continue; |
||
| 237 | } |
||
| 238 | |||
| 239 | $elementClass = $this->mapPhpColors( |
||
| 240 | $sourceElement->getAttribute('style') |
||
| 241 | ); |
||
| 242 | |||
| 243 | foreach ($sourceElement->childNodes as $sourceChildElement) { |
||
| 244 | if ($sourceChildElement instanceof DOMElement |
||
| 245 | && 'br' === $sourceChildElement->tagName |
||
| 246 | ) { |
||
| 247 | // create new li and new line |
||
| 248 | $liElement = $targetDom->createElement('li'); |
||
| 249 | $targetNode->appendChild($liElement); |
||
| 250 | } else { |
||
| 251 | // append content to current li element |
||
| 252 | $span = $targetDom->createElement('span'); |
||
| 253 | $span->nodeValue = \htmlspecialchars($sourceChildElement->textContent, ENT_COMPAT); |
||
| 254 | $span->setAttribute('class', $elementClass); |
||
| 255 | $liElement->appendChild($span); |
||
| 256 | } |
||
| 257 | } |
||
| 258 | } |
||
| 259 | |||
| 260 | return $targetDom; |
||
| 261 | } |
||
| 262 | |||
| 263 | /** |
||
| 264 | * Return colors defined in ini files. |
||
| 265 | * |
||
| 266 | * @param string $style The given style name, e.g. "comment" |
||
| 267 | * |
||
| 268 | * @return string |
||
| 269 | */ |
||
| 270 | protected function mapPhpColors(string $style): string |
||
| 271 | { |
||
| 272 | $color = \substr($style, 7); |
||
| 273 | |||
| 274 | return $this->phpHighlightColorMap[$color]; |
||
| 275 | } |
||
| 276 | |||
| 277 | /** |
||
| 278 | * Highlighting source code of given file. |
||
| 279 | * |
||
| 280 | * Php code is using native php highlighter. |
||
| 281 | * If PEAR Text_Highlighter is installed all defined files in $highlightMap |
||
| 282 | * will be highlighted as well. |
||
| 283 | * |
||
| 284 | * @param string $file The filename / real path to file |
||
| 285 | * |
||
| 286 | * @return DOMDocument Html representation of parsed source code |
||
| 287 | */ |
||
| 288 | protected function highlightCode(string $file): DOMDocument |
||
| 289 | { |
||
| 290 | $sourceCode = $this->ioHelper->loadFile($file); |
||
| 291 | $extension = \pathinfo($file, PATHINFO_EXTENSION); |
||
| 292 | |||
| 293 | if (\in_array($extension, $this->phpSuffixes, true)) { |
||
| 294 | return $this->highlightPhpCode($sourceCode); |
||
| 295 | } |
||
| 296 | |||
| 297 | $sourceCode = \preg_replace(['/^.*$/m', '/ /'], ['<li>$0</li>', ' '], \htmlentities($sourceCode, ENT_COMPAT)); |
||
| 298 | $sourceCode = '<div class="code"><ol class="code">'.$sourceCode.'</ol></div>'; |
||
| 299 | $sourceCode = $this->stripInvalidXml($sourceCode); |
||
| 300 | |||
| 301 | $doc = new DOMDocument(); |
||
| 302 | $doc->loadHTML($sourceCode); |
||
| 303 | |||
| 304 | return $doc; |
||
| 305 | } |
||
| 306 | |||
| 307 | /** |
||
| 308 | * Source code is highlighted an formatted. |
||
| 309 | * |
||
| 310 | * Besides highlighting, whole lines will be marked with different colors |
||
| 311 | * and JQuery functions (like tooltips) are integrated. |
||
| 312 | * |
||
| 313 | * @param string $filename The file to format |
||
| 314 | * @param array $outputIssues Sorted issueList by line number |
||
| 315 | * |
||
| 316 | * @return string Html formatted string |
||
| 317 | */ |
||
| 318 | private function formatSourceCode(string $filename, array $outputIssues): string |
||
| 319 | { |
||
| 320 | $sourceDom = $this->highlightCode($filename); |
||
| 321 | $xpath = new DOMXPath($sourceDom); |
||
| 322 | $lines = $xpath->query('//ol/li'); |
||
| 323 | |||
| 324 | // A shortcut to prevent possible trouble with log(0) |
||
| 325 | // Note that this is exactly what will happen anyways. |
||
| 326 | if ($lines->length === 0) { |
||
| 327 | return $sourceDom->saveHTML(); |
||
| 328 | } |
||
| 329 | |||
| 330 | $lineNumber = 0; |
||
| 331 | $linePlaces = \floor(\log($lines->length, 10)) + 1; |
||
| 332 | |||
| 333 | foreach ($lines as $line) { |
||
| 334 | /** |
||
| 335 | * @var DOMElement $line |
||
| 336 | */ |
||
| 337 | $line = $line; |
||
| 338 | ++$lineNumber; |
||
| 339 | $line->setAttribute('id', 'line_'.$lineNumber); |
||
| 340 | |||
| 341 | $lineClasses = [ |
||
| 342 | ($lineNumber % 2 === 0) ? 'odd' : 'even', |
||
| 343 | ]; |
||
| 344 | |||
| 345 | if (isset($outputIssues[$lineNumber])) { |
||
| 346 | $lineClasses[] = 'hasIssues'; |
||
| 347 | $message = '|'; |
||
| 348 | |||
| 349 | foreach ($outputIssues[$lineNumber] as $issue) { |
||
| 350 | $message .= \sprintf( |
||
| 351 | ' |
||
| 352 | <div class="tooltip"> |
||
| 353 | <div class="title %s">%s</div> |
||
| 354 | <div class="text">%s</div> |
||
| 355 | </div> |
||
| 356 | ', |
||
| 357 | $issue->getFoundBy(), |
||
| 358 | $issue->getFoundBy(), |
||
| 359 | $issue->getDescription() |
||
| 360 | ); |
||
| 361 | } |
||
| 362 | |||
| 363 | $line->setAttribute('title', $message); |
||
| 364 | } |
||
| 365 | |||
| 366 | // Add line number |
||
| 367 | $nuSpan = $sourceDom->createElement('span'); |
||
| 368 | $nuSpan->setAttribute('class', 'lineNumber'); |
||
| 369 | |||
| 370 | for ($i = 0; $i < $linePlaces - \strlen((string) $lineNumber); ++$i) { |
||
| 371 | $nuSpan->appendChild($sourceDom->createEntityReference('nbsp')); |
||
| 372 | } |
||
| 373 | |||
| 374 | $nuSpan->appendChild($sourceDom->createTextNode((string) $lineNumber)); |
||
| 375 | $nuSpan->appendChild($sourceDom->createEntityReference('nbsp')); |
||
| 376 | $line->insertBefore($nuSpan, $line->firstChild); |
||
| 377 | |||
| 378 | //create anchor for the new line |
||
| 379 | $anchor = $sourceDom->createElement('a'); |
||
| 380 | $anchor->setAttribute('name', 'line_'.$lineNumber); |
||
| 381 | $line->appendChild($anchor); |
||
| 382 | |||
| 383 | $lineErrorCount = (isset($outputIssues[$lineNumber]) |
||
| 384 | ? \count($outputIssues[$lineNumber]) |
||
| 385 | : 0); |
||
| 386 | |||
| 387 | // set li css class depending on line errors |
||
| 388 | switch ($lineErrorCount) { |
||
| 389 | case 0: |
||
| 390 | break; |
||
| 391 | case 1: |
||
| 392 | $lineClasses[] = $outputIssues[$lineNumber][0]->getFoundBy(); |
||
| 393 | |||
| 394 | break; |
||
| 395 | case 1 < $lineErrorCount: |
||
| 396 | $lineClasses[] = 'moreErrors'; |
||
| 397 | |||
| 398 | break; |
||
| 399 | |||
| 400 | // This can't happen, count always returns >= 0 |
||
| 401 | // @codeCoverageIgnoreStart |
||
| 402 | default: |
||
| 403 | break; |
||
| 404 | |||
| 405 | // @codeCoverageIgnoreEnd |
||
| 406 | } |
||
| 407 | |||
| 408 | $line->setAttribute('class', \implode(' ', $lineClasses)); |
||
| 409 | } |
||
| 410 | |||
| 411 | return $sourceDom->saveHTML(); |
||
| 412 | } |
||
| 413 | |||
| 414 | /** |
||
| 415 | * Sorting a list of issues combining issues matching same line number |
||
| 416 | * for each file. |
||
| 417 | * |
||
| 418 | * @param array $issueList List of issues |
||
| 419 | * |
||
| 420 | * @return array |
||
| 421 | */ |
||
| 422 | private function formatIssues(array $issueList): array |
||
| 433 | } |
||
| 434 | |||
| 435 | /** |
||
| 436 | * Removes invalid XML |
||
| 437 | * |
||
| 438 | * @access private |
||
| 439 | * |
||
| 440 | * @param string $value |
||
| 441 | * |
||
| 442 | * @return string |
||
| 443 | */ |
||
| 444 | private function stripInvalidXml(string $value): string |
||
| 471 | } |
||
| 472 | } |
||
| 473 |