rednose/combo-handler-bundle
Rednose ComboHandlerBundle integrates combo-handler support into Symfony, letting applications combine multiple CSS or JavaScript assets into a single HTTP response. It reduces request overhead, simplifies asset delivery, and helps improve frontend loading performance.
You cannot composer require rednose/combo-handler-bundle into a Laravel app and get anything useful. It is a Symfony 2.x bundle — it wires into AppKernel, routing.yml, the Symfony DI container and the legacy HTTP kernel. None of that exists in Laravel. The honest first step is to treat it as a reference implementation and build the combo-handler behavior natively.
/combo?files=... is parsed), the loader/resolver service (how it turns a list of paths into files on disk), and the response builder (how Content-Type, Last-Modified, ETag/cache headers are set). Those three concerns are the entire feature. Everything else is Symfony plumbing you will replace.Route::get('/combo', ComboController::class)), a dedicated service that resolves + concatenates, and a cache layer (filesystem or Redis) keyed by the sorted list of files plus a version hash.<script src> tags for your own JS files and collapse them into one <script src="{{ route('combo', ['files' => 'js/a.js,js/b.js,js/c.js']) }}">. Verify the single response, the correct Content-Type, and a cache hit on the second request before you touch anything else.If you are genuinely on an old Symfony 2 codebase (and it happens — legacy projects linger), then the bundle-level steps apply: register the bundle in AppKernel.php, import its routing, and configure the allowed asset roots. But pin the version explicitly in composer.json; with zero stars and zero dependents nobody is maintaining it, and Symfony 2 is end-of-life.
The package is a design reference; here are the patterns worth lifting and how they map to Laravel primitives.
Route + allowlist + resolver (the core loop). The bundle's request shape is a single endpoint with a comma-separated files parameter. In Laravel:
Route::get('/combo/{files}', ComboController::class)
->where('files', '[A-Za-z0-9/_.\-,\x20]+');
Resolve each entry against a configured root (config('combo.roots') — public_path('assets'), public_path('vendor')), normalize with realpath(), and reject anything that does not str_starts_with the resolved root. This is your path-traversal defense and it is not optional — a combo endpoint that trusts a client-supplied file list is a file-disclosure vulnerability.
Extension → content-type mapping. The bundle maps extensions to MIME types and refuses mismatched or unknown extensions. Do the same with a fixed array (.css/.js only, realistically). Derive the response Content-Type from the requested extension set, and reject requests that mix extensions (a a.css,b.js combo has no correct content type).
Cache-keyed concatenation. The expensive part is disk I/O and concatenation; cache the merged output keyed by a hash of the normalized file list plus each file's mtime (or a git SHA / asset version). Store it under storage/app/combo/ or Redis. Serve from cache with Cache-Control: public, max-age=..., an ETag from the cache key, and a Last-Modified from the newest mtime. On a deploy, the version component invalidates naturally.
Conditional requests. Handle If-None-Match / If-Modified-Since and return 304. The bundle sets cache-friendly headers; replicating that is a handful of lines and is what makes the endpoint worth having.
Minification hook. The bundle is extensible with custom loaders — the equivalent is a pipeline in your service: concat → (optional) minify → cache. Run minification once, at cache-write time, never per request.
Integration tip — Blade macro. Wrap the URL generation in a helper so views stay clean:
Blade::directive('combo', fn ($expr) => "<?php echo combo_url($expr); ?>");
When the bundle is real. If you are legitimately on Symfony 2, the patterns are the same: define allowed source roots in bundle config, import the bundle routing under a /combo prefix, and extend the resolver service (register it in services.yml under the bundle's service id) for custom loaders. The extension point is the loader/resolver service; swap it rather than editing vendor code.
.. sequences, absolute paths, symlink escapes, URL-encoded variants, and null bytes are all live attack vectors. Allowlist roots, realpath(), reject symlinks, and validate extension before touching the filesystem. Write tests for each of these — an untested combo endpoint is a bug waiting to be found by someone else.a.css,b.css and b.css,a.css produce two entries for identical output — cardinality blows up, and a crafted list can fill your cache. Normalize (sort, dedupe, canonicalize paths) before hashing.rename()) and consider a lock or a short "stale-while-revalidate" window. Partial writes served to clients are the ugly failure mode here.mtime or a build version in the key, and invalidate at deploy time. If you front this with a CDN, remember the CDN holds the stale copy — purge on release..css and .js in one request, or sniffing extensions loosely, leads to browsers interpreting a stylesheet as script. Enforce single-extension per request and set the header explicitly. Add X-Content-Type-Options: nosniff.concat module) is a swap rather than a rewrite.How can I help you explore Laravel packages today?