Introducing Assetic: Asset Management for PHP 5.3

Post on 08-Sep-2014

60.513 views 0 download

Tags:

description

The performance of your application depends heavily on the number and size of assets on each page. Even your blazingly fastest Symfony2 application can be bogged down by bloated Javascript and CSS files. This session will give you a basic introduction to PHP's new asset management framework, Assetic, and explore how it integrates with Symfony2 for a pleasant, common sense developer experience.

Transcript of Introducing Assetic: Asset Management for PHP 5.3

Introducing AsseticAsset Management for PHP 5.3

Kris Wallsmith February 9, 2011

@kriswallsmith

• Symfony core team member

• Doctrine contributor

• Symfony Guru at

• 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.

ShopOpenSky.com

• PHP 5.3 + Symfony2

• MongoDB + Doctrine MongoDB ODM

• MySQL + Doctrine2 ORM

• Less CSS

• jQuery

Agenda

• Strawman

• The code

• Twig Integration

• Symfony2 Integration

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• CoffeeScript

• Compass Framework

• CSSEmbed

• Google Closure Compiler

• JSMin

• LESS

• Packer

• SASS

• Sprockets

• YUI Compressor

The ones written in PHP…

This is a difficult problem

Assetic makes it easy

Are you ready tokick some Assetic!?!

# /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();

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();

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

Filters act on asset contents during “load” and “dump”

Assets can be gathered in collections

A collection is an asset

FilterFilter

Asset

FilterFilter

Asset

Load

Dum

p

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 AssetCollection( array(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 AssetCollection(array( 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 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

• 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();

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 will only be compressed once

Asset Factory

# /path/to/asset_factory.php

$fm = new FilterManager();$fm->set('coffee', new CoffeeScriptFilter());$fm->set('closure', new GoogleClosure\CompilerApi());

$factory = new AssetFactory('/path/to/web');$factory->setAssetManager($am);$factory->setFilterManager($fm);

include '/path/to/asset_factory.php';

$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

// new AssetFactory('/path/to/web', true);

include '/path/to/asset_factory.php';

$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', 'closure'));

header('Content-Type: text/javascript');echo $asset->dump();

// new AssetFactory('/path/to/web', true);

include '/path/to/asset_factory.php';

$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', '?closure'));

header('Content-Type: text/javascript');echo $asset->dump();

// new AssetFactory('/path/to/web', false);

include '/path/to/asset_factory.php';

$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', '?closure'), array('debug' => true));

header('Content-Type: text/javascript');echo $asset->dump();

Factory Workers

Everything passes through the workers’ hands

$worker = new EnsureFilterWorker( '/\.css$/', // the output pattern $fm->get('yui_css'), // the filter false // the debug mode);

$factory = new AssetFactory('/path/to/web');$factory->addWorker($worker);

// compressed$factory->createAsset('css/sass/*', 'sass', array( 'output' => 'css',));

$worker = new EnsureFilterWorker( '/\.css$/', // the output pattern $fm->get('yui_css'), // the filter false // the debug mode);

$factory = new AssetFactory('/path/to/web');$factory->addWorker($worker);

// uncompressed$factory->createAsset('css/sass/*', 'sass', array( 'output' => 'css', 'debug' => true,));

Not Lazy Enough?

Asset Formulae and theLazy Asset Manager

$asset = $factory->createAsset( array('js/src/*.coffee'), array('coffee', '?closure'), array('output' => 'js'));

$formula = array( array('js/src/*.coffee'), array('coffee', '?closure'), array('output' => 'js'));

$am = new LazyAssetManager($factory);$am->setFormula('core_js', $formula);

header('Content-Type: text/javascript');echo $am->get('core_js')->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();

Run the filters once and cache the content

Better: HTTP Caching

# /path/to/web/js/core.php

$mtime = gmdate($core->getLastModified());

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);

# /path/to/scripts/dump_assets.php

$am = new AssetManager();$am->set('foo', $foo);// etc...

$writer = new AssetWriter('/path/to/web');foreach (array_slice($argv, 1) as $name) { $writer->writeAsset($am->get($name));}

Best-est:Content Distribution Network

new AssetWriter('s3://my-bucket')

A CloudFront S3 bucket

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' %}<script src="{{ asset_url }}"></script>{% endassetic %}

<script src="js/92429d8.js"></script>

{% assetic 'js/*.coffee', filter='coffee', output='js' %}<script src="{{ asset_url }}"></script>{% endassetic %}

{% assetic 'js/*.coffee', filter='coffee,?closure', output='js', name='core_js' %}<script src="{{ asset_url }}"></script>{% endassetic %}

Formula LoaderUses the Twig parser to extract asset formulae from templates

$loader = new FormulaLoader($twig);

// loop through your templates$formulae = array();foreach ($templates as $template) { $formulae += $loader->load($template);}

$am = new LazyAssetManager($factory);$am->addFormulae($formulae);

if (!file_exists($cache = '/path/to/formulae.php')) { $loader = new FormulaLoader($twig);

// loop through your templates $formulae = array(); foreach ($templates as $template) { $formulae += $loader->load($template); }

file_put_contents($cache, '<?php return '.var_export($formulae, true));}

$am = new LazyAssetManager($factory);$am->addFormulae(require $cache);

AsseticBundle

{% 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" />

{% 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 %}

{% assetic filter='scss,?yui_css', output='css/all.css', '@MainBundle/Resources/sass/main.scss', debug=true, '@AnotherBundle/Resources/sass/more.scss' %}<link href="{{ asset_url }}" rel="stylesheet" />{% endassetic %}

<link href="css/all_part1.css" rel="stylesheet" /><link href="css/all_part2.css" rel="stylesheet" />

Each "leaf" asset is referenced individually

Configuration

assetic.config: debug: %kernel.debug% use_controller: %kernel.debug% document_root: %kernel.root_dir%/../web

{# when use_controller=true #}

<script src="{{ path('route', { 'name': 'core_js' }) }}"...

# routing_dev.yml_assetic: resource: . type: assetic

{# when use_controller=false #}

<script src="{{ asset('js/core.js') }}"></script>

Lots for free

The Symfony2 Assets Helper

• Multiple asset domains

• Cache buster

app.config: 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

Register a stream wrapper in boot()

PHP templatesComing soon…

<?php foreach ($view['assetic']->urls( array('@MainBundle/Resources/sass/main.scss', '@AnotherBundle/Resources/sass/more.scss'), array('scss', '?yui_css'), array('output' => 'css/all.css')) as $url): ?> <link href="<?php echo $url ?>" rel="stylesheet" /><?php endforeach; ?>

Fork me!http://github.com/kriswallsmith/symfony-sandbox

What’s Next?

• Finish Symfony2 helpers for PHP templates

• Filter configuration

• Image sprites, embedded image data

• --watch commands

• Client-aware optimizations?

• Better CDN integration

Assetic is a killer feature of Symfony2…

…but is only one month old,so be nice :)

Questions?

Assetichttp://github.com/kriswallsmith/assetic