Introducing AsseticAsset Management for PHP 5.3
March 4, 2011
@kriswallsmith
• Symfony Guru at
• Symfony core team member
• Doctrine contributor
• 10+ years experience with PHP and web development
• Open source evangelist and international speaker
OpenSky connects you with innovators, trendsetters and tastemakers. You choose
the ones you like and each week they invite you to their private online sales.
OpenSky connects you with innovators, trendsetters and tastemakers. You choose
the ones you like and each week they invite you to their private online sales.
ShopOpenSky.com
• PHP 5.3 + Symfony2
• MongoDB + Doctrine MongoDB ODM
• MySQL + Doctrine2 ORM
• Less CSS
• jQuery
Symfony2 is FAST
But you can still f*** that up
We build tools thatencourage best practices
Best practices like…
• Dependency injection (DI)
• Proper caching, edge side includes (ESI)
• Test-driven development (TDD)
• Don't repeat yourself (DRY)
• Keep it simple, SVP (KISS)
• Performance
If you haven’t optimized your frontend, you haven’t optimized
Get your assets in line.
A poorly optimized frontendcan destroy UX
…and SEO!
http://googlewebmastercentral.blogspot.com/2010/04/using-site-speed-in-web-search-ranking.html
Asset Management
Lots of awesome tools:
Lots of awesome tools:• CoffeeScript
• Compass Framework
• CSSEmbed
• Google Closure Compiler
• JSMin
• LESS
• Packer
• SASS
• Sprockets
• Stylus
• YUI Compressor
The ones written in PHP…
The ones written in PHP…
This is a difficult problem
Assetic makes it easy
as•cet•i•cismdescribes a lifestyle characterized by abstinence from various sorts of worldly
pleasures often with the aim of pursuing religious and spiritual goals
No B.S.
Enough talk
# /path/to/web/js/core.php
$core = new FileAsset('/path/to/jquery.js');$core->load();
header('Content-Type: text/javascript');echo $core->dump();
# /path/to/web/js/core.php
$core = new AssetCollection(array( new FileAsset('/path/to/jquery.js'), new GlobAsset('/path/to/js/core/*.js'),));$core->load();
header('Content-Type: text/javascript');echo $core->dump();
# /path/to/web/js/core.php
$core = new AssetCollection(array( new FileAsset('/path/to/jquery.js'), new GlobAsset('/path/to/js/core/*.js'),));$core->load();
header('Content-Type: text/javascript');echo $core->dump();
Merge many files into one == fewer HTTP requests
# /path/to/web/js/core.php
$core = new AssetCollection(array( new FileAsset('/path/to/jquery.js'), new GlobAsset('/path/to/js/core/*.js'),), array( new YuiCompressorJsFilter('/path/to/yui.jar'),));$core->load();
header('Content-Type: text/javascript');echo $core->dump();
# /path/to/web/js/core.php
$core = new AssetCollection(array( new FileAsset('/path/to/jquery.js'), new GlobAsset('/path/to/js/core/*.js'),), array( new YuiCompressorJsFilter('/path/to/yui.jar'),));$core->load();
header('Content-Type: text/javascript');echo $core->dump();
Compress the merged asset == less data over the wire
<script src="js/core.php"></script>
Assetic isAssets & Filters
Inspired by Python’s webassets
https://github.com/miracle2k/webassets
Assets have lazy, mutable content
A filter acts on an asset’s contents during “load” and “dump”
Assets can be gathered in collections
A collection is an asset
Asset
Filter
Asset
FilterFilter
Asset
FilterFilter
Asset
Load
FilterFilter
Asset
Dum
p
FilterFilter
Asset
Asset Collection
FilterFilter
Asset
FilterFilter
Asset
Asset Collection
Asset Collection
FilterFilter
Asset
FilterFilter
Asset
FilterFilter
Asset
FilterFilter
Asset
# /path/to/web/css/styles.php
$styles = new FileAsset('/path/to/main.sass', array( new SassFilter(),));
header('Content-Type: text/css');echo $styles->dump();
# /path/to/web/css/styles.php
$styles = new FileAsset('/path/to/main.sass', array( new SassFilter(),));
header('Content-Type: text/css');echo $styles->dump();
Load is implied
# /path/to/web/css/styles.php
$styles = new AssetCollection(array( new FileAsset('/path/to/main.sass', array( new SassFilter(), )), new FileAsset('/path/to/more.css'),));
header('Content-Type: text/css');echo $styles->dump();
# /path/to/web/css/styles.php
$styles = new AssetCollection(array( new FileAsset('/path/to/main.sass', array( new SassFilter(), )), new FileAsset('/path/to/more.css'),), array( new YuiCompressorCss('/path/to/yui.jar'),));
header('Content-Type: text/css');echo $styles->dump();
# /path/to/web/css/styles.php
$styles = new AssetCollection(array( new FileAsset('/path/to/main.sass', array( new SassFilter(), )), new FileAsset('/path/to/more.css'),), array( new YuiCompressorCss('/path/to/yui.jar'),));
header('Content-Type: text/css');echo $styles->dump();
Lazy! The filesystem isn't touched until now
Basic Asset Classes
• AssetCollection
• AssetReference
• FileAsset
• GlobAsset
• StringAsset
Core Filter Classes• CallablesFilter
• CoffeeScriptFilter
• CssRewriteFilter
• GoogleClosure\CompilerApiFilter
• GoogleClosure\CompilerJarFilter
• LessFilter
• Sass\SassFilter
• Sass\ScssFilter
• SprocketsFilter
• StylusFilter
• Yui\CssCompressorFilter
• Yui\JsCompressorFilter
• More to come…
Asset Manager
$am = new AssetManager();$am->set('jquery', new FileAsset('/path/to/jquery.js'));
$plugin = new AssetCollection(array( new AssetReference($am, 'jquery'), new FileAsset('/path/to/jquery.plugin.js'),));
$core = new AssetCollection(array( $jquery, $plugin1, $plugin2,));
header('text/javascript');echo $core->dump();
$core = new AssetCollection(array( $jquery, $plugin1, $plugin2,));
header('text/javascript');echo $core->dump();
jQuery will only be included once
Filter Manager
$yui = new YuiCompressorJs();$yui->setNomunge(true);
$fm = new FilterManager();$fm->set('yui_js', $yui);
$jquery = new FileAsset('/path/to/core.js');$jquery->ensureFilter($fm->get('yui_js'));
$core = new AssetCollection(array( $jquery, new GlobAsset('/path/to/js/core/*.js'),));$core->ensureFilter($fm->get('yui_js'));
$jquery = new FileAsset('/path/to/core.js');$jquery->ensureFilter($fm->get('yui_js'));
$core = new AssetCollection(array( $jquery, new GlobAsset('/path/to/js/core/*.js'),));$core->ensureFilter($fm->get('yui_js'));
jQuery will only be compressed once
Asset Factory
$fm = new FilterManager();$fm->set('coffee', new CoffeeScriptFilter());$fm->set('closure', new ClosureFilter());
$factory = new AssetFactory('/path/to/web');$factory->setFilterManager($fm);
$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', 'closure'));
header('Content-Type: text/javascript');echo $asset->dump();
Debug Mode
Debugging compressedJavascript sucks
Mark filters for omissionin debug mode using a “?”
// new AssetFactory('/path/to/web', $debug = true);
$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', 'closure'));
header('Content-Type: text/javascript');echo $asset->dump();
// new AssetFactory('/path/to/web', true);
$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', '?closure'));
header('Content-Type: text/javascript');echo $asset->dump();
// new AssetFactory('/path/to/web', false);
$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', '?closure'), array('debug' => true));
header('Content-Type: text/javascript');echo $asset->dump();
Good: Basic Caching
# /path/to/web/css/styles.php
$styles = new AssetCollection( array(new FileAsset('/path/to/main.sass')), array(new SassFilter()));
echo $styles->dump();
# /path/to/web/css/styles.php
$styles = new AssetCache(new AssetCollection( array(new FileAsset('/path/to/main.sass')), array(new SassFilter())), new FilesystemCache('/path/to/cache'));
echo $styles->dump();
# /path/to/web/css/styles.php
$styles = new AssetCache(new AssetCollection( array(new FileAsset('/path/to/main.sass')), array(new SassFilter())), new FilesystemCache('/path/to/cache'));
echo $styles->dump();
Run the filters once and cache the content
Better: HTTP Caching
// $core = new AssetCache(...
$mtime = gmdate('D, d M y H:i:s', $core->getLastModified()).' GMT';
if ($mtime == $_SERVER['HTTP_IF_MODIFIED_SINCE']) { header('HTTP/1.0 304 Not Modified'); exit();}
header('Content-Type: text/javascript');header('Last-Modified: '.$mtime);echo $core->dump();
Best: Static Assets
# /path/to/scripts/dump_assets.php
$am = new AssetManager();$am->set('foo', $foo);// etc...
$writer = new AssetWriter('/path/to/web');$writer->writeManagerAssets($am);
Best-est:Content Distribution Network
new AssetWriter('s3://my-bucket')
new AssetWriter('s3://my-bucket')
A CloudFront S3 bucket
Custom Stream Wrappers
$s3 = new Zend_Service_Amazon_S3($key, $secret);$s3->registerStreamWrapper();
Not Lazy Enough?
Asset Formulae and theLazy Asset Manager
$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', '?closure'), array('output' => 'js/*.js'));
$formula = array( array('js/src/*.coffee'), array('coffee', '?closure'), array('output' => 'js/*.js'));
$am = new LazyAssetManager($factory);$am->setFormula('core_js', $formula);
header('Content-Type: text/javascript');echo $am->get('core_js')->dump();
A ThoughtAssets are a part of the view layer
and should be defined there.
<!-- header.php -->
<?php foreach (assetic_javascripts( array('js/core.js', 'js/more.js'), array('?yui_js')) as $url): ?>
<script src="<?php echo $url ?>"></script>
<?php endforeach; ?>
An IssueAssets defined in the view layer must actually exist somewhere
Option Number BadLazily dump assets to the
web directory
Option Number GoodEagerly dump assets to the
web directory
A template is a configuration file
Formula Loadersextract asset formulae from templates
$loader = new FunctionCallsFormulaLoader();$resource = new DirectoryResource( '/path/to/templates', '/\.php$/');
$formulae = $loader->load($resource);
$am = new LazyAssetManager($factory);$am->setLoader('php', $loader);$am->addResource($resource, 'php');
$writer = new AssetWriter('/path/to/web');$writer->writeManagerAssets($am);
$am = new LazyAssetManager($factory);$am->setLoader('php', $loader);$am->addResource($resource, 'php');
$writer = new AssetWriter('/path/to/web');$writer->writeManagerAssets($am);
Expensive every time
$cache = new ConfigCache('/path/to/cache');
$loader = new CachedFormulaLoader( $loader, $cache, $debug);
$cache = new ConfigCache('/path/to/cache');
$loader = new CachedFormulaLoader( $loader, $cache, $debug);
Whether to stat each file for changes
Twig Integration
$twig->addExtension(new AsseticExtension($factory));
{% assetic 'js/*.coffee' filter='coffee' %}<script src="{{ asset_url }}"></script>{% endassetic %}
<script src="assets/92429d8"></script>
{% assetic 'js/*.coffee' filter='coffee' %}<script src="{{ asset_url }}"></script>{% endassetic %}
{% assetic 'js/*.coffee' filter='coffee' output='js/*.js' %}<script src="{{ asset_url }}"></script>{% endassetic %}
<script src="js/92429d8.js"></script>
{% assetic 'js/*.coffee' filter='coffee' output='js/*.js' %}<script src="{{ asset_url }}"></script>{% endassetic %}
{% javascripts 'js/*.coffee' filter='coffee,?closure' %}<script src="{{ asset_url }}"></script>{% endjavascripts %}
{% javascripts 'js/*.coffee' filter='coffee,?closure' %}<script src="{{ asset_url }}"></script>{% endjavascripts %}
Adds a default output string
{% javascripts 'js/*.coffee' filter='coffee,?closure' debug=true %}<script src="{{ asset_url }}"></script>{% endjavascripts %}
<script src="js/92429d8_1.js"></script><script src="js/92429d8_2.js"></script><script src="js/92429d8_3.js"></script>
<script src="js/92429d8_1.js"></script><script src="js/92429d8_2.js"></script><script src="js/92429d8_3.js"></script>
Each "leaf" asset is referenced individually
AsseticBundleSymfony2 integration
{% assetic filter='scss,?yui_css', output='css/all.css', '@MainBundle/Resources/sass/main.scss', '@AnotherBundle/Resources/sass/more.scss' %}<link href="{{ asset_url }}" rel="stylesheet" />{% endassetic %}
<link href="css/all.css" rel="stylesheet" />
Configuration
assetic: debug: %kernel.debug% use_controller: %kernel.debug% read_from: %kernel.root_dir%/../web write_to: s3://mybucket
{# when use_controller=true #}
<script src="{{ path('assetic_foo') }}"...
# routing_dev.yml_assetic: resource: . type: assetic
{# when use_controller=false #}
<script src="{{ asset('js/core.js') }}"></script>
{# when use_controller=false #}
<script src="{{ asset('js/core.js') }}"></script>
Lots for free
The Symfony2 Assets Helper
• Multiple asset domains
• Cache buster
framework: templating: assets_version: 1.2.3 assets_base_urls: - http://assets1.domain.com - http://assets2.domain.com - http://assets3.domain.com - http://assets4.domain.com
{% assetic filter='scss,?yui_css', output='css/all.css', '@MainBundle/Resources/sass/main.scss', '@AnotherBundle/Resources/sass/more.scss' %}<link href="{{ asset_url }}" rel="stylesheet" />{% endassetic %}
<link href="http://assets3.domain.com/css/all.css?1.2.3" ...
assetic:dump
$ php app/console assetic:dump web/
$ php app/console assetic:dump s3://my-bucket
assetic:dump --watchDump static assets in the background as you develop
Questions?
http://github.com/kriswallsmith/assetic