cartalyst/assetic
Cartalyst Assetic is a Laravel asset pipeline package for defining collections, applying filters, and compiling, combining, and caching CSS, JavaScript, LESS, SCSS, and CoffeeScript. It offers a configuration-driven workflow to organize front-end assets and serve optimized files.
Pin before you install. This is a legacy package (thin wrapper around the historical Assetic library) with effectively no upstream health, so treat installation as a controlled event rather than a routine composer require:
composer require cartalyst/assetic:1.0.* --prefer-dist
composer why-not php $(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
If Composer reports conflicts with PHP 8 or symfony/*, stop and escalate — that is a fork/vendor decision, not something to paper over with --ignore-platform-reqs on a shared image. Commit the lockfile.
Read the source before the README. The wrapper is small and the docs lag the code. Open three things first:
ls vendor/cartalyst/assetic/
# 1. the ServiceProvider — shows what it binds into the container (asset factory, filter manager)
# 2. the Facade — getFacadeAccessor() reveals the container key
# 3. the published config stub — collections, filters, write_to, debug
Then confirm what you actually get back:
php artisan tinker
>>> app('assetic') # or whatever the facade accessor returns
That object is your entry point for everything else. Write down its public methods — the facade method names (stylesheets(), javascripts()) should be verified here, not assumed.
Register and publish.
// config/app.php
'providers' => [
Cartalyst\Assetic\AsseticServiceProvider::class,
],
'aliases' => [
'Assetic' => Cartalyst\Assetic\Facades\Assetic::class,
],
php artisan vendor:publish # confirm the tag from the provider's publishes() array
Prove the pipeline with one collection. Configure the smallest thing that exercises filters, concatenation, and the writer:
// config/assetic.php
'collections' => [
'app' => [
'stylesheets' => ['vendor/bootstrap/css/bootstrap.min.css', 'css/app.css'],
'javascripts' => ['js/vendor/jquery.js', 'js/app.js'],
],
],
'filters' => ['cssrewrite', 'min'],
'write_to' => 'assets/compiled',
Your first real use case is almost always identical: a layout with a dozen <link> and <script> tags collapses to one call per type.
{{-- resources/views/layouts/app.blade.php --}}
<head>{{ Assetic::stylesheets('app') }}</head>
<body>
...
{{ Assetic::javascripts('app') }}
</body>
Load the page, then inspect public/assets/compiled/. Files present means the pipeline works end to end. Tags rendering but 404ing means your write_to ↔ document-root mapping is wrong — the single most common first failure.
One collection per layout, not per component. Collections are concatenation units. Name them after what renders them (admin, auth, public-app). Component-level collections lose the dedup benefit and multiply cache keys and compile passes.
Branch on environment in config, never in Blade. Development wants individual files and readable errors; production wants one concatenated, minified, revisioned artifact. Encode that once so views stay ignorant:
$prod = App::environment() === 'production';
return [
'filters' => $prod ? ['cssrewrite', 'less', 'min'] : ['cssrewrite'],
'debug' => ! $prod,
// ...
];
Filter order is load-bearing. Preprocessors (less, scssphp, coffee) must run before min. cssrewrite must run before concatenation so it still sees relative url() paths in their original location. ['min', 'less'] produces output that looks like a CSS bug but is a config bug.
Move the facade out of Blade. Register a directive or inject via a view composer:
Blade::directive('assets', fn ($expr) => "<?php echo app('assetic')->stylesheets($expr); ?>");
This isn't cosmetic — it is the seam that makes a future Vite/Mix cutover a one-file change instead of a grep across every view. Direct facade calls scattered through templates are the expensive part of migration.
Compile once per request. Facade calls re-enter the pipeline. Never call a collection inside a @foreach; compile the result once into a variable and echo it.
Warm the cache at deploy, not on first request. Iterate collections in an Artisan command invoked between composer install and app promotion:
foreach (array_keys(config('assetic.collections')) as $name) {
$this->line("Compiling {$name}");
app('assetic')->stylesheets($name);
app('assetic')->javascripts($name);
}
Skipping this makes the first post-deploy visitor pay compilation cost and exposes you to concurrent-write races on the output file.
Assert revisioning. Whichever fingerprinting mode the writer is configured for (filename hash vs. query string), test it: change a source file, reload twice, confirm the emitted URL changes. Stale-content-under-stable-URL is this package's signature failure mode.
Custom filters via the interface. Implement Assetic\Filter\FilterInterface, register the class in the config filter map or bind it in the container. Legitimate, but every custom filter is another thing a future migration must reproduce — log it.
Keep a migration inventory as you work. For each collection you touch: name → sources → filters → output path → referencing views. That's the estimate for the eventual replacement, built incrementally instead of reconstructed under deadline pressure.
Composer resolution is the first wall. The transitive symfony/* components and filter libraries (lessphp, scssphp, coffeescript wrappers) routinely conflict with PHP 8 and Composer 2's stricter resolution. --ignore-platform-reqs is only defensible in a frozen container or a dedicated branch where you control the interpreter.
Filter binaries are runtime, not Composer, dependencies. LESS/Sass/Coffee filters historically shell out via symfony/process. Present on a laptop, missing in CI and prod containers. Check explicitly in every deploy pipeline, and verify output content — silent degradation (unfiltered output served as compiled) is worse than a hard failure.
The writer cache is not artisan cache:clear. It's a separate file store keyed off inputs, historically blind to filter-config changes. When "my change didn't appear": check source on disk → check the artifact mtime in write_to → delete the compiled directory by hand → reload. Do not assume framework cache commands touch it.
cssrewrite and path assumptions. It rewrites relative url(...) based on where the file was, not where the bundle is. Change write_to depth or move a vendor package and background images vanish (styles apply, images 404). Fix with absolute paths in source CSS or a correctly rooted cssrewrite earlier in the chain.
Config cache will fool you. If your published config isn't taking effect, php artisan config:clear first. This bites everyone once.
Six-step debug sequence that resolves almost everything: dump config('assetic') → dd() the resolved collection definition → inspect the facade object in tinker → check artifact directory mtime and contents → inspect rendered HTML for the emitted URL → curl -I that URL.
Assume you own this code. Low stars, zero dependents, no maintenance signal: the next PHP bump has no upstream fix. Vendor the package into the repo, name an owner in the README, and document detection signals (Composer conflict, missing binary, stale cache, manifest 404) so the next engineer doesn't rediscover them at 2am.
Flag the governance gap. With no Node toolchain in the graph, front-end dependency versions are invisible to Dependabot, npm audit, and supply-chain scanners. Nobody will warn you that bundled jQuery or Bootstrap has gone stale. That's a risk-register entry, not a tooling footnote — and it is the strongest argument for scheduling the pipeline replacement rather than extending it.
How can I help you explore Laravel packages today?