diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 0bea5c0a23..b1c46ca214 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -21,6 +21,7 @@ Yii Framework 2 Change Log - Enh #6975: Pressing arrows while focused in inputs of Active Form with `validateOnType` enabled no longer triggers validation (slinstj) - Enh #7488: Added `StringHelper::explode` to perform explode with trimming and skipping of empty elements (SilverFire, nineinchnick, creocoder, samdark) - Enh #7530: Improved default values for `yii\data\Sort` link labels in a `ListView` when used with an `ActiveDataProvider` (cebe) +- Enh #7539: `yii\console\controllers\AssetController` provides dependency trace in case bundle circular dependency detected (klimov-paul) - Enh #7562: `yii help` now lists all sub-commands by default (callmez) - Enh #7571: HTTP status 500 and "An internal server error occurred." are now returned in case there was an exception in layout and `YII_DEBUG` is false (samdark) - Enh #7636: `yii\web\Session::getHasSessionId()` uses a more lenient way to check if session ID is provided in URL (robsch) diff --git a/framework/console/controllers/AssetController.php b/framework/console/controllers/AssetController.php index fb50c2b402..f63c297e10 100644 --- a/framework/console/controllers/AssetController.php +++ b/framework/console/controllers/AssetController.php @@ -248,7 +248,7 @@ class AssetController extends Controller $this->loadDependency($dependencyBundle, $result); $result[$name] = $dependencyBundle; } elseif ($result[$name] === false) { - throw new Exception("A circular dependency is detected for bundle '$name'."); + throw new Exception("A circular dependency is detected for bundle '{$name}': " . $this->composeCircularDependencyTrace($name, $result) . "."); } } } @@ -423,9 +423,9 @@ class AssetController extends Controller $this->registerBundle($bundles, $depend, $registered); } unset($registered[$name]); - $registered[$name] = true; + $registered[$name] = $bundle; } elseif ($registered[$name] === false) { - throw new Exception("A circular dependency is detected for target '$name'."); + throw new Exception("A circular dependency is detected for target '{$name}': " . $this->composeCircularDependencyTrace($name, $registered) . "."); } } @@ -756,4 +756,26 @@ EOD; $config['class'] = get_class($bundle); return $config; } + + /** + * Composes trace info for bundle circular dependency. + * @param string $circularDependencyName name of the bundle, which have circular dependency + * @param array $registered list of bundles registered while detecting circular dependency. + * @return string bundle circular dependency trace string. + */ + private function composeCircularDependencyTrace($circularDependencyName, array $registered) + { + $dependencyTrace = []; + $startFound = false; + foreach ($registered as $name => $value) { + if ($name === $circularDependencyName) { + $startFound = true; + } + if ($startFound && $value === false) { + $dependencyTrace[] = $name; + } + } + $dependencyTrace[] = $circularDependencyName; + return implode(' -> ', $dependencyTrace); + } } diff --git a/tests/unit/framework/console/controllers/AssetControllerTest.php b/tests/unit/framework/console/controllers/AssetControllerTest.php index ae031b045e..8ef7532a6f 100644 --- a/tests/unit/framework/console/controllers/AssetControllerTest.php +++ b/tests/unit/framework/console/controllers/AssetControllerTest.php @@ -377,6 +377,61 @@ EOL; $this->assertContains($externalAssetBundleClassName, $compressedRegularAssetConfig['depends'], 'Dependency on external bundle is lost!'); } + /** + * @depends testActionCompress + * + * @see https://github.com/yiisoft/yii2/issues/7539 + */ + public function testDetectCircularDependency() + { + // Given : + $namespace = __NAMESPACE__; + + $this->declareAssetBundleClass([ + 'namespace' => $namespace, + 'class' => 'AssetStart', + 'depends' => [ + $namespace . '\AssetA' + ], + ]); + $this->declareAssetBundleClass([ + 'namespace' => $namespace, + 'class' => 'AssetA', + 'depends' => [ + $namespace . '\AssetB' + ], + ]); + $this->declareAssetBundleClass([ + 'namespace' => $namespace, + 'class' => 'AssetB', + 'depends' => [ + $namespace . '\AssetC' + ], + ]); + $this->declareAssetBundleClass([ + 'namespace' => $namespace, + 'class' => 'AssetC', + 'depends' => [ + $namespace . '\AssetA' + ], + ]); + + $bundles = [ + $namespace . '\AssetStart' + ]; + $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle.php'; + + $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; + $this->createCompressConfigFile($configFile, $bundles); + + // Assert : + $expectedExceptionMessage = ": {$namespace}\AssetA -> {$namespace}\AssetB -> {$namespace}\AssetC -> {$namespace}\AssetA"; + $this->setExpectedException('yii\console\Exception', $expectedExceptionMessage); + + // When : + $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); + } + /** * Data provider for [[testAdjustCssUrl()]]. * @return array test data.