livewire/livewire: Fix Route::lazy() and Route::defer() macros not available for package service providers
Contributed by marcheffels
Part of the livewire v4.4.7 release
Livewire 4.x ships a small but important routing fix: the lazy() and defer() route macros were not yet registered at the moment a package service provider booted ahead of Livewire's own provider. Packages that register lazy-loaded Livewire pages could therefore crash with a BadMethodCallException — not because of anything they did wrong, but purely because of the order Laravel happened to boot service providers in. This PR moves macro registration into Livewire's earlier register phase, guaranteeing the macros exist before any provider boots. If you're upgrading from v3.8.10, there's nothing you need to change in your app: this is a pure behavior fix, most relevant to package authors and apps that consume packages with lazy routes.
What was broken
Consider a third-party package that ships a lazy-loaded Livewire page and registers it from its own service provider:
class AdminPackageServiceProvider extends ServiceProvider
{
public function boot(): void
{
Route::livewire('/admin/dashboard', 'admin-dashboard')
->lazy() // BadMethodCallException if this provider boots first
->name('admin.dashboard');
}
}
If Livewire's service provider happened to boot before this one, everything worked fine. If it booted after, the lazy() call hit Laravel's Macroable trait with no macro defined and threw:
BadMethodCallException: Call to undefined method Illuminate\Routing\Route::lazy()
Because package discovery order isn't something apps control (and can differ between environments and dependency trees), this produced the worst kind of bug: code that passes on one machine and fatals on another. The same hazard applied to any provider — including your app's own route service provider — that registered routes during boot() before Livewire booted.
The root cause: the macros were registered too late
Before this change, the lazy() and defer() macros were registered by SupportLazyLoading::registerRouteMacro(), which was invoked from SupportLazyLoading::provide(). That method runs during LivewireServiceProvider::boot() — the last phase of Laravel's provider lifecycle.
Laravel discovers and boots providers in an order packages can't rely on. If a package's provider boots before Livewire's, its boot() runs before the macros exist. This is exactly the same class of bug that was fixed for Route::livewire() in PR #9903; the lazy()/defer() macros were simply left registered in the old, fragile spot.
The fix: register the macros during Livewire's register phase
The change is tiny and surgical. The call was removed from SupportLazyLoading::provide() and added to HandleRouting::register():
// src/Mechanisms/HandleRouting/HandleRouting.php
public function register()
{
// ... existing Route::livewire() macro setup ...
SupportLazyLoading::registerRouteMacro();
}
Why this works: HandleRouting is a Livewire "mechanism," and mechanisms run their register() phase during LivewireServiceProvider::register(). Laravel always completes the register() phase of all providers before calling boot() on any of them. So no matter where Livewire sits in the discovery order, the macros are guaranteed to be defined before any package's or app's boot() runs. This also makes the lazy-loading macros consistent with where the Route::livewire() macro has been registered since #9903.
The PR also adds a regression test, ProviderOrderUnitTest, that registers a RoutesBeforeLivewireServiceProvider ahead of LivewireServiceProvider, then asserts the resulting routes carry the expected lazy and defer defaults:
$this->assertTrue(Route::getRoutes()->getByName('package.lazy')->defaults['lazy']);
$this->assertTrue(Route::getRoutes()->getByName('package.defer')->defaults['defer']);
That test would have thrown the BadMethodCallException on the old code, and now passes regardless of provider order.
What this means for your upgrade from v3.8.10
The late-registration pattern existed on the 3.x path too, so apps on v3.8.10 could hit this intermittently. Upgrading to 4.x eliminates it, with no breaking changes, no configuration, and no code edits required. Concretely:
- If you never saw this error, nothing changes for you — routes registered in
routes/web.php, package providers, and Livewire's own macros all behave as before, just with the ordering hazard gone. - If you did see it, you can remove any workaround you may have added: shims like calling
Route::macro()yourself, moving lazy route registration out of package providers, or deferring route files until after boot. - Package authors can now safely call
Route::livewire(...)->lazy()or->defer()directly insideboot()without documenting "make sure Livewire boots first" caveats.
Takeaways
- What changed:
lazy()/defer()macro registration moved fromSupportLazyLoading::provide()(runs atboot()) toHandleRouting::register()(runs before allboot()calls). - Why it matters: package providers that boot before Livewire no longer crash with
BadMethodCallExceptionwhen registering lazy or deferred routes. - Precedent: same root cause and same fix location as the earlier
Route::livewire()macro fix in #9903. - Regression coverage: a new unit test simulates a package booting before Livewire and verifies the route defaults.
- Upgrade impact: none required — a transparent bug fix on the path from v3.8.10 into 4.x.