Thursday, March 29, 2012

Using Packages with the clientScript Component

With the advent of Yii 1.1.10 the package manager of the CClientScript became even more powerful with the ability to dynamically add packages to the list and chain the return value.

If you're not familiar with using packages, I will run through it very briefly. The Class reference for the CClientScript can be found here: http://www.yiiframework.com/doc/api/1.1/CClientScript

Most people who have used Yii for a while are quite familiar with using the clientScript component to dynamically add CSS and Javascript files to the page headers, so lets start there.



For example, lets say I have a CSS file and a Javascript file that I use whenever I have a form based page and for one reason or another I don't want to use a form specific layout.

Scenario 1, static files in public web root:
Yii::app()->clientScript->registerCSSFile( '/myassets/css/form_page_only.css' );
Yii::app()->clientScript->registerScriptFile( '/myassets/js/form_page_only.js' );

But what if the assets are in a widget or extension and need to be published, rather than simply referenced?

Scenario 2, using the http://www.yiiframework.com/doc/api/1.1/CAssetManager to publish files from :
$assetUrl = Yii::app()->assetManager->publish('/path/to/my/library/');

Yii::app()->clientScript->registerCSSFile( $assetUrl.'/css/form_page_only.css' );
Yii::app()->clientScript->registerScriptFile( $assetUrl.'/js/form_page_only.js' );

Ugh, now we've mixed in another class to learn and another line to include on each of the form views. There has to be a better way, right? Right!

Welcome to packages!

Packages let us group assets and asset paths together, ensuring that all the associated and required files get included with a single keyword. The syntax is quite simple, as most things in Yii, it's a simple keyed array.

The packages for scenario one and two would like something like this, in the main config file:
'components'=>array(
   ...
   'clientScript'=>array(
       'packages' => array(
          'scenario1'=>array(
             // pass a baseUrl because we don't need to publish 
             'baseUrl'=> '/myassets/',   
             'css'    => array( 'css/form_page_only.css' ),
             'js'     => array( 'js/form_page_only.js' ),
          ),
          'scenario2'=>array(
              // note the use of the Yii path alias format
             'basePath'=> 'path.to.my.library', 
             'css'    => array( 'css/form_page_only.css' ),
             'js'     => array( 'js/form_page_only.js' ),
          ),
       ),
    ),
);


Now, we can consistently and efficiently include the files with a single line.

In Scenario 1 form:
Yii::app()->clientScript->registerPackage('scenario1');

Or in Scenario 2 form:
Yii::app()->clientScript->registerPackage('scenario2');

Note that we're not longer having to call the assestManager directly to get the files published first, the package knows what it needs to do.
The package will publish the asset path if necessary and then register the listed css and js files.

If you have images assets that you need published as well, they will be published as long as they are in the basePath.

As you can see, this clearly makes our code more DRY and ensures that should a change in the file name or path need to be made, it can be made in a single centralized location.


It is also possible to use package definitions that you have not declared in the main config file.

As of Yii 1.1.10 you can now use the addPackage() method of the CClientScript to dynamically add a package.

So, lets take this possible scenario that would have been called without packages, using chaining rather than calling Yii::app()->clientScript directly each time, and assuming I have the following file structure:
/my/path/to/obscurefiles/
/my/path/to/obscurefiles/css/...(various css files here)
/my/path/to/obscurefiles/js/...(various js files here)
/my/path/to/obscurefiles/images/...(various images here)

$assetUrl = Yii::app()->assetManager->publish('/my/path/to/obscurefiles/');

Yii::app()->clientScript->registerCSSFile( $assetUrl.'/css/cssFile1.css')
                        ->registerCssFile( $assetUrl.'/css/cssFile2.css')
                        ->registerScriptFile( $assetUrl.'/js/javascriptfile1.js')
                        ->registerScriptFile( $assetUrl.'/js/javascriptfile2.js')
                        ->registerCoreSript('jquery');

echo CHtml::image( $assetUrl.'/images/publishedAsset1.png' );


That's a lot of repetition, and not very friendly to read. With Yii 1.1.10 we can refactor that code into something that's a little more clear:
// Assume that I use /my/path/to a lot since it's where I hide all my files that
// I don't want on the web root, so I have a Yii alias for it of 'mypath'
// Otherwise I can certainly add it here via
// Yii::setPathOfAlias('mypath', '/my/path/to');

// We use the 'depends' parameter to list the core or custom packages that
// should be included whenever this package is called.

$myPackage = array(
   'basePath'=> 'mypath.obscurefiles', 
   'css'     => array( 'css/cssFile1.css', 'css/cssFile2.css' ),
   'js'      => array( 'js/javascriptfile1.js','js/javascriptfile2.js' ),
   'depends' => array('jquery')
);

// This one line (wrapped here for clarity) will add the package, register it 
// to the page and return the URL of the assets directory where it was published.
$assetUrl = Yii::app()->clientScript
                      ->addPackage('myPack', $myPackage)
                      ->registerPackage('myPack')
                      ->getPackageBaseUrl('myPack');

echo CHtml::image( $assetUrl.'/images/publishedAsset1.png' );


NOTE: If I'm not using the $assetUrl within the page for anything, I can skip appending the getPackageBaseUrl() call to the end of the chain and forgo the assignment to $assetUrl entirely, like so:
Yii::app()->clientScript->addPackage('myPack', $myPackage)->registerPackage('myPack');
And if you later do need to access the assetUrl for that package, you can still get the url path by simply calling Yii::app()->clientScript->getPackageBaseUrl('myPack');

Pre-Yii 1.1.10 you could accomplish this by manually tweaking the packages array of the clientScript before calling register package, but it's a lot more 'hands on' to do so, and cannot be chained:
Yii::app()->clientScript->packages['myPack'] = $myPackage;
Yii::app()->clientScript->registerPackage('myPack');

It should be noted that the "depends" array can contain your custom package names, it is NOT restricted to core Yii packages. It will check your package list first, then fall back to the Yii core, the same way registerPackage does.

Therefor, it would be quite possible to have a package such as:
'packages'=>array(
   'myPackage'=>array(...),
   'myPackage2'=>array(
       ...
       'depends'=>array('myPackage'),
    ),
    'myPackage3'=>array(
       ...
       'depends'=>array('myPackage2, jquery-ui'),
    )
)

I hope that helps to shed some light on the use of packages and maintainability. In my opinion, it's another one of Yii's hidden treasures -- you could go years without using it, but why put yourself through that? ;)