From a previous article: https://binarydodo.wordpress.com/2017/05/17/the-symfony-assetsinstall-command/, we have seen that we can place assets in bundles, and then when deploying need to arrange for them to be present in ‘web’.  One approach we saw is the ‘Resources/public’ convention and the related assets:install command.  We can also place assets directly in ‘web’ if they are not reusable in other applications.

From here we can just reference our assets directly, using the public location that they are stored in.  For example:

<img src="/images/banner.jpg" />
		<link rel="stylesheet" href="/css/mainstyle.css" />
<script src="/js/jquery.js" />

However, there are certain limitations to ‘hardcoding’ asset URLs like this in our templates.  The asset component tries to address these limitations.

As per the asset component documentation (http://symfony.com/doc/current/components/asset.html), hardcoding asset URLs can be a disadvantage because:

  • Templates get verbose: we have to write the full path for each asset in our templates
  • Versioning is difficult: adding a version (e.g. main.css?v=5) to the asset URLs is essential for some applications because it allows you to control how the assets are cached.
  • Moving assets location is cumbersome and error-prone: it requires you to carefully update the URLs of all assets included in all templates.
  • It’s nearly impossible to use multiple CDNs: this technique requires you to change the URL of the asset randomly for each request.

To understand versioning of assets, realise that HTTP clients like web browsers will normally cache things like images, CSS and javascripts.  Say we reference ‘main.css’ in our HTML.  The first time the browser encounters it, it will download a fresh copy from the server and store it in its cache.  From then on, whenever it will need ‘main.css’ for that domain, it may not request the server, but may use its cached copy.  What happens when on server side we have deployed a modified ‘main.css’?  The browser will not see the new file.  Since the client identifies a resource using its URL, if upon deployment we actually change the URL of the resource, the client will see it as a new resource to be downloaded, and will pull a fresh copy.  One strategy to change the URL is to append a version string to the resource URL, and change that number whenever the resource is modified.  As can be imagined, if we hardcode URLs in templates/HTML, we need to hunt down all URLs and modify the URLs manually.

The asset component tries to address all these limitations by providing a more flexible alternative.  Prior to symfony 2.7, the functionality provided by the asset component was provided by the templating component (http://symfony.com/blog/new-in-symfony-2-7-the-new-asset-component).

As every symfony component, the asset component is a decoupled PHP library.  It can be used in a PHP project that is not built around symfony.  The official component documentation page provides examples of how to use the component in a decoupled manner.  We need to create certain objects that need to be populated with information about how our assets stored.  Then, in our templates, we call methods on these objects to query them for URL strings.

However, most often than not, the asset component will be used in the context of a symfony application.  The symfony framework therefore makes provisions for making the use of the asset component very easy.  In particular, we need not ‘manually’ create the objects needed for the configuration; we use the assets section, under the framework section, in our application configuration, in order to configure everything about how our assets are stored.  In our templates, we use the twig functions ‘asset’ and ‘asset_version’ to generate URLs for our assets, based on the configuration.  These functions are extensions to twig, provided by the symfony framework, in the namespace ‘Symfony\Bridge\Twig\Extension’.

Semantics of the asset component

In this section, we review briefly the concepts behind the asset component, and how to use it.  For detailed information, refer to the official documentation (http://symfony.com/doc/current/components/asset.html).  All code examples below are replicated from there.

The Package class

A package is a logical grouping of assets that share certain similar properties, e.g., the same CDN host or the same base path.  A package is configured once with all information needed about the assets that it encompasses, and then it can be queried to generate URLs, through the getUrl() method, or to generate the version string associated with the package, through the getVersion() method.  A general package is represented by the Package class, which implements PackageInterface.  PackageInterface defines the getUrl() and getVersion() methods.

The package constructor needs a versioning strategy object as its first argument.  This should be an instance of VersionStrategyInterface.  When the package is asked to generate URLs, it will use this object to get a version string to append to the URL.  We can define our own custom versioning strategies by implementing VersionStrategyInterface, or we could use two default implementations that come with the asset component: EmptyVersionStrategy and StaticVersionStrategy.  The former is used to create packages for URLs that do not need versioning information, and the latter can be used to embed a static string into generated URLs.


use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy;

$package = new Package(new StaticVersionStrategy('v1'));

echo $package->getUrl('/image.png');
// result: /image.png?v1


// put the 'version' word before the version value
$package = new Package(new StaticVersionStrategy('v1', '%s?version=%s'));

echo $package->getUrl('/image.png');
// result: /image.png?version=v1

// put the asset version before its path
$package = new Package(new StaticVersionStrategy('v1', '%2$s/%1$s'));

echo $package->getUrl('/image.png');
// result: /v1/image.png

The PathPackage class

Some assets in our application will have the same base path.  For example, all images that are stored in the ‘images/gallery/flags’ folder under ‘web’ will be available through URLs starting with ‘/images/gallery/flags/’.  To avoid repeating this path in our templates, we can create a PathPackage with that base URL.  Then in our templates we need only specify the last part of the URL in order to generate the full URL.

 use Symfony\Component\Asset\PathPackage;
// ...

$package = new PathPackage('/static/images', new StaticVersionStrategy('v1'));

echo $package->getUrl('/logo.png');
// result: /static/images/logo.png?v1 

The PathPackage also accepts an optional third argument: a RequestContextStack object.  This object provides information about the current request to the package.  With the request context set, the PathPackage will prepend the current request base URL.  So, for example, if our entire site is hosted under the /somewhere directory of our web server root directory and the configured base path is /static/images, all paths will be prefixed with /somewhere/static/images.

To use the RequestContextStack however, we need to load the HttpFoundation component, because constructing a RequestContextStack requires a Symfony\Component\HttpFoundation\RequestStack object.  In a symfony application this is of course readily available.


use Symfony\Component\Asset\PathPackage;
use Symfony\Component\Asset\Context\RequestStackContext;
// ...

$package = new PathPackage(
 '/static/images',
 new StaticVersionStrategy('v1'),
 new RequestStackContext($requestStack)
);

echo $package->getUrl('/logo.png');
// result: /somewhere/static/images/logo.png?v1

The UrlPackage class

In our application we might also have assets that need to be loaded from external URLs, e.g., if we use CDNs.  Let’s say certain images need to be loaded from http://topcdn.com.  We can create a UrlPackage for that URL, so as to avoid repeating the URL in our templates.

use Symfony\Component\Asset\UrlPackage;

$package = new UrlPackage(
 'http://static.example.com/images/',
 new StaticVersionStrategy('v1')
);

echo $package->getUrl('/logo.png');
// result: http://static.example.com/images/logo.png?v1

If we configure more than 1 URL, one will be selected at random when getUrl() is called:

use Symfony\Component\Asset\UrlPackage;
// ...

$urls = array(
 'http://static1.example.com/images/',
 'http://static2.example.com/images/',
);
$package = new UrlPackage($urls, new StaticVersionStrategy('v1'));

echo $package->getUrl('/logo.png');
// result: http://static1.example.com/images/logo.png?v1
echo $package->getUrl('/icon.png');
// result: http://static2.example.com/images/logo.png?v1

The UrlPackage constructor also accepts a RequestContextObject, which is used to determine if the request is http or https, and in case there is a choice of configured URLs, select the one with the appropriate scheme.

The Packages class

We can ultimately have several Package objects in our application.  The Packages class can act as a collection for all these package objects.  It contains all packages, each of which is given a name.  When calling getUrl(), we specify the package name, and Packages class will delegate the call.  If we do not specify a package name, then Packages class also has a default package that it needs us to define.


use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\PathPackage;
use Symfony\Component\Asset\UrlPackage;
use Symfony\Component\Asset\Packages;
// ...

$versionStrategy = new StaticVersionStrategy('v1');

$defaultPackage = new Package($versionStrategy);

$namedPackages = array(
'img' => new UrlPackage('http://img.example.com/', $versionStrategy),
'doc' => new PathPackage('/somewhere/deep/for/documents', $versionStrategy),
);

$packages = new Packages($defaultPackage, $namedPackages)

echo $packages->getUrl('/main.css');
// result: /main.css?v1

echo $packages->getUrl('/logo.png', 'img');
// result: http://img.example.com/logo.png?v1

echo $packages->getUrl('/resume.pdf', 'doc');
// result: /somewhere/deep/for/documents/resume.pdf?v1

Asset component with symfony framework

As I mentioned earlier, when using the full symfony framework, there is no need to do things as we described in the code examples above.  The different packages can be defined through configuration files, and twig functions can be used in templates instead of calling the getUrl() method on packages.

Twig extension functions example:


{# Symfony 2.7 (do nothing, version is automatically appended) #}
{{ asset('logo.png') }}

{# use the asset_version() function if you need to output it manually #}
{{ asset_version('logo.png') }}

Assets are configured under the framework section in symfony configuration files.  See here for the full default config of the asset component: http://symfony.com/doc/current/reference/configuration/framework.html#assets.

For convenience, below are some example configurations that I am copy pasting from http://symfony.com/blog/new-in-symfony-2-7-the-new-asset-component:

# app/config/config.yml
framework:
....assets:
......version: 'v5'
......version_format: '%%s?version=%%s'
......packages:
........avatar:
..........base_path: /img/avatars
........doc:
..........base_path: /docs/pdf
..........version_format: '%2$s/%1$s'
{{ asset(user.uuid ~ '.png', 'avatar') }}
{# /img/avatars/1b0ae6a5-1c39-4b49-bcc1-2a787c8ec139.png?version=v5 #}

{{ asset('contracts/signup.pdf', 'doc') }}
{# /v5/docs/pdf/contracts/signup.pdf #}

Advertisements