[4.x] Fix component named Index resolving to an empty name - #10475
Conversation
| // An index component at the namespace root collapses to an empty name, so restore 'index'... | ||
| if ($name === '') { | ||
| $name = 'index'; | ||
| } |
There was a problem hiding this comment.
feels weird to me to not solve this problem further upstream.
like won't there be a case now where a truly empty string name (which admittedly would be weird) gets upgraded to index? maybe that's even desired behavior i suppose?
does feel kind of funny to use "Index" as a root level name anyways but I see that this is the most logical behavior: look for a a class called "Index"
So this handles the case of Index at the root of the class namespace - what about the case of looking for index.blade.php as the root of the SFC...
Also is this in the codepath of namespaced components and what effect would that have?
Just something to think a little harder about.
There was a problem hiding this comment.
Thanks for reworking this. Stripping the namespace before the .index reads much cleaner than my empty-string guard, and I agree it's better to never produce the empty name than to restore it after the fact.
On the SFC question you raised: single and multi-file components never go through generateNameFromClass. Their name comes from the tag (<livewire:index /> gives index), and resolveSingleFileComponentPath / resolveMultiFileComponentPath turn that name into a path. A root-level index.blade.php resolves through the plain singleFile lookup at {location}/index.blade.php, and an index/ directory through multiFileAsIndex, so nothing on the view side ever collapses to an empty name. The class path was the only one deriving a name backwards from a fully qualified string, which is why it was the only place hitting this.
And for namespaced components it's the same loop, so an Index at a registered namespace root now comes back as admin::index instead of admin::, which the namespace test you added covers.
There was a problem hiding this comment.
Yep, that matches what I found — thanks for confirming the SFC side, your read is right. Nothing derives a name from a view path anywhere in src/, so the class path really was the only backwards direction.
Two things worth recording here:
The bug is nastier than the issue reports. The empty name flows into generateClassFromName(), which builds \App\Livewire\\Index — note the double backslash. Composer PSR-4-resolves that to the same file under a spelling PHP doesn't consider already-loaded, so it requires it a second time and you get a hard Fatal error: Cannot redeclare class, not just the ComponentNotFoundException. Depends on load order which one you hit.
The realistic trigger is a page component. A root-level Index is almost always somebody's index page, i.e. Route::get('/', Index::class) — which is exactly the class→name direction. Referencing it by name (<livewire:index />) was never broken, which is probably why this went unnoticed for a while. I added src/Tests/RootIndexComponentUnitTest.php covering both directions plus an update roundtrip against a real route; on the unpatched code the page-component test 500s with Unable to find component: [] and the name-based one passes.
Full Unit suite is green (1420 tests).
Unrelated to this PR, but I noticed it while poking at the same function: the namespace match is a plain startsWith, so it isn't dot-boundary aware. With a class location of App\Livewire, a class in a sibling namespace like App\LivewireForms\ContactForm matches and gets its prefix chewed off mid-segment, yielding forms.contact-form. Separate issue, leaving it alone here.
…ing an empty name Fixes the same bug further upstream. The ordering was the actual problem: '.index' was stripped from the fully-qualified name before the namespace prefix was removed, so an 'Index' at the root of the namespace collapsed into the namespace itself and left nothing behind. Doing it in the other order means the empty name is never produced in the first place, so there's nothing to restore, and a genuinely empty name is never silently promoted to 'index'. Also adds coverage for the same case under a registered namespace (which returned 'admin::' before), and an end-to-end test rendering a root-level Index through a real route. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fb28afa to
5ac7ece
Compare
|
I've rebased this on the |
…10475) Co-authored-by: Caleb Porzio <calebporzio@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…10475) Co-authored-by: Caleb Porzio <calebporzio@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…10475) Co-authored-by: Caleb Porzio <calebporzio@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A component class named
Indexat the root of the configuredclass_namespace(for exampleApp\Livewire\Index) throwsComponentNotFoundExceptionwith an empty name ("Unable to find component: []").generateNameFromClass()strips a trailing.indexbefore it removes the namespace prefix, soapp.livewire.indexcollapses toapp.livewire. That makes the name equal to the namespace, so removing the prefix leaves an empty string. A root-levelIndexnow falls back toindex, the name it resolved to under v3.Fixes #10473