Skip to content

[Twig] Decouple Twig commands from the Famework #9855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Bridge/Twig/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

2.5.0
-----

* moved command `twig:lint` from `TwigBundle`

2.4.0
-----

Expand Down
173 changes: 173 additions & 0 deletions src/Symfony/Bridge/Twig/Command/LintCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Twig\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;

/**
* Command that will validate your template syntax and output encountered errors.
*
* @author Marc Weistroff <marc.weistroff@sensiolabs.com>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure this is you @GromNaN ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original command has been written by @marcw.

* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class LintCommand extends Command
{
private $twig;

/**
* {@inheritDoc}
*/
public function __construct($name = 'twig:lint')
{
parent::__construct($name);
}

/**
* Sets the twig environment
*
* @param \Twig_Environment $twig
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just Twig_Environment or is the \ also needed here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remove the \, it becomes a relative class name, and you need to add a use statement for it. and Symfony does not add use statements for classes of the global namespace

*/
public function setTwigEnvironment(\Twig_Environment $twig)
{
$this->twig = $twig;
}

/**
* @return \Twig_Environment $twig
*/
protected function getTwigEnvironment()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make this abstract and remove the setter. This would prevent making the setter non-functioning in the Bundle Command.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a developer, I don't want to be forced to create a class to use this command.

{
return $this->twig;
}

protected function configure()
{
$this
->setDescription('Lints a template and outputs encountered errors')
->addArgument('filename')
->setHelp(<<<EOF
The <info>%command.name%</info> command lints a template and outputs to stdout
the first encountered syntax error.

<info>php %command.full_name% filename</info>

The command gets the contents of <comment>filename</comment> and validates its syntax.

<info>php %command.full_name% dirname</info>

The command finds all twig templates in <comment>dirname</comment> and validates the syntax
of each Twig template.

<info>cat filename | php %command.full_name%</info>

The command gets the template contents from stdin and validates its syntax.
EOF
)
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$twig = $this->getTwigEnvironment();
$template = null;
$filename = $input->getArgument('filename');

if (!$filename) {
if (0 !== ftell(STDIN)) {
throw new \RuntimeException("Please provide a filename or pipe template content to stdin.");
}

while (!feof(STDIN)) {
$template .= fread(STDIN, 1024);
}

return $this->validateTemplate($twig, $output, $template);
}

$files = $this->findFiles($filename);

$errors = 0;
foreach ($files as $file) {
$errors += $this->validateTemplate($twig, $output, file_get_contents($file), $file);
}

return $errors > 0 ? 1 : 0;
}

protected function findFiles($filename)
{
if (is_file($filename)) {
return array($filename);
} elseif (is_dir($filename)) {
return Finder::create()->files()->in($filename)->name('*.twig');
}

throw new \RuntimeException(sprintf('File or directory "%s" is not readable', $filename));
}

protected function validateTemplate(\Twig_Environment $twig, OutputInterface $output, $template, $file = null)
{
try {
$twig->parse($twig->tokenize($template, $file ? (string) $file : null));
$output->writeln('<info>OK</info>'.($file ? sprintf(' in %s', $file) : ''));
} catch (\Twig_Error $e) {
$this->renderException($output, $template, $e, $file);

return 1;
}

return 0;
}

protected function renderException(OutputInterface $output, $template, \Twig_Error $exception, $file = null)
{
$line = $exception->getTemplateLine();
$lines = $this->getContext($template, $line);

if ($file) {
$output->writeln(sprintf("<error>KO</error> in %s (line %s)", $file, $line));
} else {
$output->writeln(sprintf("<error>KO</error> (line %s)", $line));
}

foreach ($lines as $no => $code) {
$output->writeln(sprintf(
"%s %-6s %s",
$no == $line ? '<error>>></error>' : ' ',
$no,
$code
));
if ($no == $line) {
$output->writeln(sprintf('<error>>> %s</error> ', $exception->getRawMessage()));
}
}
}

protected function getContext($template, $line, $context = 3)
{
$lines = explode("\n", $template);

$position = max(0, $line - $context);
$max = min(count($lines), $line - 1 + $context);

$result = array();
while ($position < $max) {
$result[$position + 1] = $lines[$position];
$position++;
}

return $result;
}
}
102 changes: 102 additions & 0 deletions src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Twig\Tests\Command;

use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Console\Application;
use Symfony\Bridge\Twig\Command\LintCommand;

/**
* @covers \Symfony\Bridge\Twig\Command\LintCommand
*/
class LintCommandTest extends \PHPUnit_Framework_TestCase
{
private $files;

public function testLintCorrectFile()
{
$tester = $this->createCommandTester();
$filename = $this->createFile('{{ foo }}');

$ret = $tester->execute(array('filename' => $filename));

$this->assertEquals(0, $ret, 'Returns 0 in case of success');
$this->assertRegExp('/^OK in /', $tester->getDisplay());
}

public function testLintIncorrectFile()
{
$tester = $this->createCommandTester();
$filename = $this->createFile('{{ foo');

$ret = $tester->execute(array('filename' => $filename));

$this->assertEquals(1, $ret, 'Returns 1 in case of error');
$this->assertRegExp('/^KO in /', $tester->getDisplay());
}

/**
* @expectedException \RuntimeException
*/
public function testLintFileNotReadable()
{
$tester = $this->createCommandTester();
$filename = $this->createFile('');
unlink($filename);

$ret = $tester->execute(array('filename' => $filename));
}

/**
* @return CommandTester
*/
private function createCommandTester()
{
$twig = new \Twig_Environment(new \Twig_Loader_Filesystem());

$command = new LintCommand();
$command->setTwigEnvironment($twig);

$application = new Application();
$application->add($command);
$command = $application->find('twig:lint');

return new CommandTester($command);
}

/**
* @return string Path to the new file
*/
private function createFile($content)
{
$filename = tempnam(sys_get_temp_dir(), 'sf-');
file_put_contents($filename, $content);

$this->files[] = $filename;

return $filename;
}

public function setUp()
{
$this->files = array();
}

public function tearDown()
{
foreach ($this->files as $file) {
if (file_exists($file)) {
unlink($file);
}
}
}
}
3 changes: 2 additions & 1 deletion src/Symfony/Bridge/Twig/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"symfony/translation": "~2.2",
"symfony/yaml": "~2.0",
"symfony/security": "~2.4",
"symfony/stopwatch": "~2.2"
"symfony/stopwatch": "~2.2",
"symfony/console": "~2.2"
},
"suggest": {
"symfony/form": "For using the FormExtension",
Expand Down
Loading