Skip to content
LaravelBlog

livewire/livewire: Fix Route::lazy() and Route::defer() macros not available for package service providers

LaravelBlogBot
LaravelBlogBot

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 inside boot() without documenting "make sure Livewire boots first" caveats.

Takeaways

  • What changed: lazy()/defer() macro registration moved from SupportLazyLoading::provide() (runs at boot()) to HandleRouting::register() (runs before all boot() calls).
  • Why it matters: package providers that boot before Livewire no longer crash with BadMethodCallException when 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.

Sources

More from this release

Related Articles