From 907748d176791b77312fde16252beb80283190c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 19 Dec 2013 23:11:11 +0100 Subject: [PATCH] [Twig] Decouple Twig commands from the Famework --- src/Symfony/Bridge/Twig/CHANGELOG.md | 5 + .../Bridge/Twig/Command/LintCommand.php | 173 ++++++++++++++++++ .../Twig/Tests/Command/LintCommandTest.php | 102 +++++++++++ src/Symfony/Bridge/Twig/composer.json | 3 +- .../Bundle/TwigBundle/Command/LintCommand.php | 144 ++++----------- src/Symfony/Bundle/TwigBundle/composer.json | 2 +- 6 files changed, 319 insertions(+), 110 deletions(-) create mode 100644 src/Symfony/Bridge/Twig/Command/LintCommand.php create mode 100644 src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 346d52a5ad41..4be010ba20e5 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.5.0 +----- + + * moved command `twig:lint` from `TwigBundle` + 2.4.0 ----- diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php new file mode 100644 index 000000000000..5ebd255450d4 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -0,0 +1,173 @@ + + * + * 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 + * @author Jérôme Tamarelle + */ +class LintCommand extends Command +{ + private $twig; + + /** + * {@inheritDoc} + */ + public function __construct($name = 'twig:lint') + { + parent::__construct($name); + } + + /** + * Sets the twig environment + * + * @param \Twig_Environment $twig + */ + public function setTwigEnvironment(\Twig_Environment $twig) + { + $this->twig = $twig; + } + + /** + * @return \Twig_Environment $twig + */ + protected function getTwigEnvironment() + { + return $this->twig; + } + + protected function configure() + { + $this + ->setDescription('Lints a template and outputs encountered errors') + ->addArgument('filename') + ->setHelp(<<%command.name% command lints a template and outputs to stdout +the first encountered syntax error. + +php %command.full_name% filename + +The command gets the contents of filename and validates its syntax. + +php %command.full_name% dirname + +The command finds all twig templates in dirname and validates the syntax +of each Twig template. + +cat filename | php %command.full_name% + +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('OK'.($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("KO in %s (line %s)", $file, $line)); + } else { + $output->writeln(sprintf("KO (line %s)", $line)); + } + + foreach ($lines as $no => $code) { + $output->writeln(sprintf( + "%s %-6s %s", + $no == $line ? '>>' : ' ', + $no, + $code + )); + if ($no == $line) { + $output->writeln(sprintf('>> %s ', $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; + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php new file mode 100644 index 000000000000..da4460ccbe93 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -0,0 +1,102 @@ + + * + * 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); + } + } + } +} diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index ea3fb7cde5b9..02e98c3ccd5c 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -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", diff --git a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php index aa01ce6e7b0a..79a765d768e5 100644 --- a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php +++ b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php @@ -11,141 +11,69 @@ namespace Symfony\Bundle\TwigBundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Bridge\Twig\Command\LintCommand as BaseLintCommand; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\Finder\Finder; /** * Command that will validate your template syntax and output encountered errors. * * @author Marc Weistroff + * @author Jérôme Tamarelle */ -class LintCommand extends ContainerAwareCommand +class LintCommand extends BaseLintCommand implements ContainerAwareInterface { - protected function configure() + /** + * @var ContainerInterface|null + */ + private $container; + + /** + * {@inheritdoc} + */ + public function setContainer(ContainerInterface $container = null) { - $this - ->setName('twig:lint') - ->setDescription('Lints a template and outputs encountered errors') - ->addArgument('filename') - ->setHelp(<<%command.name% command lints a template and outputs to stdout -the first encountered syntax error. + $this->container = $container; + } -php %command.full_name% filename + /** + * {@inheritdoc} + */ + protected function getTwigEnvironment() + { + return $this->container->get('twig'); + } -The command gets the contents of filename and validates its syntax. + /** + * {@inheritdoc} + */ + protected function configure() + { + parent::configure(); -php %command.full_name% dirname + $this + ->setHelp( + $this->getHelp().<<dirname and validates the syntax -of each Twig template. php %command.full_name% @AcmeMyBundle The command finds all twig templates in the AcmeMyBundle bundle and validates the syntax of each Twig template. - -cat filename | php %command.full_name% - -The command gets the template contents from stdin and validates its syntax. EOF ) ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function findFiles($filename) { - $twig = $this->getContainer()->get('twig'); - $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); - } - - if (0 !== strpos($filename, '@') && !is_readable($filename)) { - throw new \RuntimeException(sprintf('File or directory "%s" is not readable', $filename)); - } - - $files = array(); - if (is_file($filename)) { - $files = array($filename); - } elseif (is_dir($filename)) { - $files = Finder::create()->files()->in($filename)->name('*.twig'); - } else { + if (0 === strpos($filename, '@')) { $dir = $this->getApplication()->getKernel()->locateResource($filename); - $files = Finder::create()->files()->in($dir)->name('*.twig'); - } - - $errors = 0; - foreach ($files as $file) { - $errors += $this->validateTemplate($twig, $output, file_get_contents($file), $file); - } - - return $errors > 0 ? 1 : 0; - } - - protected function validateTemplate(\Twig_Environment $twig, OutputInterface $output, $template, $file = null) - { - try { - $twig->parse($twig->tokenize($template, $file ? (string) $file : null)); - $output->writeln('OK'.($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("KO in %s (line %s)", $file, $line)); - } else { - $output->writeln(sprintf("KO (line %s)", $line)); - } - - foreach ($lines as $no => $code) { - $output->writeln(sprintf( - "%s %-6s %s", - $no == $line ? '>>' : ' ', - $no, - $code - )); - if ($no == $line) { - $output->writeln(sprintf('>> %s ', $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 Finder::create()->files()->in($dir)->name('*.twig'); } - return $result; + return parent::findFiles($filename); } } diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 83b8285d5674..88d866989ad2 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=5.3.3", - "symfony/twig-bridge": "~2.2", + "symfony/twig-bridge": "~2.5", "symfony/http-kernel": "~2.1" }, "require-dev": {