diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php
index 0753e19ed9..2833bd23de 100644
--- a/src/Symfony/Component/Console/Application.php
+++ b/src/Symfony/Component/Console/Application.php
@@ -99,7 +99,7 @@ class Application
* @param InputInterface $input An Input instance
* @param OutputInterface $output An Output instance
*
- * @return integer 0 if everything went fine, or an error code
+ * @return int 0 if everything went fine, or an error code
*
* @throws \Exception When doRun returns Exception
*
@@ -159,7 +159,7 @@ class Application
* @param InputInterface $input An Input instance
* @param OutputInterface $output An Output instance
*
- * @return integer 0 if everything went fine, or an error code
+ * @return int 0 if everything went fine, or an error code
*/
public function doRun(InputInterface $input, OutputInterface $output)
{
@@ -270,7 +270,7 @@ class Application
/**
* Sets whether to catch exceptions or not during commands execution.
*
- * @param bool $boolean Whether to catch exceptions or not during commands execution
+ * @param bool $boolean Whether to catch exceptions or not during commands execution
*
* @api
*/
@@ -282,7 +282,7 @@ class Application
/**
* Sets whether to automatically exit after a command execution or not.
*
- * @param bool $boolean Whether to automatically exit after a command execution or not
+ * @param bool $boolean Whether to automatically exit after a command execution or not
*
* @api
*/
@@ -449,7 +449,7 @@ class Application
*
* @param string $name The command name or alias
*
- * @return Boolean true if the command exists, false otherwise
+ * @return bool true if the command exists, false otherwise
*
* @api
*/
@@ -674,8 +674,8 @@ class Application
/**
* Returns a text representation of the Application.
*
- * @param string $namespace An optional namespace name
- * @param bool $raw Whether to return raw command list
+ * @param string $namespace An optional namespace name
+ * @param bool $raw Whether to return raw command list
*
* @return string A string representing the Application
*
@@ -691,8 +691,8 @@ class Application
/**
* Returns an XML representation of the Application.
*
- * @param string $namespace An optional namespace name
- * @param bool $asDom Whether to return a DOM or an XML string
+ * @param string $namespace An optional namespace name
+ * @param bool $asDom Whether to return a DOM or an XML string
*
* @return string|\DOMDocument An XML string representing the Application
*
@@ -708,34 +708,22 @@ class Application
/**
* Renders a caught exception.
*
- * @param \Exception $e An exception instance
+ * @param \Exception $e An exception instance
* @param OutputInterface $output An OutputInterface instance
*/
public function renderException($e, $output)
{
- $strlen = function ($string) {
- if (!function_exists('mb_strlen')) {
- return strlen($string);
- }
-
- if (false === $encoding = mb_detect_encoding($string)) {
- return strlen($string);
- }
-
- return mb_strlen($string, $encoding);
- };
-
do {
$title = sprintf(' [%s] ', get_class($e));
- $len = $strlen($title);
+ $len = $this->stringWidth($title);
// HHVM only accepts 32 bits integer in str_split, even when PHP_INT_MAX is a 64 bit integer: https://github.com/facebook/hhvm/issues/1327
$width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : (defined('HHVM_VERSION') ? 1 << 31 : PHP_INT_MAX);
$formatter = $output->getFormatter();
$lines = array();
foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) {
- foreach (str_split($line, $width - 4) as $line) {
+ foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
// pre-format lines to get the right string length
- $lineLength = $strlen(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4;
+ $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4;
$lines[] = array($line, $lineLength);
$len = max($lineLength, $len);
@@ -744,7 +732,7 @@ class Application
$messages = array('', '');
$messages[] = $emptyLine = $formatter->format(sprintf('%s', str_repeat(' ', $len)));
- $messages[] = $formatter->format(sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $strlen($title)))));
+ $messages[] = $formatter->format(sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))));
foreach ($lines as $line) {
$messages[] = $formatter->format(sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1])));
}
@@ -890,7 +878,7 @@ class Application
* @param InputInterface $input An Input instance
* @param OutputInterface $output An Output instance
*
- * @return integer 0 if everything went fine, or an error code
+ * @return int 0 if everything went fine, or an error code
*/
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
{
@@ -1125,4 +1113,53 @@ class Application
return array_keys($alternatives);
}
+
+ private function stringWidth($string)
+ {
+ if (!function_exists('mb_strwidth')) {
+ return strlen($string);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return strlen($string);
+ }
+
+ return mb_strwidth($string, $encoding);
+ }
+
+ private function splitStringByWidth($string, $width)
+ {
+ // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
+ // additionally, array_slice() is not enough as some character has doubled width.
+ // we need a function to split string not by character count but by string width
+
+ if (!function_exists('mb_strwidth')) {
+ return str_split($string, $width);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return str_split($string, $width);
+ }
+
+ $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
+ $lines = array();
+ $line = '';
+ foreach (preg_split('//u', $utf8String) as $char) {
+ // test if $char could be appended to current line
+ if (mb_strwidth($line.$char) <= $width) {
+ $line .= $char;
+ continue;
+ }
+ // if not, push current line to array and make new line
+ $lines[] = str_pad($line, $width);
+ $line = $char;
+ }
+ if (strlen($line)) {
+ $lines[] = count($lines) ? str_pad($line, $width) : $line;
+ }
+
+ mb_convert_variables($encoding, 'utf8', $lines);
+
+ return $lines;
+ }
}
diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php
index 534b9f4319..b2a8389fa0 100644
--- a/src/Symfony/Component/Console/Helper/Helper.php
+++ b/src/Symfony/Component/Console/Helper/Helper.php
@@ -45,11 +45,11 @@ abstract class Helper implements HelperInterface
*
* @param string $string The string to check its length
*
- * @return integer The length of the string
+ * @return int The length of the string
*/
protected function strlen($string)
{
- if (!function_exists('mb_strlen')) {
+ if (!function_exists('mb_strwidth')) {
return strlen($string);
}
@@ -57,6 +57,6 @@ abstract class Helper implements HelperInterface
return strlen($string);
}
- return mb_strlen($string, $encoding);
+ return mb_strwidth($string, $encoding);
}
}
diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php
index 10dcb30951..0965d24ad5 100644
--- a/src/Symfony/Component/Console/Tests/ApplicationTest.php
+++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php
@@ -469,6 +469,33 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
}
+ public function testRenderExceptionWithDoubleWidthCharacters()
+ {
+ $application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth'));
+ $application->setAutoExit(false);
+ $application->expects($this->any())
+ ->method('getTerminalWidth')
+ ->will($this->returnValue(120));
+ $application->register('foo')->setCode(function () {throw new \Exception('エラーメッセージ');});
+ $tester = new ApplicationTester($application);
+
+ $tester->run(array('command' => 'foo'), array('decorated' => false));
+ $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getDisplay(true), '->renderException() renderes a pretty exceptions with previous exceptions');
+
+ $tester->run(array('command' => 'foo'), array('decorated' => true));
+ $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getDisplay(true), '->renderException() renderes a pretty exceptions with previous exceptions');
+
+ $application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth'));
+ $application->setAutoExit(false);
+ $application->expects($this->any())
+ ->method('getTerminalWidth')
+ ->will($this->returnValue(32));
+ $application->register('foo')->setCode(function () {throw new \Exception('コマンドの実行中にエラーが発生しました。');});
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'), array('decorated' => false));
+ $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
+ }
+
public function testRun()
{
$application = new Application();
diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1.txt
new file mode 100644
index 0000000000..6a98660364
--- /dev/null
+++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1.txt
@@ -0,0 +1,11 @@
+
+
+
+ [Exception]
+ エラーメッセージ
+
+
+
+foo
+
+
diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt
new file mode 100644
index 0000000000..c68a60f564
--- /dev/null
+++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt
@@ -0,0 +1,11 @@
+
+
+[37;41m [0m
+[37;41m [Exception] [0m
+[37;41m エラーメッセージ [0m
+[37;41m [0m
+
+
+[32mfoo[0m
+
+
diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth2.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth2.txt
new file mode 100644
index 0000000000..545cd7b0b4
--- /dev/null
+++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth2.txt
@@ -0,0 +1,12 @@
+
+
+
+ [Exception]
+ コマンドの実行中にエラーが
+ 発生しました。
+
+
+
+foo
+
+
diff --git a/src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php
index 87a45e1d44..34d70d8fc2 100644
--- a/src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php
+++ b/src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php
@@ -69,6 +69,21 @@ class FormatterHelperTest extends \PHPUnit_Framework_TestCase
);
}
+ public function testFormatBlockWithDoubleWidthDiacriticLetters()
+ {
+ if (!extension_loaded('mbstring')) {
+ $this->markTestSkipped('This test requires mbstring to work.');
+ }
+ $formatter = new FormatterHelper();
+ $this->assertEquals(
+ ' '."\n" .
+ ' 表示するテキスト '."\n" .
+ ' ',
+ $formatter->formatBlock('表示するテキスト', 'error', true),
+ '::formatBlock() formats a message in a block'
+ );
+ }
+
public function testFormatBlockLGEscaping()
{
$formatter = new FormatterHelper();