Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Asset Laravel Package

orchestra/asset

Asset management and pipeline component for Laravel: define named containers, register scripts and stylesheets with dependencies, then render combined, versioned output—keeping templates clean while cache busting and asset URLs stay automatic. Part of the Orchestra Platform ecosystem.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the wiring.

composer require orchestra/asset
php artisan vendor:publish --provider="Orchestra\Asset\AssetServiceProvider"

On Laravel 11+ discovery should pick up Orchestra\Asset\AssetServiceProvider; otherwise add it to config/app.php providers and alias Orchestra\Support\Facades\Asset to Asset. Before writing app code, confirm the manifest actually declares discovery for your installed version:

cat vendor/orchestra/asset/composer.json | grep -A5 '"extra"'

The published documentation is essentially a changelog, so the package's own source is your reference. Read three things, in this order:

  1. src/AssetServiceProvider.php — is container state a singleton? Is there a flush hook? (This answers the Octane question for you.)
  2. The manager/container class it binds — that's the whole API surface: container(), add(), style(), script(), styles(), scripts(), all().
  3. The published config — version key, any path prefix, default container name.

First use case: get the tags out of your layout.

The minimum useful adoption is replacing hand-written <link>/<script> tags in layouts/app.blade.php with two container renders. Register in AppServiceProvider::boot() for the spike:

use Orchestra\Support\Facades\Asset;

Asset::container('default')->style('app', 'css/app.css');
Asset::container('default')->script('app', 'js/app.js');
<head>
    {!! Asset::container('default')->styles() !!}
</head>
<body>
    @yield('content')
    {!! Asset::container('default')->scripts() !!}
</body>

If the CSS lands in <head> and the JS before </body>, the split-by-type model works and you've validated the core loop. Everything after that — dependency ordering, per-module containers, versioning — is layering.

