livewire/livewire: Finder: Only match a component's class namespace on whole segments (#10476)
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:
- Silently mounting the wrong component. If
App\Livewire\Forms\ContactFormexists, it registersforms.contact-formfirst, so rendering or routing to theLivewireFormsclass mounts theLivewireone — with no exception, just the wrong UI. - A misleading exception. If no colliding component exists, resolution fails with a
ComponentNotFoundExceptionfor 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()andaddNamespace()registration paths.
Categories: Fix (3), Upgrade notes (11)