diff --git a/framework/yii/base/View.php b/framework/yii/base/View.php index df0b2b2..e3dcda8 100644 --- a/framework/yii/base/View.php +++ b/framework/yii/base/View.php @@ -142,11 +142,11 @@ class View extends Component */ public $dynamicPlaceholders = array(); /** - * @var array list of the registered asset bundles. The keys are the bundle names, and the values - * are booleans indicating whether the bundles have been registered. + * @var AssetBundle[] list of the registered asset bundles. The keys are the bundle names, and the values + * are the registered [[AssetBundle]] objects. * @see registerAssetBundle */ - public $assetBundles; + public $assetBundles = array(); /** * @var string the page title */ @@ -523,6 +523,9 @@ class View extends Component $this->trigger(self::EVENT_END_PAGE); $content = ob_get_clean(); + foreach(array_keys($this->assetBundles) as $bundle) { + $this->registerAssetFiles($bundle); + } echo strtr($content, array( self::PH_HEAD => $this->renderHeadHtml(), self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(), @@ -530,7 +533,6 @@ class View extends Component )); unset( - $this->assetBundles, $this->metaTags, $this->linkTags, $this->css, @@ -541,6 +543,24 @@ class View extends Component } /** + * Registers all files provided by an asset bundle including depending bundles files. + * Removes a bundle from [[assetBundles]] once registered. + * @param string $name name of the bundle to register + */ + private function registerAssetFiles($name) + { + if (!isset($this->assetBundles[$name])) { + return; + } + $bundle = $this->assetBundles[$name]; + foreach($bundle->depends as $dep) { + $this->registerAssetFiles($dep); + } + $bundle->registerAssets($this); + unset($this->assetBundles[$name]); + } + + /** * Marks the beginning of an HTML body section. */ public function beginBody() @@ -570,21 +590,44 @@ class View extends Component * Registers the named asset bundle. * All dependent asset bundles will be registered. * @param string $name the name of the asset bundle. + * @param integer|null $position if set, this forces a minimum position for javascript files. + * This will adjust depending assets javascript file position or fail if requirement can not be met. + * If this is null, asset bundles position settings will not be changed. + * See [[registerJsFile]] for more details on javascript position. * @return AssetBundle the registered asset bundle instance * @throws InvalidConfigException if the asset bundle does not exist or a circular dependency is detected */ - public function registerAssetBundle($name) + public function registerAssetBundle($name, $position = null) { if (!isset($this->assetBundles[$name])) { $am = $this->getAssetManager(); $bundle = $am->getBundle($name); $this->assetBundles[$name] = false; - $bundle->registerAssets($this); + // register dependencies + $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null; + foreach ($bundle->depends as $dep) { + $this->registerAssetBundle($dep, $pos); + } $this->assetBundles[$name] = $bundle; } elseif ($this->assetBundles[$name] === false) { throw new InvalidConfigException("A circular dependency is detected for bundle '$name'."); + } else { + $bundle = $this->assetBundles[$name]; + } + + if ($position !== null) { + $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null; + if ($pos === null) { + $bundle->jsOptions['position'] = $pos = $position; + } elseif ($pos > $position) { + throw new InvalidConfigException("An asset bundle that depends on '$name' has a higher javascript file position configured than '$name'."); + } + // update position for all dependencies + foreach ($bundle->depends as $dep) { + $this->registerAssetBundle($dep, $pos); + } } - return $this->assetBundles[$name]; + return $bundle; } /** diff --git a/framework/yii/web/AssetBundle.php b/framework/yii/web/AssetBundle.php index d324ef3..aa2d02b 100644 --- a/framework/yii/web/AssetBundle.php +++ b/framework/yii/web/AssetBundle.php @@ -130,7 +130,6 @@ class AssetBundle extends Object /** * Registers the CSS and JS files with the given view. - * This method will first register all dependent asset bundles. * It will then try to convert non-CSS or JS files (e.g. LESS, Sass) into the corresponding * CSS or JS files using [[AssetManager::converter|asset converter]]. * @param \yii\base\View $view the view that the asset files to be registered with. @@ -139,10 +138,6 @@ class AssetBundle extends Object */ public function registerAssets($view) { - foreach ($this->depends as $name) { - $view->registerAssetBundle($name); - } - $this->publish($view->getAssetManager()); foreach ($this->js as $js) { diff --git a/tests/unit/data/views/layout.php b/tests/unit/data/views/layout.php new file mode 100644 index 0000000..cd9d9d6 --- /dev/null +++ b/tests/unit/data/views/layout.php @@ -0,0 +1,22 @@ + +beginPage(); ?> + + + + Test + head(); ?> + + +beginBody(); ?> + + + +endBody(); ?> + + +endPage(); ?> \ No newline at end of file diff --git a/tests/unit/data/views/rawlayout.php b/tests/unit/data/views/rawlayout.php new file mode 100644 index 0000000..6864642 --- /dev/null +++ b/tests/unit/data/views/rawlayout.php @@ -0,0 +1,5 @@ +beginPage(); ?>1head(); ?>2beginBody(); ?>3endBody(); ?>4endPage(); ?> \ No newline at end of file diff --git a/tests/unit/data/views/simple.php b/tests/unit/data/views/simple.php new file mode 100644 index 0000000..437ba90 --- /dev/null +++ b/tests/unit/data/views/simple.php @@ -0,0 +1 @@ +This is a damn simple view file. \ No newline at end of file diff --git a/tests/unit/data/web/assets/.gitignore b/tests/unit/data/web/assets/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/tests/unit/data/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/tests/unit/framework/web/AssetBundleTest.php b/tests/unit/framework/web/AssetBundleTest.php new file mode 100644 index 0000000..ce33ce7 --- /dev/null +++ b/tests/unit/framework/web/AssetBundleTest.php @@ -0,0 +1,264 @@ + + */ + +namespace yiiunit\framework\web; + +use Yii; +use yii\base\View; +use yii\web\AssetBundle; +use yii\web\AssetManager; + +/** + * @group web + */ +class AssetBundleTest extends \yiiunit\TestCase +{ + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + + Yii::setAlias('@testWeb', '/'); + Yii::setAlias('@testWebRoot', '@yiiunit/data/web'); + } + + protected function getView() + { + $view = new View(); + $view->setAssetManager(new AssetManager(array( + 'basePath' => '@testWebRoot/assets', + 'baseUrl' => '@testWeb/assets', + ))); + + return $view; + } + + public function testRegister() + { + $view = $this->getView(); + + $this->assertEmpty($view->assetBundles); + TestSimpleAsset::register($view); + $this->assertEquals(1, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestSimpleAsset', $view->assetBundles); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestSimpleAsset'] instanceof AssetBundle); + + $expected = << +4 +EOF; + $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function testSimpleDependency() + { + $view = $this->getView(); + + $this->assertEmpty($view->assetBundles); + TestAssetBundle::register($view); + $this->assertEquals(3, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetBundle', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestJqueryAsset', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetLevel3', $view->assetBundles); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3'] instanceof AssetBundle); + + $expected = << +23 + +4 +EOF; + $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function positionProvider() + { + return array( + array(View::POS_HEAD, true), + array(View::POS_HEAD, false), + array(View::POS_BEGIN, true), + array(View::POS_BEGIN, false), + array(View::POS_END, true), + array(View::POS_END, false), + ); + } + + /** + * @dataProvider positionProvider + */ + public function testPositionDependency($pos, $jqAlreadyRegistered) + { + $view = $this->getView(); + + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestAssetBundle'] = array( + 'jsOptions' => array( + 'position' => $pos, + ), + ); + + $this->assertEmpty($view->assetBundles); + if ($jqAlreadyRegistered) { + TestJqueryAsset::register($view); + } + TestAssetBundle::register($view); + $this->assertEquals(3, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetBundle', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestJqueryAsset', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetLevel3', $view->assetBundles); + + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3'] instanceof AssetBundle); + + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle']->jsOptions['position']); + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset']->jsOptions['position']); + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3']->jsOptions['position']); + + switch($pos) + { + case View::POS_HEAD: + $expected = << + + +234 +EOF; + break; + case View::POS_BEGIN: + $expected = << +2 + +34 +EOF; + break; + default: + case View::POS_END: + $expected = << +23 + +4 +EOF; + break; + } + $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function positionProvider2() + { + return array( + array(View::POS_BEGIN, true), + array(View::POS_BEGIN, false), + array(View::POS_END, true), + array(View::POS_END, false), + ); + } + + /** + * @dataProvider positionProvider + */ + public function testPositionDependencyConflict($pos, $jqAlreadyRegistered) + { + $view = $this->getView(); + + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestAssetBundle'] = array( + 'jsOptions' => array( + 'position' => $pos - 1, + ), + ); + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestJqueryAsset'] = array( + 'jsOptions' => array( + 'position' => $pos, + ), + ); + + $this->assertEmpty($view->assetBundles); + if ($jqAlreadyRegistered) { + TestJqueryAsset::register($view); + } + $this->setExpectedException('yii\\base\\InvalidConfigException'); + TestAssetBundle::register($view); + } + + public function testCircularDependency() + { + $this->setExpectedException('yii\\base\\InvalidConfigException'); + TestAssetCircleA::register($this->getView()); + } +} + +class TestSimpleAsset extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = array( + 'jquery.js', + ); +} + +class TestAssetBundle extends AssetBundle +{ + public $basePath = '@testWebRoot/files'; + public $baseUrl = '@testWeb/files'; + public $css = array( + 'cssFile.css', + ); + public $js = array( + 'jsFile.js', + ); + public $depends = array( + 'yiiunit\\framework\\web\\TestJqueryAsset' + ); +} + +class TestJqueryAsset extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = array( + 'jquery.js', + ); + public $depends = array( + 'yiiunit\\framework\\web\\TestAssetLevel3' + ); +} + +class TestAssetLevel3 extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; +} + +class TestAssetCircleA extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = array( + 'jquery.js', + ); + public $depends = array( + 'yiiunit\\framework\\web\\TestAssetCircleB' + ); +} + +class TestAssetCircleB extends AssetBundle +{ + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = array( + 'jquery.js', + ); + public $depends = array( + 'yiiunit\\framework\\web\\TestAssetCircleA' + ); +} \ No newline at end of file