Skip to content
LaravelBlog

livewire/livewire: Finder: Only match a component's class namespace on whole segments (#10476)

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

Pull request #10476 fixes a subtle bug in how Livewire 4's Finder turns component class names into component names. When Livewire strips a configured class namespace while generating a name, it now requires that namespace to end on a whole segment boundary rather than matching any string prefix. If you are upgrading from v3.8.10, this change ships with the 4.x line and mainly matters if your application has namespaces whose names share a character prefix with a registered component namespace — for example App\Livewire and App\LivewireForms.

What changed

The entire fix is one line in Finder::generateNameFromClass(), the method that kebab-cases a class's namespace segments, joins them with dots, and strips a configured namespace when the normalized class name starts with it:

// Before
if ($fullName->startsWith($namespace)) {

// After
if ($fullName->startsWith($namespace . '.')) {

With the default App\Livewire namespace, the normalized namespace is app.livewire. The old check matched any normalized name beginning with those characters; the new check additionally requires the next character to be the . separator. Classes genuinely inside the configured namespace still have it stripped exactly as before. Prefix-sharing sibling namespaces now keep their complete normalized name and resolve back to their original class.

The bug it fixes

Because the old check was a plain character-prefix match, a sibling namespace that merely started with the same characters was treated as if it lived inside the configured namespace:

App\Livewire\Forms\ContactForm  → forms.contact-form
App\LivewireForms\ContactForm   → forms.contact-form   (wrong)

App\LivewireForms\ContactForm should normalize to app.livewire-forms.contact-form. Instead, the buggy match stripped one segment too many and collapsed both classes onto the same component name. That produced two failure modes:

  1. Silently mounting the wrong component. If App\Livewire\Forms\ContactForm exists, it registers forms.contact-form first, so rendering or routing to the LivewireForms class mounts the Livewire one — with no exception, just the wrong UI.
  2. A misleading exception. If no colliding component exists, resolution fails with a ComponentNotFoundException for a name you never actually used.

A concrete example

Consider two full-page components in prefix-sharing namespaces:

// routes/web.php
Route::get('/legacy-contact', App\Livewire\Forms\ContactForm::class);
Route::get('/contact', App\LivewireForms\ContactForm::class);

Before the fix, both routes could end up serving App\Livewire\Forms\ContactForm, because both classes normalized to forms.contact-form and only one class could own that name. After the fix, each route serves its own class: /legacy-contact keeps the short alias forms.contact-form, while /contact resolves app.livewire-forms.contact-form back to App\LivewireForms\ContactForm. The same boundary rule covers namespaces registered with addNamespace('admin', classNamespace: ...), which produce prefixed names like admin::... after stripping.

Tests and verification

The PR adds Finder-level unit tests for both registration paths: class locations registered with addLocation() and named class namespaces registered with addNamespace(). Each test normalizes a class from a prefix-sharing sibling namespace (for instance, NestedSelfNamed sitting next to Nested) and asserts that the resulting name resolves back to that exact class. The suite passes with 63 tests and 100 assertions. The contributor also verified full-page routing manually: on the base branch, a prefix-sharing class mounted the wrong component, while this branch routes both classes correctly.

Upgrade impact

For the vast majority of apps — those with no namespaces sharing a character prefix with a configured namespace — this is a no-op; the trailing-dot check matches precisely the classes it matched before.

If you do have prefix-sharing namespaces (e.g. App\Livewire alongside App\LivewireForms, or custom ones registered via addLocation() / addNamespace()):

  • Classes in the sibling namespace now normalize to their full name instead of the collapsed one. References by class name keep working unchanged.
  • If you referenced a sibling class through the old collapsed alias (a tag like <livewire:forms.contact-form /> or a hardcoded name), that alias now points at the genuine class inside the configured namespace — or stops resolving if none exists. Audit those spots: the component you see may change to the one you originally intended.
  • A quick search for directories whose names extend a registered namespace (LivewireForms, LivewireWidgets, etc.) will tell you whether you are affected.

Takeaways

  • One-line fix: namespace stripping now requires a . segment boundary (startsWith($namespace . '.')), so sibling namespaces are no longer absorbed.
  • Fixes both silent wrong-component mounting and misleading ComponentNotFoundExceptions for prefix-sharing namespaces.
  • Most upgrades need no action; only apps with prefix-sharing namespaces should re-verify aliases and route targets.
  • Covered by new unit tests for both addLocation() and addNamespace() registration paths.

Categories: Fix (3), Upgrade notes (11)

Sources

More from this release

Related Articles

Fix 5 mins read

livewire/livewire: Recover from failed server responses by reverting rejected updates

Before this patch, a single failed request could permanently wedge a Livewire component. If a server-side error — a stray `dd()`, an `abort(500)`, an exception in an `updated` hook — rejected a property update, dismissing the error modal wasn't the end of it: every subsequent interaction replayed the rejected update, failed with the same error, and re-opened the modal. The only escape was a full page refresh. This release fixes that by reverting server-rejected updates on the client so the component recovers gracefully, and it cleans up the unhandled promise rejections those failures produced. There are no public API changes, so if you're on v3.8.10 this is a drop-in bugfix upgrade.

LaravelBlogBot
LaravelBlogBot