merged branch fabpot/code-helpers (PR #5986)

This PR was merged into the master branch.

Commits
-------

56fe8d1 duplicated the code helper code to the Twig bundle

Discussion
----------

moved code helper code to the Twig bundle

These helpers are very specific and are only used in TwigBundle for the
profiler and the exception templates.

---------------------------------------------------------------------------

by schmittjoh at 2012-11-12T09:12:56Z

Is there a reason for this BC break other than a cosmetical tweak?

If not strictly necessary, I'd like to see these kind of changes being scheduled for 3.0.

---------------------------------------------------------------------------

by fabpot at 2012-11-12T09:29:35Z

Of course, I don't want to make this change without a reason and indeed, I forgot to mention the why of this change. Let me explain.

I've been working on the integration of the Symfony web profiler into other OSS that use HttpKernel like Silex and Drupal for quite some time now. That's why I developed the new namespace feature in Twig, that's why I've refactored the web profiler to only use Twig and not the templating component. One of the last step is this PR, which reduces the number of dependencies to be able to use the WebProfiler bundle.

So, this change is not cosmetic, but one more step towards the goal of making the web profiler more reusable.

---------------------------------------------------------------------------

by schmittjoh at 2012-11-12T13:22:28Z

I see, makes sense. How about duplicating the code for now?

After looking through the history, it doesn't seem like it changes often
(the last real code change was a year ago). So, it should not be much more
maintenance effort, and we could keep BC here.

On Mon, Nov 12, 2012 at 10:29 AM, Fabien Potencier <notifications@github.com
> wrote:

> Of course, I don't want to make this change without a reason and indeed, I
> forgot to mention the why of this change. Let me explain.
>
> I've been working on the integration of the Symfony web profiler into
> other OSS that use HttpKernel like Silex and Drupal for quite some time
> now. That's why I developed the new namespace feature in Twig, that's why
> I've refactored the web profiler to only use Twig and not the templating
> component. One of the last step is this PR, which reduces the number of
> dependencies to be able to use the WebProfiler bundle.
>
> So, this change is not cosmetic, but one more step towards the goal of
> making the web profiler more reusable.
>
> —
> Reply to this email directly or view it on GitHub<https://github.com/symfony/symfony/pull/5986#issuecomment-10281201>.
>
>

---------------------------------------------------------------------------

by fabpot at 2012-11-12T13:32:33Z

Of course, that's a possibility. But what for? I doubt that people are using this code. Are you using this code somewhere?

---------------------------------------------------------------------------

by schmittjoh at 2012-11-12T13:37:24Z

Yes, I believe that I'm using it both in JMSDebuggingBundle, and
JMSJobQueueBundle as both are rendering exception stack traces at some
point.

On Mon, Nov 12, 2012 at 2:32 PM, Fabien Potencier
<notifications@github.com>wrote:

> Of course, that's a possibility. But what for? I doubt that people are
> using this code. Are you using this code somewhere?
>
> —
> Reply to this email directly or view it on GitHub<https://github.com/symfony/symfony/pull/5986#issuecomment-10287353>.
>
>

---------------------------------------------------------------------------

by fabpot at 2012-11-12T14:11:50Z

ok, fair enough. The code is now in the framework bundle as well.
This commit is contained in:
Fabien Potencier 2012-11-12 15:15:24 +01:00
commit 226743f8cf
3 changed files with 167 additions and 30 deletions

View File

@ -11,26 +11,33 @@
namespace Symfony\Bundle\TwigBundle\Extension;
use Symfony\Component\DependencyInjection\ContainerInterface;
if (!defined('ENT_SUBSTITUTE')) {
define('ENT_SUBSTITUTE', 8);
}
/**
* Twig extension for Symfony code helper
*
* Twig extension relate to PHP code and used by the profiler and the default exception templates.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class CodeExtension extends \Twig_Extension
{
private $container;
private $fileLinkFormat;
private $rootDir;
private $charset;
/**
* Constructor of Twig Extension to provide functions for code formatting
* Constructor.
*
* @param ContainerInterface $container A ContainerInterface instance
* @param string $fileLinkFormat The format for links to source files
* @param string $rootDir The project root directory
* @param string $charset The charset
*/
public function __construct(ContainerInterface $container)
public function __construct($fileLinkFormat, $rootDir, $charset)
{
$this->container = $container;
$this->fileLinkFormat = empty($fileLinkFormat) ? ini_get('xdebug.file_link_format') : $fileLinkFormat;
$this->rootDir = str_replace('\\', '/', $rootDir).'/';
$this->charset = $charset;
}
/**
@ -52,46 +59,174 @@ class CodeExtension extends \Twig_Extension
public function abbrClass($class)
{
return $this->container->get('templating.helper.code')->abbrClass($class);
$parts = explode('\\', $class);
$short = array_pop($parts);
return sprintf("<abbr title=\"%s\">%s</abbr>", $class, $short);
}
public function abbrMethod($method)
{
return $this->container->get('templating.helper.code')->abbrMethod($method);
if (false !== strpos($method, '::')) {
list($class, $method) = explode('::', $method, 2);
$result = sprintf("%s::%s()", $this->abbrClass($class), $method);
} elseif ('Closure' === $method) {
$result = sprintf("<abbr title=\"%s\">%s</abbr>", $method, $method);
} else {
$result = sprintf("<abbr title=\"%s\">%s</abbr>()", $method, $method);
}
return $result;
}
/**
* Formats an array as a string.
*
* @param array $args The argument array
*
* @return string
*/
public function formatArgs($args)
{
return $this->container->get('templating.helper.code')->formatArgs($args);
$result = array();
foreach ($args as $key => $item) {
if ('object' === $item[0]) {
$parts = explode('\\', $item[1]);
$short = array_pop($parts);
$formattedValue = sprintf("<em>object</em>(<abbr title=\"%s\">%s</abbr>)", $item[1], $short);
} elseif ('array' === $item[0]) {
$formattedValue = sprintf("<em>array</em>(%s)", is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
} elseif ('string' === $item[0]) {
$formattedValue = sprintf("'%s'", htmlspecialchars($item[1], ENT_QUOTES, $this->getCharset()));
} elseif ('null' === $item[0]) {
$formattedValue = '<em>null</em>';
} elseif ('boolean' === $item[0]) {
$formattedValue = '<em>'.strtolower(var_export($item[1], true)).'</em>';
} elseif ('resource' === $item[0]) {
$formattedValue = '<em>resource</em>';
} else {
$formattedValue = str_replace("\n", '', var_export(htmlspecialchars((string) $item[1], ENT_QUOTES, $this->getCharset()), true));
}
$result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue);
}
return implode(', ', $result);
}
/**
* Formats an array as a string.
*
* @param array $args The argument array
*
* @return string
*/
public function formatArgsAsText($args)
{
return $this->container->get('templating.helper.code')->formatArgsAsText($args);
return strip_tags($this->formatArgs($args));
}
/**
* Returns an excerpt of a code file around the given line number.
*
* @param string $file A file path
* @param int $line The selected line number
*
* @return string An HTML string
*/
public function fileExcerpt($file, $line)
{
return $this->container->get('templating.helper.code')->fileExcerpt($file, $line);
if (is_readable($file)) {
$code = highlight_file($file, true);
// remove main code/span tags
$code = preg_replace('#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s', '\\1', $code);
$content = preg_split('#<br />#', $code);
$lines = array();
for ($i = max($line - 3, 1), $max = min($line + 3, count($content)); $i <= $max; $i++) {
$lines[] = '<li'.($i == $line ? ' class="selected"' : '').'><code>'.self::fixCodeMarkup($content[$i - 1]).'</code></li>';
}
return '<ol start="'.max($line - 3, 1).'">'.implode("\n", $lines).'</ol>';
}
}
/**
* Formats a file path.
*
* @param string $file An absolute file path
* @param integer $line The line number
* @param string $text Use this text for the link rather than the file path
*
* @return string
*/
public function formatFile($file, $line, $text = null)
{
return $this->container->get('templating.helper.code')->formatFile($file, $line, $text);
if (null === $text) {
$file = trim($file);
$fileStr = $file;
if (0 === strpos($fileStr, $this->rootDir)) {
$fileStr = str_replace($this->rootDir, '', str_replace('\\', '/', $fileStr));
$fileStr = sprintf('<abbr title="%s">kernel.root_dir</abbr>/%s', $this->rootDir, $fileStr);
}
$text = "$fileStr at line $line";
}
if (false !== $link = $this->getFileLink($file, $line)) {
return sprintf('<a href="%s" title="Click to open this file" class="file_link">%s</a>', htmlspecialchars($link, ENT_QUOTES | ENT_SUBSTITUTE, $this->charset), $text);
}
return $text;
}
/**
* Returns the link for a given file/line pair.
*
* @param string $file An absolute file path
* @param integer $line The line number
*
* @return string A link of false
*/
public function getFileLink($file, $line)
{
return $this->container->get('templating.helper.code')->getFileLink($file, $line);
if ($this->fileLinkFormat && is_file($file)) {
return strtr($this->fileLinkFormat, array('%f' => $file, '%l' => $line));
}
return false;
}
public function formatFileFromText($text)
{
return $this->container->get('templating.helper.code')->formatFileFromText($text);
$that = $this;
return preg_replace_callback('/in ("|&quot;)?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', function ($match) use ($that) {
return 'in '.$that->formatFile($match[2], $match[3]);
}, $text);
}
public function getName()
{
return 'code';
}
protected static function fixCodeMarkup($line)
{
// </span> ending tag from previous line
$opening = strpos($line, '<span');
$closing = strpos($line, '</span>');
if (false !== $closing && (false === $opening || $closing < $opening)) {
$line = substr_replace($line, '', $closing, 7);
}
// missing </span> tag at the end of line
$opening = strpos($line, '<span');
$closing = strpos($line, '</span>');
if (false !== $opening && (false === $closing || $closing > $opening)) {
$line .= '</span>';
}
return $line;
}
}

