diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 183ae83..2453b1c 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,12 +4,13 @@ Yii Framework 2 Change Log 2.0.36 under development ------------------------ +- Bug #18047: Fix colorization markers output in Table.php (cheeseq) - Bug #18028: Fix division by zero exception in Table.php::calculateRowHeight (fourhundredfour) - Enh #18019: Allow jQuery 3.5.0 to be installed (wouter90) - Bug #18026: Fix `ArrayHelper::getValue()` did not work with `ArrayAccess` objects (mikk150) - Enh #18048: Use `Instance::ensure()` to set `User::$accessChecker` (lav45) - Bug #18051: Fix missing support for custom validation method in EachValidator (bizley) -- Enh #17722: Add action injection support (SamMousa, samdark) +- Enh #17722: Add action injection support (SamMousa, samdark, erickskrauch) - Bug #18041: Fix RBAC migration for MSSQL (darkdef) - Bug #18081: Fix for PDO_DBLIB/MSSQL. Set flag ANSI_NULL_DFLT_ON to ON for current connect to DB (darkdef) - Bug #13828: Fix retrieving inserted data for a primary key of type uniqueidentifier for SQL Server 2005 or later (darkdef) @@ -20,6 +21,8 @@ Yii Framework 2 Change Log - Bug #18101: Fix behavior of OUTPUT INSERTED.* for SQL Server query: "insert default values"; correct MSSQL unit tests; turn off profiling echo message in migration test (darkdef) - Bug #18105: Fix for old trigger in RBAC migration with/without prefixTable (darkdef) - Enh #18120: Include path to the log file into error message if `FileTarget::export` fails (uaoleg) +- Enh #15202: Add optional param `--silent-exit-on-exception` in `yii\console\Controller` (egorrishe) +- Bug #18110: Add quotes to return value of viewName in MSSQL schema. It is `[someView]` now (darkdef) 2.0.35 May 02, 2020 diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 0b5fbff..b98135c 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -565,9 +565,12 @@ class Controller extends Component implements ViewContextInterface if (($component = $this->module->get($name, false)) instanceof $typeName) { $args[] = $component; $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; + } elseif ($this->module->has($typeName) && ($service = $this->module->get($typeName)) instanceof $typeName) { + $args[] = $service; + $requestedParams[$name] = 'Module ' . get_class($this->module) . " DI: $typeName \$$name"; } elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) { $args[] = $service; - $requestedParams[$name] = "DI: $typeName \$$name"; + $requestedParams[$name] = "Container DI: $typeName \$$name"; } elseif ($type->allowsNull()) { $args[] = null; $requestedParams[$name] = "Unavailable service: $name"; diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 27ee52d..eb892e5 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -41,6 +41,12 @@ abstract class ErrorHandler extends Component * @var \Exception|null the exception that is being handled currently. */ public $exception; + /** + * @var bool if TRUE - `handleException()` will finish script with `ExitCode::OK`. + * FALSE - `ExitCode::UNSPECIFIED_ERROR`. + * @since 2.0.36 + */ + public $silentExitOnException; /** * @var string Used to reserve memory for fatal error handler. @@ -56,6 +62,12 @@ abstract class ErrorHandler extends Component private $_registered = false; + public function init() + { + $this->silentExitOnException = $this->silentExitOnException !== null ? $this->silentExitOnException : YII_ENV_TEST; + parent::init(); + } + /** * Register this error handler. * @since 2.0.32 this will not do anything if the error handler was already registered @@ -121,7 +133,7 @@ abstract class ErrorHandler extends Component $this->clearOutput(); } $this->renderException($exception); - if (!YII_ENV_TEST) { + if (!$this->silentExitOnException) { \Yii::getLogger()->flush(true); if (defined('HHVM_VERSION')) { flush(); diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 7126a50..4026997 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -65,6 +65,13 @@ class Controller extends \yii\base\Controller * @since 2.0.10 */ public $help; + /** + * @var bool if TRUE - script finish with `ExitCode::OK` in case of exception. + * FALSE - `ExitCode::UNSPECIFIED_ERROR`. + * Default: `YII_ENV_TEST` + * @since 2.0.36 + */ + public $silentExitOnException; /** * @var array the options passed during execution. @@ -72,6 +79,14 @@ class Controller extends \yii\base\Controller private $_passedOptions = []; + public function beforeAction($action) + { + $silentExit = $this->silentExitOnException !== null ? $this->silentExitOnException : YII_ENV_TEST; + Yii::$app->errorHandler->silentExitOnException = $silentExit; + + return parent::beforeAction($action); + } + /** * Returns a value indicating whether ANSI color is enabled. * @@ -398,7 +413,7 @@ class Controller extends \yii\base\Controller public function options($actionID) { // $actionId might be used in subclasses to provide options specific to action id - return ['color', 'interactive', 'help']; + return ['color', 'interactive', 'help', 'silentExitOnException']; } /** diff --git a/framework/console/widgets/Table.php b/framework/console/widgets/Table.php index 3141b9c..b9938b1 100644 --- a/framework/console/widgets/Table.php +++ b/framework/console/widgets/Table.php @@ -241,36 +241,38 @@ class Table extends Widget $buffer = ''; $arrayPointer = []; $finalChunk = []; + $alreadyPrintedCells = []; for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) { $buffer .= $spanLeft . ' '; foreach ($size as $index => $cellSize) { $cell = isset($row[$index]) ? $row[$index] : null; $prefix = ''; + $chunk = ''; if ($index !== 0) { $buffer .= $spanMiddle . ' '; } if (is_array($cell)) { if (empty($finalChunk[$index])) { $finalChunk[$index] = ''; - $start = 0; $prefix = $this->listPrefix; if (!isset($arrayPointer[$index])) { $arrayPointer[$index] = 0; } - } else { - $start = mb_strwidth($finalChunk[$index], Yii::$app->charset); } - $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset); + $chunk = $cell[$arrayPointer[$index]]; $finalChunk[$index] .= $chunk; if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) { $arrayPointer[$index]++; $finalChunk[$index] = ''; } } else { - $chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset); + if (!isset($alreadyPrintedCells[$index])) { + $chunk = $cell; + } + $alreadyPrintedCells[$index] = true; } $chunk = $prefix . $chunk; - $repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1; + $repeat = $cellSize - Console::ansiStrwidth($chunk) - 1; $buffer .= $chunk; if ($repeat >= 0) { $buffer .= str_repeat(' ', $repeat); @@ -333,11 +335,9 @@ class Table extends Widget foreach ($columns as $column) { $columnWidth = max(array_map(function ($val) { if (is_array($val)) { - $encodings = array_fill(0, count($val), Yii::$app->charset); - return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->listPrefix, Yii::$app->charset); + return max(array_map('yii\helpers\Console::ansiStrwidth', $val)) + Console::ansiStrwidth($this->listPrefix); } - - return mb_strwidth($val, Yii::$app->charset); + return Console::ansiStrwidth($val); }, $column)) + 2; $this->columnWidths[] = $columnWidth; $totalWidth += $columnWidth; @@ -376,10 +376,9 @@ class Table extends Widget return $size == 2 || $columnWidth == 0 ? 0 : ceil($columnWidth / ($size - 2)); }, $this->columnWidths, array_map(function ($val) { if (is_array($val)) { - $encodings = array_fill(0, count($val), Yii::$app->charset); - return array_map('mb_strwidth', $val, $encodings); + return array_map('yii\helpers\Console::ansiStrwidth', $val); } - return mb_strwidth($val, Yii::$app->charset); + return Console::ansiStrwidth($val); }, $row)); return max($rowsPerCell); } diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index fdeb546..469e54e 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -595,7 +595,12 @@ WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW' ORDER BY [t].[table_name] SQL; - return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn(); + $views = $this->db->createCommand($sql, [':schema' => $schema])->queryColumn(); + $views = array_map(static function ($item) { + return '[' . $item . ']'; + }, $views); + + return $views; } /** diff --git a/framework/helpers/BaseConsole.php b/framework/helpers/BaseConsole.php index 8c3b3fb..e9e417e 100644 --- a/framework/helpers/BaseConsole.php +++ b/framework/helpers/BaseConsole.php @@ -7,6 +7,7 @@ namespace yii\helpers; +use Yii; use yii\console\Markdown as ConsoleMarkdown; use yii\base\Model; @@ -344,6 +345,17 @@ class BaseConsole } /** + * Returns the width of the string without ANSI color codes. + * @param string $string the string to measure + * @return int the width of the string not counting ANSI format characters + * @since 2.0.36 + */ + public static function ansiStrwidth($string) + { + return mb_strwidth(static::stripAnsiFormat($string), Yii::$app->charset); + } + + /** * Converts an ANSI formatted string to HTML. * * Note: xTerm 256 bit colors are currently not supported. diff --git a/tests/framework/console/ControllerTest.php b/tests/framework/console/ControllerTest.php index 0109279..cbe1b75 100644 --- a/tests/framework/console/ControllerTest.php +++ b/tests/framework/console/ControllerTest.php @@ -170,12 +170,35 @@ class ControllerTest extends TestCase $this->assertEquals('Component: yii\console\Request $request', \Yii::$app->requestedParams['request']); $this->assertEquals($params['between'], $args[2]); $this->assertInstanceOf(DummyService::className(), $args[3]); - $this->assertEquals('DI: yiiunit\framework\console\stubs\DummyService $dummyService', \Yii::$app->requestedParams['dummyService']); + $this->assertEquals('Container DI: yiiunit\framework\console\stubs\DummyService $dummyService', \Yii::$app->requestedParams['dummyService']); $this->assertNull($args[4]); $this->assertEquals('Unavailable service: post', \Yii::$app->requestedParams['post']); $this->assertEquals($params['after'], $args[5]); } + public function testInjectedActionParamsFromModule() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + $module = new \yii\base\Module('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $module->set('yii\data\DataProviderInterface', [ + 'class' => \yii\data\ArrayDataProvider::className(), + ]); + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', $module); + $this->mockWebApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionModuleServiceInjection'); + $args = $this->controller->bindActionParams($injectionAction, []); + $this->assertInstanceOf(\yii\data\ArrayDataProvider::className(), $args[0]); + $this->assertEquals('Module yii\base\Module DI: yii\data\DataProviderInterface $dataProvider', \Yii::$app->requestedParams['dataProvider']); + } + public function assertResponseStatus($status, $response) { $this->assertInstanceOf('yii\console\Response', $response); diff --git a/tests/framework/console/FakePhp71Controller.php b/tests/framework/console/FakePhp71Controller.php index 9b5ff60..985940d 100644 --- a/tests/framework/console/FakePhp71Controller.php +++ b/tests/framework/console/FakePhp71Controller.php @@ -7,6 +7,7 @@ namespace yiiunit\framework\console; +use yii\data\DataProviderInterface; use yiiunit\framework\console\stubs\DummyService; use yii\console\Controller; use yii\console\Request; @@ -21,4 +22,8 @@ class FakePhp71Controller extends Controller public function actionNullableInjection(?Request $request, ?Post $post) { } + + public function actionModuleServiceInjection(DataProviderInterface $dataProvider) + { + } } diff --git a/tests/framework/console/controllers/HelpControllerTest.php b/tests/framework/console/controllers/HelpControllerTest.php index 5c43a21..4307948 100644 --- a/tests/framework/console/controllers/HelpControllerTest.php +++ b/tests/framework/console/controllers/HelpControllerTest.php @@ -128,6 +128,7 @@ action:route to action --interactive: whether to run the command interactively. --color: whether to enable ANSI color in the output.If not set, ANSI color will only be enabled for terminals that support it. --help: whether to display help information about current command. +--silent-exit-on-exception: if TRUE - script finish with `ExitCode\:\:OK` in case of exception.FALSE - `ExitCode\:\:UNSPECIFIED_ERROR`.Default\: `YII_ENV_TEST` STRING , $result); diff --git a/tests/framework/console/widgets/TableTest.php b/tests/framework/console/widgets/TableTest.php index 5fcacfd..76335d1 100644 --- a/tests/framework/console/widgets/TableTest.php +++ b/tests/framework/console/widgets/TableTest.php @@ -8,6 +8,7 @@ namespace yiiunit\framework\console; use yii\console\widgets\Table; +use yii\helpers\Console; use yiiunit\TestCase; /** @@ -314,6 +315,56 @@ EXPECTED; ); } + public function testColorizedInput() + { + $table = new Table(); + + $expected = <<<"EXPECTED" +╔═══════╤═══════╤══════════╗ +║ test1 │ test2 │ test3 ║ +╟───────┼───────┼──────────╢ +║ col1 │ \e[33mcol2\e[0m │ col3 ║ +╟───────┼───────┼──────────╢ +║ col1 │ col2 │ • col3-0 ║ +║ │ │ • \e[31mcol3-1\e[0m ║ +║ │ │ • col3-2 ║ +╚═══════╧═══════╧══════════╝ + +EXPECTED; + + $this->assertEqualsWithoutLE( + $expected, + $table + ->setHeaders(['test1', 'test2', 'test3']) + ->setRows([ + ['col1', Console::renderColoredString('%ycol2%n'), 'col3'], + ['col1', 'col2', ['col3-0', Console::renderColoredString('%rcol3-1%n'), 'col3-2']], + ]) + ->run() + ); + } + + public function testColorizedInputStripsANSIMarkersInternally() + { + $table = new Table(); + + $table + ->setHeaders(['t1', 't2', 't3']) + ->setRows([ + ['col1', Console::renderColoredString('%ycol2%n'), 'col3'], + ['col1', 'col2', ['col3-0', Console::renderColoredString('%rcol3-1%n'), 'col3-2']], + ]) + ->setScreenWidth(200) + ->run(); + + $columnWidths = \PHPUnit_Framework_Assert::readAttribute($table, "columnWidths"); + + $this->assertArrayHasKey(1, $columnWidths); + $this->assertEquals(4+2, $columnWidths[1]); + $this->assertArrayHasKey(2, $columnWidths); + $this->assertEquals(8+2, $columnWidths[2]); + } + public function testCalculateRowHeightShouldNotThrowDivisionByZeroException() { $rows = [ diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index fd2048c..1ddc903 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -155,11 +155,43 @@ class ControllerTest extends TestCase $this->assertEquals('Component: yii\web\Request $request', \Yii::$app->requestedParams['request']); $this->assertEquals($params['between'], $args[2]); $this->assertInstanceOf(VendorImage::className(), $args[3]); - $this->assertEquals('DI: yiiunit\framework\web\stubs\VendorImage $vendorImage', \Yii::$app->requestedParams['vendorImage']); + $this->assertEquals('Container DI: yiiunit\framework\web\stubs\VendorImage $vendorImage', \Yii::$app->requestedParams['vendorImage']); $this->assertNull($args[4]); $this->assertEquals('Unavailable service: post', \Yii::$app->requestedParams['post']); $this->assertEquals($params['after'], $args[5]); } + + public function testInjectedActionParamsFromModule() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + $module = new \yii\base\Module('fake', new \yii\web\Application([ + 'id' => 'app', + 'basePath' => __DIR__, + + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + ], + ])); + $module->set('yii\data\DataProviderInterface', [ + 'class' => \yii\data\ArrayDataProvider::className(), + ]); + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', $module); + $this->mockWebApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionModuleServiceInjection'); + $args = $this->controller->bindActionParams($injectionAction, []); + $this->assertInstanceOf(\yii\data\ArrayDataProvider::className(), $args[0]); + $this->assertEquals('Module yii\base\Module DI: yii\data\DataProviderInterface $dataProvider', \Yii::$app->requestedParams['dataProvider']); + } + /** * @see https://github.com/yiisoft/yii2/issues/17701 */ diff --git a/tests/framework/web/FakePhp71Controller.php b/tests/framework/web/FakePhp71Controller.php index 9c67256..9c8a44b 100644 --- a/tests/framework/web/FakePhp71Controller.php +++ b/tests/framework/web/FakePhp71Controller.php @@ -7,6 +7,7 @@ namespace yiiunit\framework\web; +use yii\data\DataProviderInterface; use yii\web\Controller; use yii\web\Request; use yiiunit\framework\web\stubs\VendorImage; @@ -27,4 +28,8 @@ class FakePhp71Controller extends Controller public function actionNullableInjection(?Request $request, ?Post $post) { } + + public function actionModuleServiceInjection(DataProviderInterface $dataProvider) + { + } }