gubler/elixir-bundle
A Symfony bundle for integrating Laravel Elixir asset versioning. It provides a Twig helper that reads the Elixir manifest and returns cache-busted CSS and JavaScript paths, simplifying front-end asset management in Symfony applications.
gubler/elixir-bundle is a thin Symfony adapter around Laravel Elixir — the gulp-based asset pipeline that shipped with Laravel 5.0–5.3. If you're coming from Laravel, the mental model transfers almost 1:1: same gulpfile.js, same mix.sass()/mix.scripts()/mix.version() API, same elixir() helper name — except the helper is exposed to Twig instead of Blade, and paths resolve against your Symfony web/ (or public/) directory instead of public/.
Minimal steps
composer require gubler/elixir-bundle
Register the bundle (app/AppKernel.php on Symfony 2/3, or config/bundles.php on 4+):
new Gubler\ElixirBundle\GublerElixirBundle(),
Then bring your own Node toolchain — the bundle does not ship gulp or Elixir:
npm init -y
npm install --save-dev gulp@3 laravel-elixir
Create a gulpfile.js at the project root exactly as you would in Laravel:
var elixir = require('laravel-elixir');
elixir(function (mix) {
mix.sass('app.scss', 'web/css/app.css')
.scripts(['app.js', 'vendor.js'], 'web/js/app.js');
if (process.env.NODE_ENV === 'production') {
mix.version(['css/app.css', 'js/app.js']);
}
});
Where to look first
DependencyInjection/Configuration.php — the config keys (public_dir/build_dir/manifest path naming varies between releases; read the tree, don't guess). As of v0.3.0 there is also a prefix URL key (added for CDN/subdirectory hosting — verify the exact name in the config tree, it's not documented in the release note).Twig/ — this is where the elixir() function is defined and where the manifest is read. This is also where the new prefix is applied to the resolved URL.Resources/config/services.xml (or .yml) — the service IDs you'd override to customize path resolution.First use case
The whole point is cache-busted asset URLs in Twig without Assetic:
<link rel="stylesheet" href="{{ elixir('css/app.css') }}">
<script src="{{ elixir('js/app.js') }}"></script>
In dev (no manifest) this should return the plain path; in prod (after mix.version()) it returns /build/css/app-8f3a1c.css. With v0.3.0's prefix configured, that becomes https://cdn.example.com/build/css/app-8f3a1c.css.
Keep gulp out of the request cycle. The bundle is a resolver, not a compiler. Never shell out to gulp from a controller or a console command that runs in prod. Build in CI or at deploy time:
# .github/workflows/deploy.yml (sketch)
- run: npm ci
- run: NODE_ENV=production ./node_modules/.bin/gulp --production
- run: php bin/console cache:clear --env=prod
Layout-level helper with a dev fallback. Because the manifest only exists after a versioned build, template authors hit "manifest not found" constantly. Wrap it once:
{# templates/_assets.html.twig #}
{% macro asset(path) %}
{{ elixir(path) }}
{% endmacro %}
…or configure the bundle so a missing manifest falls back to the unversioned path (check whether your version supports a fallback/strict option — early versions simply threw).
Serve assets from a CDN or subdirectory with the new prefix option (v0.3.0). This is the headline change in the release and the most useful one since 2017: instead of overriding the Twig extension service just to swap a host, set the prefix config and let the helper prepend it.
# config/config.yml (name/path depends on Symfony version — check Configuration.php)
gubler_elixir:
public_dir: '%kernel.root_dir%/../web'
build_dir: '%kernel.root_dir%/../web/build'
prefix: 'https://cdn.example.com/assets'
Caveats: the prefix is a URL rewrite only — it does not copy files to the CDN and it does not change where the manifest is read from. Your deploy pipeline still needs to sync web/build/ to the CDN origin, and the prefix must be a plain string with no trailing-slash ambiguity (see Gotchas).
Mirror the Laravel directory shape. Everything in the bundle assumes Elixir's defaults: source in resources/assets/{sass,js}, output to public/build/..., manifest in public/build/rev-manifest.json. On Symfony that usually means mapping public_dir to web/ and build_dir to web/build. Don't fight the defaults — you'll spend an afternoon on path joins.
Concatenate, don't bundle. Elixir predates ES modules. The idiomatic workflow is mix.scripts([...]) to concatenate an ordered list of plain <script> files, and mix.styles([...]) for vendor CSS. Order matters and is the most common source of "works in dev, breaks in prod" bugs.
Versioning strategy. Only version files you actually reference from Twig. mix.version(['css/app.css', 'js/app.js']) writes hashed copies plus the manifest — the originals stay in place, so a stale manifest is silently served rather than 404ing.
Watch mode during development. Run gulp watch in one terminal alongside php bin/console server:run (or your Docker stack). Elixir's watch is file-polling-based and CPU-hungry on large trees; scope it with mix.sass('app.scss', ...) rather than directory-wide tasks.
Versioning without Elixir's manifest. If you need to keep using this bundle but want off gulp, the manifest is just JSON. Any build tool that emits {"css/app.css": "css/app-8f3a1c.css"} at the configured path will satisfy the Twig extension. That's the cleanest extension point in the whole package — and it composes cleanly with the prefix option, so you can keep the elixir() call and prefix while swapping the build tool underneath.
This is a dead-end dependency. The bundle's last release before v0.3.0 was 2017-01-30, the same month Laravel deprecated Elixir in favour of Laravel Mix (webpack). v0.3.0 is a one-commit community contribution (prefix URL) that does not modernise the stack — gulp 3 and the Symfony ceiling below are unchanged. It is archived, has 3 stars, and zero dependents. You are the only person debugging it. Treat adoption as accepting ownership of a frozen, unmaintained adapter.
gulp 3 does not run on modern Node. Elixir pins gulp ^3, whose graceful-fs@3 / natives chain throws ReferenceError: primordials is not defined on Node ≥ 12. Workarounds are Node 8/10 in a container, or pinning [email protected] via resolutions/overrides — neither is a long-term answer. This alone makes the package impractical on any current stack.
Symfony version ceiling. The bundle targets Symfony 2.x/3.x. On Symfony 4+ you'll fight composer require constraints; on Symfony Flex there's no recipe, so bundle registration is manual. On Symfony 5.4+/6+/7+ it simply won't install without --ignore-platform-reqs-style hacks. v0.3.0 did not widen these constraints.
Prefix URL is a string rewrite, not a deployment story (v0.3.0). Two failure modes are easy to introduce:
prefix: 'https://cdn.example.com/assets/' plus a manifest value css/app-8f3a1c.css can yield //. Normalise the prefix (no trailing slash) and confirm how the extension concatenates — do not assume.web/build/, you get a 200 from Symfony locally and a 404 from the CDN in prod. Your CI must sync before/with cache:clear --env=prod.Manifest path mismatches are the #1 support ticket. Three (or four, once you add a prefix) places must agree: Elixir's public.build.path in gulpfile.js, the bundle's public_dir/build_dir config, the URL prefix Symfony serves from, and now the new prefix URL. A trailing slash or a web vs public rename produces "file exists but 404s."
Case sensitivity bites on deploy. macOS dev + Linux CI: mix.sass('App.scss') finds app.scss locally but not in prod. Standardize filenames early.
CSS url() references are not rewritten. Elixir doesn't process relative paths inside compiled CSS. If app.scss references ../images/logo.png and you version, relocate, or prefix the output, those links break silently. The prefix option makes this worse, not better: absolute URLs in CSS are unaffected by it, so you can end up with a CSS file served from the CDN that still requests images from your origin. Serve images/fonts from a fixed, unversioned path.
Missing manifest = 500, not a fallback. Verify the behaviour of your exact version before relying on it in prod. Safest pattern: build assets before cache:clear --env=prod so nothing renders without the manifest present.
Debugging checklist: (1) does rev-manifest.json exist at the configured path? (2) does the key exactly match the string you pass to elixir() (leading slash vs not)? (3) is the Twig extension actually registered (debug:container | grep elixir)? (4) is NODE_ENV set during build — the --production flag is what triggers versioning? (5) with a prefix set, does the emitted URL match reality when you curl it from the CDN host?
Extension points worth knowing: the Twig extension service can be overridden to inject a custom manifest reader or a dynamic prefix (useful for S3/CDN-hosted assets where the host differs per environment); the DI configuration tree lets you relocate the manifest and, since v0.3.0, set a static prefix. Those are the only real integration surface — everything else is gulp's problem.
Verdict for day-to-day work: don't adopt it in 2024+. The v0.3.0 prefix option is a welcome convenience but does nothing about gulp 3 on modern Node or the Symfony ceiling, so it doesn't change the recommendation. If you're porting a Laravel 5.1-era app to Symfony, use Webpack Encore (the same mental model, maintained, Symfony-native) or AssetMapper (no Node at all). If you inherited a codebase already on this bundle, the lowest-risk exit is to keep the elixir() Twig call and the prefix option, replace gulp with a small script that emits a compatible rev-manifest.json, then swap the Twig function for asset() and drop the bundle entirely.
How can I help you explore Laravel packages today?