Introducing Assetic
  Asset 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 that
encourage 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 frontend
     can 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              •   Packer

•   Compass Framework         •   SASS

•   CSSEmbed                  •   Sprockets

•   Google Closure Compiler   •   Stylus

•   JSMin                     •   YUI Compressor

•   LESS
The ones written in PHP…
The ones written in PHP…
This is a difficult problem
Assetic makes it easy
as•cet•i•cism
describes 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();many files into one == fewer HTTP requests
         Merge
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();
# /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();
   Compress the merged asset == less data over the wire

header('Content-Type: text/javascript');
echo $core->dump();
<script src="js/core.php"></script>
Assetic is
Assets & 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
Filter
Filter
Asset
Load
   Filter
   Filter
   Asset
Filter
Filter
Asset




         Dump
Filter
Filter
Asset
Asset Collection
Filter              Filter
Filter              Filter
Asset               Asset
Asset Collection   Filter
                                 Filter
     Asset Collection            Asset
Filter              Filter
Filter              Filter
Asset               Asset
                                 Filter
                                 Filter
                                 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(),
));
      Load is implied
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'),
));

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'),
));
     Lazy! The filesystem isn't touched until now
header('Content-Type: text/css');
echo $styles->dump();
Basic Asset Classes

•   AssetCollection

•   AssetReference

•   FileAsset

•   GlobAsset

•   StringAsset
Core Filter Classes
•   CallablesFilter                   •   SassScssFilter

•   CoffeeScriptFilter                •   SprocketsFilter

•   CssRewriteFilter                  •   StylusFilter

•   GoogleClosureCompilerApiFilter   •   YuiCssCompressorFilter

•   GoogleClosureCompilerJarFilter   •   YuiJsCompressorFilter

•   LessFilter                        •   More to come…

•   SassSassFilter
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
       $core = new AssetCollection(array(
           $jquery,
           $plugin1,
           $plugin2,
       ));

       header('text/javascript');
       echo $core->dump();
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

   $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'));
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 compressed
   Javascript sucks
Mark filters for omission
in 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'));
          Run the filters once and cache the content

echo $styles->dump();
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 the
 Lazy 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 Thought
Assets 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 Issue
Assets defined in the view layer
must actually exist somewhere
Option Number Bad
Lazily dump assets to the
      web directory
Option Number Good
Eagerly dump assets to the
      web directory
A template is a configuration file
Formula Loaders
extract 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');
               Expensive every time
$writer = new AssetWriter('/path/to/web');
$writer->writeManagerAssets($am);
$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>
Each "leaf" asset is referenced individually

<script src="js/92429d8_1.js"></script>
<script src="js/92429d8_2.js"></script>
<script src="js/92429d8_3.js"></script>
AsseticBundle
  Symfony2 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 --watch
  Dump static assets in the background as you develop
Questions?
http://github.com/kriswallsmith/assetic

Assetic (Symfony Live Paris)