Day-to-day mental model. You will touch this package in exactly three situations: adding a script to one page (a controller method), shipping assets from a module or vendor package (a service provider's boot()), and debugging why a tag didn't render (tinker). If your usage drifts outside those three, you're probably using it for something Vite should do.

Implementation Patterns

Split containers by slot, not by concern. Name containers after where they render — head, body-end, head-late — not after what they contain. The layout owns the slot names; every module registers into a slot. A module should never need to know the layout's structure beyond "head" and "body-end", and the layout should never need to know a module exists:

// In BillingServiceProvider::boot() — the host layout knows nothing about this
Asset::container('head')->style('billing', __DIR__.'/../resources/css/billing.css');
Asset::container('body-end')->script('billing', __DIR__.'/../resources/js/billing.js', ['jquery']);

If you catch yourself editing a Blade layout to add a tag for a feature, the container has been bypassed and the indirection — the actual product here — is gone.

Dependency ordering is the reason to adopt rather than build. A naive @stack/@push implementation gets you concatenation; it doesn't get you ordering. Declare it at registration:

Asset::container('body-end')->script('jquery', 'vendor/jquery.js');
Asset::container('body-end')->script('widget', 'vendor/widget.js', ['jquery']);
Asset::container('body-end')->script('widget-init', 'js/widget-init.js', ['widget']);

Keep the graph coarse. Declaring ['jquery'] on ten plugins is fine; a 30-node DAG across vendor files is where ordering bugs live and where the package's resolution stops being legible. If you can't sketch the resolved order on a napkin, simplify the declarations instead of debugging the output.

Register in providers, controllers, and view composers — never in Blade. Populating a container from a Blade partial means it runs at render time and re-runs on partial re-render, which is how you get duplicates and order-dependent surprises. The controller pattern for page-specific assets:

public function edit(Invoice $invoice)
{
    Asset::container('body-end')->script('invoice-editor', 'js/invoice-editor.js', ['app']);
    return view('invoices.edit', compact('invoice'));
}

For assets needed by many views but not globally, a view composer in a provider is the cleaner home — it keeps controllers free of presentation wiring.

Render from exactly two places. ->styles() in <head>, ->scripts() before </body>. Calling ->scripts() inside a mid-body partial silently degrades to document-flow order and voids the ordering guarantee you paid for. If a partial needs a script, register it upstream; don't render it inline.

Version with something deploy-correlated. Use a git SHA or release tag, bumped in the same deploy step that runs composer install. Wall-clock timestamps produce a different URL on every request and destroy CDN hit rates. If versioning is container-level only, accept the coarseness — a release invalidates everything, which is correct-but-blunt. Per-file hashing is Vite's job; don't try to out-clever the package.

Coexist with Vite; don't compete.

  • @vite([...]) for compiled, hashed, tree-shaken bundles with imports.
  • Orchestra Asset for the runtime tail: CDN scripts with hard ordering constraints, per-tenant theme CSS, third-party marketing tags, the two legacy pages nobody has migrated.

The healthy end state is both in the same layout. The unhealthy one is asking this package to process SCSS or asking Vite to respect a container's ordering.

Per-request registration under Octane and multi-tenancy. Containers are mutable accumulators. Anything registered into a long-lived singleton is a contamination risk — worst case, tenant A's theme CSS in tenant B's page. Register tenant/theme assets from middleware or a RequestReceived listener rather than a boot() that fires once per worker, and verify (by reading the provider) whether the package flushes between requests. A cheap soak test: run Octane with max_requests low, hit the same worker twice as different tenants, diff the <head>.

Publish vendor assets in the pipeline, never at runtime. Treat vendor:publish as a build step:

php artisan vendor:publish --tag=asset --force --no-interaction

Publishing from a controller or route writes to public/ under your process user — a footgun and a race under concurrent requests.

Test the emitted markup, not the manager. The API is a thin wrapper over string concatenation, so assert on HTML:

$this->get('/invoices/1/edit')
    ->assertSee('invoice-editor.js', false)
    ->assertSeeInOrder(['jquery.js', 'widget.js'], false);

assertSeeInOrder is the cheapest regression test for every ordering constraint you declare, and it's the one that catches a container rename during refactor.

Extend with macros and directives rather than forks. Asset::macro('cdn', fn ($url) => ...) and a @assets('body-end') Blade directive both remove repetitive shapes and keep your diff against upstream at zero — which matters a great deal for a single-maintainer package (see below).

Gotchas and Tips

It fails open, silently. A misspelled asset name, an unregistered container, or a provider that didn't boot produces no tag and no exception — just missing CSS on one page, which presents as a browser cache issue and costs an afternoon. First debug move, every time:

dump(Asset::container('body-end')->all());

Add a non-production RequestHandled listener that logs each container's resolved contents so "which assets loaded on this request" is greppable. Also confirm path resolution: check whether relative paths are run through asset() or emitted raw — if it's raw, CDN domains and multi-host setups break, and if it's asset(), your CDN needs ASSET_URL set. Verify in the source; don't assume.

Namespace asset names, share container names. Two modules registering into body-end is the intended design. Two modules both registering an asset named app into the same container will collide — overwrite or duplicate depending on implementation, and neither is what you want. Use billing.app, reports.app.

Octane support is one changelog line. "Added Laravel Octane support" is the entire documented delta for v6.1.0. Read the provider for statics/singletons holding container state, soak-test with worker recycling, and if there's no flush, add your own in middleware and comment it loudly. The difference between a stale page and a cross-tenant style leak is one missing flush.

CSP nonces and SRI are almost certainly not handled. Rendered output is plain <link>/<script> strings. If you enforce a nonce-based CSP or require integrity= on third-party scripts, you'll post-process the rendered string or extend the renderer. Test with production headers enforced — Report-Only will hide this until it's a broken page.

Version coupling to Laravel majors. Orchestra packages version-track Laravel (v6.x ↔ a Laravel major). Framework upgrades become coordinated package upgrades. Pin explicitly and put this on the upgrade runbook, not in the surprise column mid-upgrade.

Zero dependents means you're the first to hit your edge case. No ecosystem means no Stack Overflow answers and no upstream pressure on bugs you find. Mitigation: the container/ordering/versioning logic is a few hundred lines — vendor it behind the same facade, or keep a fork. Decide before rollout. It's cheap insurance and it's only expensive to arrange after the package is load-bearing.

Don't use it as a bundler. No transpilation, tree-shaking, CSS preprocessing, or code splitting. If the requirement is "ship less JavaScript," this is the wrong layer and adopting it will make the build story worse.

config:cache splits local and prod behaviour. If you register assets dynamically from config, production freezes those values at cache time while local mutates per request — the classic works-on-my-machine divergence. Run php artisan config:cache locally once before trusting anything config-driven.

Release script entry. Add the vendor:publish --force --no-interaction line immediately after composer install. If it isn't in the script it gets forgotten exactly once — on the deploy where a vendor CSS file changed — and the symptom is a 404 that looks like a cache problem.

When to walk away. If the app is single-layout, single-team, and Vite-only, the package earns nothing over @vite plus two @stacks. Its value is proportional to how many independent sources contribute front-end assets and how much you need their ordering to be declarative. Below that threshold, the silent-failure and Octane-state risks outweigh the convenience.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager