| Total Complexity | 89 |
| Total Lines | 757 |
| Duplicated Lines | 0 % |
| Coverage | 99.35% |
| Changes | 0 | ||
Complex classes like Quick 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 Quick, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 14 | class Quick |
||
| 15 | { |
||
| 16 | /** |
||
| 17 | * Generate the Quick renderer's source |
||
| 18 | * |
||
| 19 | * @param array $compiledTemplates Array of tagName => compiled template |
||
| 20 | * @return string |
||
| 21 | */ |
||
| 22 | 379 | public static function getSource(array $compiledTemplates) |
|
| 23 | { |
||
| 24 | 379 | $map = ['dynamic' => [], 'php' => [], 'static' => []]; |
|
| 25 | 379 | $tagNames = []; |
|
| 26 | 379 | $unsupported = []; |
|
| 27 | |||
| 28 | // Ignore system tags |
||
| 29 | 379 | unset($compiledTemplates['br']); |
|
| 30 | 379 | unset($compiledTemplates['e']); |
|
| 31 | 379 | unset($compiledTemplates['i']); |
|
| 32 | 379 | unset($compiledTemplates['p']); |
|
| 33 | 379 | unset($compiledTemplates['s']); |
|
| 34 | |||
| 35 | 379 | foreach ($compiledTemplates as $tagName => $php) |
|
| 36 | { |
||
| 37 | 360 | $renderings = self::getRenderingStrategy($php); |
|
| 38 | 360 | if (empty($renderings)) |
|
| 39 | { |
||
| 40 | 5 | $unsupported[] = $tagName; |
|
| 41 | 5 | continue; |
|
| 42 | } |
||
| 43 | |||
| 44 | 358 | foreach ($renderings as $i => list($strategy, $replacement)) |
|
| 45 | { |
||
| 46 | 358 | $match = (($i) ? '/' : '') . $tagName; |
|
| 47 | 358 | $map[$strategy][$match] = $replacement; |
|
| 48 | } |
||
| 49 | |||
| 50 | // Record the names of tags whose template does not contain a passthrough |
||
| 51 | 358 | if (!isset($renderings[1])) |
|
| 52 | { |
||
| 53 | 222 | $tagNames[] = $tagName; |
|
| 54 | } |
||
| 55 | } |
||
| 56 | |||
| 57 | 379 | $php = []; |
|
| 58 | 379 | $php[] = ' /** {@inheritdoc} */'; |
|
| 59 | 379 | $php[] = ' public $enableQuickRenderer=true;'; |
|
| 60 | 379 | $php[] = ' /** {@inheritdoc} */'; |
|
| 61 | 379 | $php[] = ' protected $static=' . self::export($map['static']) . ';'; |
|
| 62 | 379 | $php[] = ' /** {@inheritdoc} */'; |
|
| 63 | 379 | $php[] = ' protected $dynamic=' . self::export($map['dynamic']) . ';'; |
|
| 64 | |||
| 65 | 379 | $quickSource = ''; |
|
| 66 | 379 | if (!empty($map['php'])) |
|
| 67 | { |
||
| 68 | 228 | $quickSource = SwitchStatement::generate('$id', $map['php']); |
|
| 69 | } |
||
| 70 | |||
| 71 | // Build a regexp that matches all the tags |
||
| 72 | 379 | $regexp = '(<(?:(?!/)('; |
|
| 73 | 379 | $regexp .= ($tagNames) ? RegexpBuilder::fromList($tagNames) : '(?!)'; |
|
| 74 | 379 | $regexp .= ')(?: [^>]*)?>.*?</\\1|(/?(?!br/|p>)[^ />]+)[^>]*?(/)?)>)s'; |
|
| 75 | 379 | $php[] = ' /** {@inheritdoc} */'; |
|
| 76 | 379 | $php[] = ' protected $quickRegexp=' . var_export($regexp, true) . ';'; |
|
| 77 | |||
| 78 | // Build a regexp that matches tags that cannot be rendered with the Quick renderer |
||
| 79 | 379 | if (!empty($unsupported)) |
|
| 80 | { |
||
| 81 | 5 | $regexp = '((?<=<)(?:[!?]|' . RegexpBuilder::fromList($unsupported) . '[ />]))'; |
|
| 82 | 5 | $php[] = ' /** {@inheritdoc} */'; |
|
| 83 | 5 | $php[] = ' protected $quickRenderingTest=' . var_export($regexp, true) . ';'; |
|
| 84 | } |
||
| 85 | |||
| 86 | 379 | $php[] = ' /** {@inheritdoc} */'; |
|
| 87 | 379 | $php[] = ' protected function renderQuickTemplate($id, $xml)'; |
|
| 88 | 379 | $php[] = ' {'; |
|
| 89 | 379 | $php[] = ' $attributes=$this->matchAttributes($xml);'; |
|
| 90 | 379 | $php[] = " \$html='';" . $quickSource; |
|
| 91 | 379 | $php[] = ''; |
|
| 92 | 379 | $php[] = ' return $html;'; |
|
| 93 | 379 | $php[] = ' }'; |
|
| 94 | |||
| 95 | 379 | return implode("\n", $php); |
|
| 96 | } |
||
| 97 | |||
| 98 | /** |
||
| 99 | * Export an array as PHP |
||
| 100 | * |
||
| 101 | * @param array $arr |
||
| 102 | * @return string |
||
| 103 | */ |
||
| 104 | 379 | protected static function export(array $arr) |
|
| 117 | } |
||
| 118 | |||
| 119 | /** |
||
| 120 | * Compute the rendering strategy for a compiled template |
||
| 121 | * |
||
| 122 | * @param string $php Template compiled for the PHP renderer |
||
| 123 | * @return array[] An array containing 0 to 2 pairs of [<rendering type>, <replacement>] |
||
| 124 | */ |
||
| 125 | 390 | public static function getRenderingStrategy($php) |
|
| 144 | } |
||
| 145 | |||
| 146 | /** |
||
| 147 | * Generate the code for rendering a compiled template with the Quick renderer |
||
| 148 | * |
||
| 149 | * Parse and record every code path that contains a passthrough. Parse every if-else structure. |
||
| 150 | * When the whole structure is parsed, there are 2 possible situations: |
||
| 151 | * - no code path contains a passthrough, in which case we discard the data |
||
| 152 | * - all the code paths including the mandatory "else" branch contain a passthrough, in which |
||
| 153 | * case we keep the data |
||
| 154 | * |
||
| 155 | * @param string $php Template compiled for the PHP renderer |
||
| 156 | * @return string[] An array containing one or two strings of PHP, or an empty array |
||
| 157 | * if the PHP cannot be converted |
||
| 158 | */ |
||
| 159 | 390 | protected static function getQuickRendering($php) |
|
| 331 | } |
||
| 332 | |||
| 333 | /** |
||
| 334 | * Convert the two sides of a compiled template to quick rendering |
||
| 335 | * |
||
| 336 | * @param string &$head |
||
| 337 | * @param string &$tail |
||
| 338 | * @param bool $passthrough |
||
| 339 | * @return void |
||
| 340 | */ |
||
| 341 | 386 | protected static function convertPHP(&$head, &$tail, $passthrough) |
|
| 385 | } |
||
| 386 | } |
||
| 387 | |||
| 388 | /** |
||
| 389 | * Replace the PHP code used in a compiled template to be used by the Quick renderer |
||
| 390 | * |
||
| 391 | * @param string &$php |
||
| 392 | * @return void |
||
| 393 | */ |
||
| 394 | 386 | protected static function replacePHP(&$php) |
|
| 395 | { |
||
| 396 | // Expression that matches a $node->getAttribute() call and captures its string argument |
||
| 397 | 386 | $getAttribute = "\\\$node->getAttribute\\(('[^']+')\\)"; |
|
| 398 | |||
| 399 | // Expression that matches a single-quoted string literal |
||
| 400 | 386 | $string = "'(?:[^\\\\']|\\\\.)*+'"; |
|
| 401 | |||
| 402 | $replacements = [ |
||
| 403 | 386 | '$this->out' => '$html', |
|
| 404 | |||
| 405 | // An attribute value escaped as ENT_NOQUOTES. We only need to unescape quotes |
||
| 406 | 386 | '(htmlspecialchars\\(' . $getAttribute . ',' . ENT_NOQUOTES . '\\))' |
|
| 407 | 386 | => "str_replace('"','\"',\$attributes[\$1])", |
|
| 408 | |||
| 409 | // One or several attribute values escaped as ENT_COMPAT can be used as-is |
||
| 410 | 386 | '(htmlspecialchars\\((' . $getAttribute . '(?:\\.' . $getAttribute . ')*),' . ENT_COMPAT . '\\))' |
|
| 411 | 386 | => function ($m) use ($getAttribute) |
|
| 412 | { |
||
| 413 | 210 | return preg_replace('(' . $getAttribute . ')', '$attributes[$1]', $m[1]); |
|
| 414 | 386 | }, |
|
| 415 | |||
| 416 | // Character replacement can be performed directly on the escaped value provided that it |
||
| 417 | // is then escaped as ENT_COMPAT and that replacements do not interfere with the escaping |
||
| 418 | // of the characters &<>" or their representation &<>" |
||
| 419 | 386 | '(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . ENT_COMPAT . '\\))' |
|
| 420 | 386 | => 'strtr($attributes[$1],$2,$3)', |
|
| 421 | |||
| 422 | // A comparison between two attributes. No need to unescape |
||
| 423 | 386 | '(' . $getAttribute . '(!?=+)' . $getAttribute . ')' |
|
| 424 | 386 | => '$attributes[$1]$2$attributes[$3]', |
|
| 425 | |||
| 426 | // A comparison between an attribute and a literal string. Rather than unescape the |
||
| 427 | // attribute value, we escape the literal. This applies to comparisons using XPath's |
||
| 428 | // contains() as well (translated to PHP's strpos()) |
||
| 429 | 386 | '(' . $getAttribute . '===(' . $string . '))s' |
|
| 430 | 386 | => function ($m) |
|
| 431 | { |
||
| 432 | 23 | return '$attributes[' . $m[1] . ']===' . htmlspecialchars($m[2], ENT_COMPAT); |
|
| 433 | 386 | }, |
|
| 434 | |||
| 435 | 386 | '((' . $string . ')===' . $getAttribute . ')s' |
|
| 436 | 386 | => function ($m) |
|
| 437 | { |
||
| 438 | 2 | return htmlspecialchars($m[1], ENT_COMPAT) . '===$attributes[' . $m[2] . ']'; |
|
| 439 | 386 | }, |
|
| 440 | |||
| 441 | 386 | '(strpos\\(' . $getAttribute . ',(' . $string . ')\\)([!=]==(?:0|false)))s' |
|
| 442 | 386 | => function ($m) |
|
| 443 | { |
||
| 444 | 42 | return 'strpos($attributes[' . $m[1] . '],' . htmlspecialchars($m[2], ENT_COMPAT) . ')' . $m[3]; |
|
| 445 | 386 | }, |
|
| 446 | |||
| 447 | 386 | '(strpos\\((' . $string . '),' . $getAttribute . '\\)([!=]==(?:0|false)))s' |
|
| 448 | 386 | => function ($m) |
|
| 449 | { |
||
| 450 | 15 | return 'strpos(' . htmlspecialchars($m[1], ENT_COMPAT) . ',$attributes[' . $m[2] . '])' . $m[3]; |
|
| 451 | 386 | }, |
|
| 452 | |||
| 453 | 386 | '(str_(contains|(?:end|start)s_with)\\(' . $getAttribute . ',(' . $string . ')\\))s' |
|
| 454 | 386 | => function ($m) |
|
| 455 | { |
||
| 456 | return 'str_' . $m[1] . '($attributes[' . $m[2] . '],' . htmlspecialchars($m[3], ENT_COMPAT) . ')'; |
||
| 457 | 386 | }, |
|
| 458 | |||
| 459 | 386 | '(str_(contains|(?:end|start)s_with)\\((' . $string . '),' . $getAttribute . '\\))s' |
|
| 460 | 386 | => function ($m) |
|
| 461 | { |
||
| 462 | return 'str_' . $m[1] . '(' . htmlspecialchars($m[2], ENT_COMPAT) . ',$attributes[' . $m[3] . '])'; |
||
| 463 | 386 | }, |
|
| 464 | |||
| 465 | // An attribute value used in an arithmetic comparison or operation does not need to be |
||
| 466 | // unescaped. The same applies to empty(), isset() and conditionals |
||
| 467 | 386 | '(' . $getAttribute . '(?=(?:==|[-+*])\\d+))' => '$attributes[$1]', |
|
| 468 | 386 | '(\\b(\\d+(?:==|[-+*]))' . $getAttribute . ')' => '$1$attributes[$2]', |
|
| 469 | 386 | '(empty\\(' . $getAttribute . '\\))' => 'empty($attributes[$1])', |
|
| 470 | 386 | "(\\\$node->hasAttribute\\(('[^']+')\\))" => 'isset($attributes[$1])', |
|
| 471 | 386 | 'if($node->attributes->length)' => 'if($this->hasNonNullValues($attributes))', |
|
| 472 | |||
| 473 | // In all other situations, unescape the attribute value before use |
||
| 474 | 386 | '(' . $getAttribute . ')' => 'htmlspecialchars_decode($attributes[$1])' |
|
| 475 | ]; |
||
| 476 | |||
| 477 | 386 | foreach ($replacements as $match => $replace) |
|
| 478 | { |
||
| 479 | 386 | if ($replace instanceof Closure) |
|
| 480 | { |
||
| 481 | 386 | $php = preg_replace_callback($match, $replace, $php); |
|
| 482 | } |
||
| 483 | 386 | elseif ($match[0] === '(') |
|
| 484 | { |
||
| 485 | 386 | $php = preg_replace($match, $replace, $php); |
|
|
1 ignored issue
–
show
|
|||
| 486 | } |
||
| 487 | else |
||
| 488 | { |
||
| 489 | 386 | $php = str_replace($match, $replace, $php); |
|
|
1 ignored issue
–
show
|
|||
| 490 | } |
||
| 491 | } |
||
| 492 | } |
||
| 493 | |||
| 494 | /** |
||
| 495 | * Build the source for the two sides of a templates based on the structure extracted from its |
||
| 496 | * original source |
||
| 497 | * |
||
| 498 | * @param array $branches |
||
| 499 | * @return string[] |
||
| 500 | */ |
||
| 501 | 386 | protected static function buildPHP(array $branches) |
|
| 522 | } |
||
| 523 | |||
| 524 | /** |
||
| 525 | * Get the unique values for the "passthrough" key of given branches |
||
| 526 | * |
||
| 527 | * @param array $branches |
||
| 528 | * @return integer[] |
||
| 529 | */ |
||
| 530 | 179 | protected static function getBranchesPassthrough(array $branches) |
|
| 531 | { |
||
| 532 | 179 | $values = []; |
|
| 533 | 179 | foreach ($branches as $branch) |
|
| 534 | { |
||
| 535 | 179 | $values[] = $branch['passthrough']; |
|
| 536 | } |
||
| 537 | |||
| 538 | // If the last branch isn't an "else", we act as if there was an additional branch with no |
||
| 539 | // passthrough |
||
| 540 | 179 | if ($branch['statement'] !== 'else') |
|
| 541 | { |
||
| 542 | 121 | $values[] = 0; |
|
| 543 | } |
||
| 544 | |||
| 545 | 179 | return array_unique($values); |
|
| 546 | } |
||
| 547 | |||
| 548 | /** |
||
| 549 | * Get a string suitable as a preg_replace() replacement for given PHP code |
||
| 550 | * |
||
| 551 | * @param string $php Original code |
||
| 552 | * @return array|bool Array of [regexp, replacement] if possible, or FALSE otherwise |
||
| 553 | */ |
||
| 554 | 309 | protected static function getDynamicRendering($php) |
|
| 555 | { |
||
| 556 | 309 | $rendering = ''; |
|
| 557 | |||
| 558 | 309 | $literal = "(?<literal>'((?>[^'\\\\]+|\\\\['\\\\])*)')"; |
|
| 559 | 309 | $attribute = "(?<attribute>htmlspecialchars\\(\\\$node->getAttribute\\('([^']+)'\\),2\\))"; |
|
| 560 | 309 | $value = "(?<value>$literal|$attribute)"; |
|
| 561 | 309 | $output = "(?<output>\\\$this->out\\.=$value(?:\\.(?&value))*;)"; |
|
| 562 | |||
| 563 | 309 | $copyOfAttribute = "(?<copyOfAttribute>if\\(\\\$node->hasAttribute\\('([^']+)'\\)\\)\\{\\\$this->out\\.=' \\g-1=\"'\\.htmlspecialchars\\(\\\$node->getAttribute\\('\\g-1'\\),2\\)\\.'\"';\\})"; |
|
| 564 | |||
| 565 | 309 | $regexp = '(^(' . $output . '|' . $copyOfAttribute . ')*$)'; |
|
| 566 | 309 | if (!preg_match($regexp, $php, $m)) |
|
| 567 | { |
||
| 568 | 213 | return false; |
|
| 569 | } |
||
| 570 | |||
| 571 | // Attributes that are copied in the replacement |
||
| 572 | 102 | $copiedAttributes = []; |
|
| 573 | |||
| 574 | // Attributes whose value is used in the replacement |
||
| 575 | 102 | $usedAttributes = []; |
|
| 576 | |||
| 577 | 102 | $regexp = '(' . $output . '|' . $copyOfAttribute . ')A'; |
|
| 578 | 102 | $offset = 0; |
|
| 579 | 102 | while (preg_match($regexp, $php, $m, 0, $offset)) |
|
| 580 | { |
||
| 581 | // Test whether it's normal output or a copy of attribute |
||
| 582 | 102 | if ($m['output']) |
|
| 583 | { |
||
| 584 | // 12 === strlen('$this->out.=') |
||
| 585 | 102 | $offset += 12; |
|
| 586 | |||
| 587 | 102 | while (preg_match('(' . $value . ')A', $php, $m, 0, $offset)) |
|
| 588 | { |
||
| 589 | // Test whether it's a literal or an attribute value |
||
| 590 | 102 | if ($m['literal']) |
|
| 591 | { |
||
| 592 | // Unescape the literal |
||
| 593 | 102 | $str = stripslashes(substr($m[0], 1, -1)); |
|
| 594 | |||
| 595 | // Escape special characters |
||
| 596 | 102 | $rendering .= preg_replace('([\\\\$](?=\\d))', '\\\\$0', $str); |
|
| 597 | } |
||
| 598 | else |
||
| 599 | { |
||
| 600 | 101 | $attrName = end($m); |
|
| 601 | |||
| 602 | // Generate a unique ID for this attribute name, we'll use it as a |
||
| 603 | // placeholder until we have the full list of captures and we can replace it |
||
| 604 | // with the capture number |
||
| 605 | 101 | if (!isset($usedAttributes[$attrName])) |
|
| 606 | { |
||
| 607 | 101 | $usedAttributes[$attrName] = uniqid($attrName, true); |
|
| 608 | } |
||
| 609 | |||
| 610 | 101 | $rendering .= $usedAttributes[$attrName]; |
|
| 611 | } |
||
| 612 | |||
| 613 | // Skip the match plus the next . or ; |
||
| 614 | 102 | $offset += 1 + strlen($m[0]); |
|
| 615 | } |
||
| 616 | } |
||
| 617 | else |
||
| 618 | { |
||
| 619 | 28 | $attrName = end($m); |
|
| 620 | |||
| 621 | 28 | if (!isset($copiedAttributes[$attrName])) |
|
| 622 | { |
||
| 623 | 28 | $copiedAttributes[$attrName] = uniqid($attrName, true); |
|
| 624 | } |
||
| 625 | |||
| 626 | 28 | $rendering .= $copiedAttributes[$attrName]; |
|
| 627 | 28 | $offset += strlen($m[0]); |
|
| 628 | } |
||
| 629 | } |
||
| 630 | |||
| 631 | // Gather the names of the attributes used in the replacement either by copy or by value |
||
| 632 | 102 | $attrNames = array_keys($copiedAttributes + $usedAttributes); |
|
| 633 | |||
| 634 | // Sort them alphabetically |
||
| 635 | 102 | sort($attrNames); |
|
| 636 | |||
| 637 | // Keep a copy of the attribute names to be used in the fillter subpattern |
||
| 638 | 102 | $remainingAttributes = array_combine($attrNames, $attrNames); |
|
| 639 | |||
| 640 | // Prepare the final regexp |
||
| 641 | 102 | $regexp = '(^[^ ]+'; |
|
| 642 | 102 | $index = 0; |
|
| 643 | 102 | foreach ($attrNames as $attrName) |
|
| 644 | { |
||
| 645 | // Add a subpattern that matches (and skips) any attribute definition that is not one of |
||
| 646 | // the remaining attributes we're trying to match |
||
| 647 | 102 | $regexp .= '(?> (?!' . RegexpBuilder::fromList($remainingAttributes) . '=)[^=]+="[^"]*")*'; |
|
| 648 | 102 | unset($remainingAttributes[$attrName]); |
|
| 649 | |||
| 650 | 102 | $regexp .= '('; |
|
| 651 | |||
| 652 | 102 | if (isset($copiedAttributes[$attrName])) |
|
| 653 | { |
||
| 654 | 28 | self::replacePlaceholder($rendering, $copiedAttributes[$attrName], ++$index); |
|
| 655 | } |
||
| 656 | else |
||
| 657 | { |
||
| 658 | 101 | $regexp .= '?>'; |
|
| 659 | } |
||
| 660 | |||
| 661 | 102 | $regexp .= ' ' . $attrName . '="'; |
|
| 662 | |||
| 663 | 102 | if (isset($usedAttributes[$attrName])) |
|
| 664 | { |
||
| 665 | 101 | $regexp .= '('; |
|
| 666 | |||
| 667 | 101 | self::replacePlaceholder($rendering, $usedAttributes[$attrName], ++$index); |
|
| 668 | } |
||
| 669 | |||
| 670 | 102 | $regexp .= '[^"]*'; |
|
| 671 | |||
| 672 | 102 | if (isset($usedAttributes[$attrName])) |
|
| 673 | { |
||
| 674 | 101 | $regexp .= ')'; |
|
| 675 | } |
||
| 676 | |||
| 677 | 102 | $regexp .= '")?'; |
|
| 678 | } |
||
| 679 | |||
| 680 | 102 | $regexp .= '.*)s'; |
|
| 681 | |||
| 682 | 102 | return [$regexp, $rendering]; |
|
| 683 | } |
||
| 684 | |||
| 685 | /** |
||
| 686 | * Get a string suitable as a str_replace() replacement for given PHP code |
||
| 687 | * |
||
| 688 | * @param string $php Original code |
||
| 689 | * @return bool|string Static replacement if possible, or FALSE otherwise |
||
| 690 | */ |
||
| 691 | 381 | protected static function getStaticRendering($php) |
|
| 705 | } |
||
| 706 | |||
| 707 | /** |
||
| 708 | * Get string rendering strategies for given chunks |
||
| 709 | * |
||
| 710 | * @param string $php |
||
| 711 | * @return array |
||
| 712 | */ |
||
| 713 | 382 | protected static function getStringRenderings($php) |
|
| 744 | } |
||
| 745 | |||
| 746 | /** |
||
| 747 | * Replace all instances of a uniqid with a PCRE replacement in a string |
||
| 748 | * |
||
| 749 | * @param string &$str PCRE replacement |
||
| 750 | * @param string $uniqid Unique ID |
||
| 751 | * @param integer $index Capture index |
||
| 752 | * @return void |
||
| 753 | */ |
||
| 754 | 102 | protected static function replacePlaceholder(&$str, $uniqid, $index) |
|
| 771 | ); |
||
| 772 | } |
||
| 773 | } |