sanpi/assetic
Laravel integration for the Assetic asset management library. Define CSS/JS asset collections, apply filters such as LESS, Sass, CoffeeScript and minification, then dump and serve compiled, cached assets via Blade helpers for a simple PHP asset pipeline.
Despite the package description's claims of Laravel view/helper integration, there is no service provider, facade, config publisher, or Blade directive. sanpi/assetic is a fork of kriswallsmith/assetic and is a plain Composer library. Everything in this guide is glue you write and own.
Install
composer require sanpi/assetic
Then immediately confirm it did not disturb the Symfony components Laravel already pins (this is the #1 failure mode):
composer why symfony/process
composer why symfony/filesystem
composer show sanpi/assetic
If Composer downgraded a Symfony component, stop and reconsider before writing any code — the whole integration rests on a resolvable dependency graph.
Where to look first (all under vendor/sanpi/assetic/)
src/Assetic/Factory/AssetFactory.php — the object you will actually drive every day.src/Assetic/Filter/ — built-in filters (Less, Sass, CssMin, JSMin, UglifyJs2, CoffeeScript, etc.).src/Assetic/AssetWriter.php — writes compiled output to disk.src/Assetic/Factory/Worker/CacheBustingWorker.php — renames/rewrites output for versioning.src/Assetic/AssetManager.php / AssetCollection — the collection model.composer.json and changelog — its value over upstream is PSR-4/autoloading and updated PHP/Symfony constraints; the API is upstream-identical. If you know Assetic, you know this.First use case — compile one stylesheet at build time
Assetic's mental model is load assets → apply filters in order → write combined output. A minimal container binding:
// app/Providers/AsseticServiceProvider.php
namespace App\Providers;
use Assetic\Factory\AssetFactory;
use Assetic\Filter\CssRewriteFilter;
use Assetic\Filter\CssMinFilter;
use Illuminate\Support\ServiceProvider;
class AsseticServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(AssetFactory::class, function () {
$factory = new AssetFactory(public_path());
$factory->setDebug(app()->environment('local'));
return $factory;
});
}
}
Then a one-off compile in a command or test to prove the pipeline works before building config around it:
$factory = app(AssetFactory::class);
$collection = $factory->createAsset(
['css/app.css', 'css/theme.css'],
[new CssRewriteFilter()],
['output' => 'css/all.css']
);
(new \Assetic\AssetWriter(public_path()))->writeAsset($collection);
That's the whole first milestone: two files combined into public/css/all.css with a filter applied, executed from the CLI, not from a controller.
The reliable pattern is build-time compile + manifest contract + runtime lookup. The application never touches Assetic during a request.
1. Config-driven collections
Model each asset group as config, not code:
// config/assetic.php
return [
'css' => [
'app' => [
'inputs' => ['resources/css/app.css', 'resources/css/theme.css'],
'filters' => ['cssrewrite', 'cssmin'],
'output' => 'build/app.css',
],
],
'js' => [
'app' => [
'inputs' => ['resources/js/vendor.js', 'resources/js/app.js'],
'filters' => ['jsmin'],
'output' => 'build/app.js',
],
],
];
2. A filter registry in the service provider
Translate string filter names to Assetic filter instances once, so config stays declarative:
$filters = [
'cssrewrite' => fn () => new \Assetic\Filter\CssRewriteFilter(),
'cssmin' => fn () => new \Assetic\Filter\CssMinFilter(),
'jsmin' => fn () => new \Assetic\Filter\JSMinFilter(),
];
Then loop config('assetic.*'), call $factory->createAsset($inputs, $resolvedFilters, ['output' => $output]), and hand each collection to the AssetWriter.
3. The dump step is a command, run in CI
Wrap the compile loop in php artisan assetic:dump. Run it in your deploy pipeline (composer install → php artisan assetic:dump → serve). Never invoke it from a route, middleware, or view — that couples request latency to disk I/O and external binaries.
4. Cache-busting via the CacheBustingWorker + manifest
Assetic's versioning renames files to app-<hash>.css and rewrites references. Because you cannot rely on @vite or asset() to know the new name, emit your own manifest at dump time:
$writer = new \Assetic\AssetWriter(public_path());
$worker = new \Assetic\Factory\Worker\CacheBustingWorker();
$factory->addWorker($worker);
// after writing, record logical name → hashed filename
$manifest = ['app.css' => 'build/app-48f1c2.css'];
file_put_contents(public_path('build/manifest.json'), json_encode($manifest));
5. Runtime lookup: a Blade helper, not the Assetic API
// app/Support/Asset.php
class Asset
{
public static function url(string $logical): string
{
$manifest = json_decode(
file_get_contents(public_path('build/manifest.json')), true
);
return asset($manifest[$logical] ?? $logical);
}
}
Expose it as a Blade directive (@asseticUrl('app.css')). This is the "integration" the package description advertises and the package does not provide.
6. Environment-aware behavior
Use AssetFactory::setDebug(). In local, emit individual unminified files (faster iteration, readable output). In production, apply minifiers and the cache-busting worker. Drive this from app()->environment(), not from Assetic itself — it has no concept of Laravel environments.
7. Order of operations matters
Filters run left-to-right as configured. Preprocessors (less/sass) must precede minifiers (cssmin), and cssrewrite must run after any URL-relative rewriting. Encapsulate common chains as named presets in config so nobody hand-orders filters per collection.
Composer resolution is the most likely break. Laravel 10/11 pin Symfony 6/7; this fork's constraints may not reconcile. Test composer install on your exact PHP/Laravel versions before committing to the approach. If it fails, isolate Assetic in a dedicated build container and ship only public/build/ output.
No request-time execution — ever. If you find yourself calling AssetFactory inside a controller, you have misread the model. Runtime filtering ties availability to external binaries and degrades under concurrency.
Filter binaries drift silently. uglify-js, lessc, node-sass, ImageMagick — several are deprecated or removed from current base images. Verify every binary exists in your CI image and pin its version. A missing binary produces confusing exceptions deep in filter classes, not a clean "binary not found."
Silent stale assets. The worst failure mode: the dump step is skipped or the manifest diverges from disk, and users get old CSS with no error. Guard by running assetic:dump in the deploy script and asserting the manifest file exists and its entries resolve to real files in a post-deploy smoke check.
The CacheBustingWorker only helps if the manifest matches. It renames files; you must still persist the mapping yourself. Don't assume asset() will resolve hashed names.
setDebug() changes output shape, not just minification. In debug mode Assetic emits separate files; make sure your manifest helper handles both the debug and production case, or your local @asseticUrl calls will point at files that don't exist.
Over-bundling destroys caching. Combining every stylesheet into one file means a one-character change invalidates the whole bundle. Define bundle boundaries deliberately (vendor vs. app vs. per-page) rather than dumping all inputs into one collection.
No security response path. Zero stars, zero dependents, single maintainer over an EOL upstream means you own every transitive vulnerability. Pin an exact commit/version, vendor or fork internally if needed, and assign a named owner.
Extension point: custom filters. Implement Assetic\Filter\FilterInterface (filterLoad, filterDump) to wrap any tool Assetic lacks a built-in for. This is the only sanctioned extension surface — don't patch the library in vendor/.
Debugging the pipeline. There is almost no logging. When output is wrong, dump the resolved collection graph ($collection->all() to see inputs/filters) and inspect the public/build/ directory directly. Adding a temporary dd() on the filter list before AssetWriter::writeAsset() resolves most "filter didn't apply" cases.
Treat the whole thing as a time-boxed shim. For any greenfield Laravel work, skip this entirely and use Vite (laravel-vite-plugin + @vite). Adopt sanpi/assetic only to keep a pre-existing Assetic pipeline installable, behind a clean AsseticServiceProvider + manifest boundary, with a dated migration checkpoint.
How can I help you explore Laravel packages today?