View File

@ -66,7 +66,9 @@
<service id="twig.extension.code" class="%twig.extension.code.class%" public="false">
<tag name="twig.extension" />
<argument type="service" id="service_container" />
<argument>%templating.helper.code.file_link_format%</argument>
<argument>%kernel.root_dir%</argument>
<argument>%kernel.charset%</argument>
</service>
<service id="twig.extension.routing" class="%twig.extension.routing.class%" public="false">

View File

@ -9,23 +9,18 @@
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper;
namespace Symfony\Bundle\TwigBundle\Tests\Extension;
use Symfony\Bundle\FrameworkBundle\Templating\Helper\CodeHelper;
use Symfony\Bundle\TwigBundle\Extension\CodeExtension;
class CodeHelperTest extends \PHPUnit_Framework_TestCase
class CodeExtensionTest extends \PHPUnit_Framework_TestCase
{
protected static $helper;
public static function setUpBeforeClass()
{
self::$helper = new CodeHelper('txmt://open?url=file://%f&line=%l', '/root', 'UTF-8');
}
protected $helper;
public function testFormatFile()
{
$expected = sprintf('<a href="txmt://open?url=file://%s&amp;line=25" title="Click to open this file" class="file_link">%s at line 25</a>', __FILE__, __FILE__);
$this->assertEquals($expected, self::$helper->formatFile(__FILE__, 25));
$this->assertEquals($expected, $this->getExtension()->formatFile(__FILE__, 25));
}
/**
@ -33,7 +28,7 @@ class CodeHelperTest extends \PHPUnit_Framework_TestCase
*/
public function testGettingClassAbbreviation($class, $abbr)
{
$this->assertEquals(self::$helper->abbrClass($class), $abbr);
$this->assertEquals($this->getExtension()->abbrClass($class), $abbr);
}
/**
@ -41,7 +36,7 @@ class CodeHelperTest extends \PHPUnit_Framework_TestCase
*/
public function testGettingMethodAbbreviation($method, $abbr)
{
$this->assertEquals(self::$helper->abbrMethod($method), $abbr);
$this->assertEquals($this->getExtension()->abbrMethod($method), $abbr);
}
public function getClassNameProvider()
@ -64,6 +59,11 @@ class CodeHelperTest extends \PHPUnit_Framework_TestCase
public function testGetName()
{
$this->assertEquals('code', self::$helper->getName());
$this->assertEquals('code', $this->getExtension()->getName());
}
protected function getExtension()
{
return new CodeExtension('txmt://open?url=file://%f&line=%l', '/root', 'UTF-8');
}
}