vendor/symfony/twig-bridge/Extension/CodeExtension.php line 217

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Bridge\Twig\Extension;
  11. use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
  12. use Twig\Extension\AbstractExtension;
  13. use Twig\TwigFilter;
  14. /**
  15.  * Twig extension relate to PHP code and used by the profiler and the default exception templates.
  16.  *
  17.  * @author Fabien Potencier <fabien@symfony.com>
  18.  *
  19.  * @final since Symfony 4.4
  20.  */
  21. class CodeExtension extends AbstractExtension
  22. {
  23.     private $fileLinkFormat;
  24.     private $charset;
  25.     private $projectDir;
  26.     /**
  27.      * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files
  28.      * @param string                   $projectDir     The project directory
  29.      * @param string                   $charset        The charset
  30.      */
  31.     public function __construct($fileLinkFormatstring $projectDirstring $charset)
  32.     {
  33.         $this->fileLinkFormat $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
  34.         $this->projectDir str_replace('\\''/'$projectDir).'/';
  35.         $this->charset $charset;
  36.     }
  37.     /**
  38.      * {@inheritdoc}
  39.      *
  40.      * @return TwigFilter[]
  41.      */
  42.     public function getFilters()
  43.     {
  44.         return [
  45.             new TwigFilter('abbr_class', [$this'abbrClass'], ['is_safe' => ['html'], 'pre_escape' => 'html']),
  46.             new TwigFilter('abbr_method', [$this'abbrMethod'], ['is_safe' => ['html'], 'pre_escape' => 'html']),
  47.             new TwigFilter('format_args', [$this'formatArgs'], ['is_safe' => ['html']]),
  48.             new TwigFilter('format_args_as_text', [$this'formatArgsAsText']),
  49.             new TwigFilter('file_excerpt', [$this'fileExcerpt'], ['is_safe' => ['html']]),
  50.             new TwigFilter('format_file', [$this'formatFile'], ['is_safe' => ['html']]),
  51.             new TwigFilter('format_file_from_text', [$this'formatFileFromText'], ['is_safe' => ['html']]),
  52.             new TwigFilter('format_log_message', [$this'formatLogMessage'], ['is_safe' => ['html']]),
  53.             new TwigFilter('file_link', [$this'getFileLink']),
  54.             new TwigFilter('file_relative', [$this'getFileRelative']),
  55.         ];
  56.     }
  57.     public function abbrClass($class)
  58.     {
  59.         $parts explode('\\'$class);
  60.         $short array_pop($parts);
  61.         return sprintf('<abbr title="%s">%s</abbr>'$class$short);
  62.     }
  63.     public function abbrMethod($method)
  64.     {
  65.         if (str_contains($method'::')) {
  66.             [$class$method] = explode('::'$method2);
  67.             $result sprintf('%s::%s()'$this->abbrClass($class), $method);
  68.         } elseif ('Closure' === $method) {
  69.             $result sprintf('<abbr title="%s">%1$s</abbr>'$method);
  70.         } else {
  71.             $result sprintf('<abbr title="%s">%1$s</abbr>()'$method);
  72.         }
  73.         return $result;
  74.     }
  75.     /**
  76.      * Formats an array as a string.
  77.      *
  78.      * @param array $args The argument array
  79.      *
  80.      * @return string
  81.      */
  82.     public function formatArgs($args)
  83.     {
  84.         $result = [];
  85.         foreach ($args as $key => $item) {
  86.             if ('object' === $item[0]) {
  87.                 $item[1] = htmlspecialchars($item[1], \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset);
  88.                 $parts explode('\\'$item[1]);
  89.                 $short array_pop($parts);
  90.                 $formattedValue sprintf('<em>object</em>(<abbr title="%s">%s</abbr>)'$item[1], $short);
  91.             } elseif ('array' === $item[0]) {
  92.                 $formattedValue sprintf('<em>array</em>(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset));
  93.             } elseif ('null' === $item[0]) {
  94.                 $formattedValue '<em>null</em>';
  95.             } elseif ('boolean' === $item[0]) {
  96.                 $formattedValue '<em>'.strtolower(htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset)).'</em>';
  97.             } elseif ('resource' === $item[0]) {
  98.                 $formattedValue '<em>resource</em>';
  99.             } else {
  100.                 $formattedValue str_replace("\n"''htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset));
  101.             }
  102.             $result[] = \is_int($key) ? $formattedValue sprintf("'%s' => %s"htmlspecialchars($key, \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset), $formattedValue);
  103.         }
  104.         return implode(', '$result);
  105.     }
  106.     /**
  107.      * Formats an array as a string.
  108.      *
  109.      * @param array $args The argument array
  110.      *
  111.      * @return string
  112.      */
  113.     public function formatArgsAsText($args)
  114.     {
  115.         return strip_tags($this->formatArgs($args));
  116.     }
  117.     /**
  118.      * Returns an excerpt of a code file around the given line number.
  119.      *
  120.      * @param string $file       A file path
  121.      * @param int    $line       The selected line number
  122.      * @param int    $srcContext The number of displayed lines around or -1 for the whole file
  123.      *
  124.      * @return string An HTML string
  125.      */
  126.     public function fileExcerpt($file$line$srcContext 3)
  127.     {
  128.         if (is_file($file) && is_readable($file)) {
  129.             // highlight_file could throw warnings
  130.             // see https://bugs.php.net/25725
  131.             $code = @highlight_file($filetrue);
  132.             // remove main code/span tags
  133.             $code preg_replace('#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s''\\1'$code);
  134.             // split multiline spans
  135.             $code preg_replace_callback('#<span ([^>]++)>((?:[^<]*+<br \/>)++[^<]*+)</span>#', function ($m) {
  136.                 return "<span $m[1]>".str_replace('<br />'"</span><br /><span $m[1]>"$m[2]).'</span>';
  137.             }, $code);
  138.             $content explode('<br />'$code);
  139.             $lines = [];
  140.             if ($srcContext) {
  141.                 $srcContext = \count($content);
  142.             }
  143.             for ($i max($line $srcContext1), $max min($line $srcContext, \count($content)); $i <= $max; ++$i) {
  144.                 $lines[] = '<li'.($i == $line ' class="selected"' '').'><a class="anchor" name="line'.$i.'"></a><code>'.self::fixCodeMarkup($content[$i 1]).'</code></li>';
  145.             }
  146.             return '<ol start="'.max($line $srcContext1).'">'.implode("\n"$lines).'</ol>';
  147.         }
  148.         return null;
  149.     }
  150.     /**
  151.      * Formats a file path.
  152.      *
  153.      * @param string $file An absolute file path
  154.      * @param int    $line The line number
  155.      * @param string $text Use this text for the link rather than the file path
  156.      *
  157.      * @return string
  158.      */
  159.     public function formatFile($file$line$text null)
  160.     {
  161.         $file trim($file);
  162.         $line = (int) $line;
  163.         if (null === $text) {
  164.             if (null !== $rel $this->getFileRelative($file)) {
  165.                 $rel explode('/'htmlspecialchars($rel, \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset), 2);
  166.                 $text sprintf('<abbr title="%s%2$s">%s</abbr>%s'htmlspecialchars($this->projectDir, \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset), $rel[0], '/'.($rel[1] ?? ''));
  167.             } else {
  168.                 $text htmlspecialchars($file, \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset);
  169.             }
  170.         } else {
  171.             $text htmlspecialchars($text, \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset);
  172.         }
  173.         if ($line) {
  174.             $text .= ' at line '.$line;
  175.         }
  176.         if (false !== $link $this->getFileLink($file$line)) {
  177.             return sprintf('<a href="%s" title="Click to open this file" class="file_link">%s</a>'htmlspecialchars($link, \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset), $text);
  178.         }
  179.         return $text;
  180.     }
  181.     /**
  182.      * Returns the link for a given file/line pair.
  183.      *
  184.      * @param string $file An absolute file path
  185.      * @param int    $line The line number
  186.      *
  187.      * @return string|false A link or false
  188.      */
  189.     public function getFileLink($file$line)
  190.     {
  191.         if ($fmt $this->fileLinkFormat) {
  192.             return \is_string($fmt) ? strtr($fmt, ['%f' => $file'%l' => $line]) : $fmt->format($file$line);
  193.         }
  194.         return false;
  195.     }
  196.     public function getFileRelative(string $file): ?string
  197.     {
  198.         $file str_replace('\\''/'$file);
  199.         if (null !== $this->projectDir && str_starts_with($file$this->projectDir)) {
  200.             return ltrim(substr($file, \strlen($this->projectDir)), '/');
  201.         }
  202.         return null;
  203.     }
  204.     public function formatFileFromText($text)
  205.     {
  206.         return preg_replace_callback('/in ("|&quot;)?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', function ($match) {
  207.             return 'in '.$this->formatFile($match[2], $match[3]);
  208.         }, $text);
  209.     }
  210.     /**
  211.      * @internal
  212.      */
  213.     public function formatLogMessage(string $message, array $context): string
  214.     {
  215.         if ($context && str_contains($message'{')) {
  216.             $replacements = [];
  217.             foreach ($context as $key => $val) {
  218.                 if (\is_scalar($val)) {
  219.                     $replacements['{'.$key.'}'] = $val;
  220.                 }
  221.             }
  222.             if ($replacements) {
  223.                 $message strtr($message$replacements);
  224.             }
  225.         }
  226.         return htmlspecialchars($message, \ENT_COMPAT | \ENT_SUBSTITUTE$this->charset);
  227.     }
  228.     /**
  229.      * {@inheritdoc}
  230.      */
  231.     public function getName()
  232.     {
  233.         return 'code';
  234.     }
  235.     protected static function fixCodeMarkup($line)
  236.     {
  237.         // </span> ending tag from previous line
  238.         $opening strpos($line'<span');
  239.         $closing strpos($line'</span>');
  240.         if (false !== $closing && (false === $opening || $closing $opening)) {
  241.             $line substr_replace($line''$closing7);
  242.         }
  243.         // missing </span> tag at the end of line
  244.         $opening strpos($line'<span');
  245.         $closing strpos($line'</span>');
  246.         if (false !== $opening && (false === $closing || $closing $opening)) {
  247.             $line .= '</span>';
  248.         }
  249.         return trim($line);
  250.     }
  251. }