TL;DR — I shipped a release note claiming "Livewire 4 is not supported" based on two failures I never verified myself. Checking properly found that one of them wasn't real, the other was, and that the reason I couldn't tell them apart was Laravel caching compiled Blade views keyed on the Blade source — not on the Livewire version that compiled them.
The claim I shipped without checking
laravel-scheduler-manager ships a Livewire + Flux management UI for database-backed schedulers. When v0.3.0 went out, composer.json pinned livewire/livewire to ^3.7, and the README said this:
Livewire 4 is not supported. Livewire 4 changes component resolution so explicitly registered namespaced components cannot be found, and its
wire:keyprecompiler emits invalid Blade for<flux:*>tags.
Two claims. One constraint. Zero personal verification — both came from a bug report I took at face value, because the pin was going in either way and "unsupported" felt like the safe direction.
That's the part worth being uncomfortable about. A version constraint is a promise to every consumer of your package, and "I think it's broken" is not evidence. A constraint you can't defend with a CI cell is a guess wearing a semver costume.
Claim one was real, and it's nine lines
Livewire 3 is happy with the obvious registration:
foreach (static::COMPONENTS as $alias => $class) {
Livewire::component($alias, $class);
}
That fills an explicit alias → class map. scheduler-manager::dashboard resolves, every screen renders.
Livewire 4 resolves a namespaced alias differently. Finder::resolveClassComponentClassName() parses everything before the :: as a namespace, looks it up in the registered class namespaces, and returns null if it isn't there — without ever consulting the explicit classComponents map you just populated. So on Livewire 4 the map is still perfectly correct and completely unread, and every screen dies with Unable to find component: [scheduler-manager::dashboard].
The fix is to register the namespace as well:
// Livewire 4 resolves a namespaced alias ("scheduler-manager::foo")
// exclusively through its registered class namespaces and never falls
// back to the explicit component map, so Livewire::component() alone
// leaves every screen unresolvable there. addNamespace() exists only on
// Livewire 4, hence the call through the facade root rather than the
// static proxy, which would not type-check against Livewire 3.
$manager = Livewire::getFacadeRoot();
if (is_object($manager) && method_exists($manager, 'addNamespace')) {
$manager->addNamespace(
'scheduler-manager',
classNamespace: __NAMESPACE__.'\\Livewire',
);
}
foreach (static::COMPONENTS as $alias => $class) {
Livewire::component($alias, $class);
}
Two details in there are the interesting ones.
Why getFacadeRoot() and not Livewire::addNamespace(...)? Because addNamespace() doesn't exist on Livewire 3, and the package still supports Livewire 3. A static call on the facade gets type-checked against whatever Livewire is installed, so PHPStan fails the build on a Livewire 3 matrix cell for a method that legitimately isn't there. Going through the facade root gives you an object, and method_exists() is then an honest runtime question rather than a static lie. It's the same shape you'd use for any optional capability on a dependency you support across a major boundary.
Why keep Livewire::component() too? The namespace registration is a convention-based resolver — it maps scheduler-manager::dashboard onto a class name in a namespace. The explicit map is what Livewire 3 needs and is also the thing that survives you renaming a class. Registering both is harmless on either major and costs nothing.
Claim two was real too, and it's upstream
The second one — wire:key on a <flux:*> tag producing syntax error, unexpected token "endif" — turns out to be exactly what it says on the tin.
Livewire 4's SupportCompiledWireKeys precompiles wire:key by splicing a <?php ... ?> block in immediately before the attribute. Immediately before the attribute means inside the tag. On a plain HTML element Blade doesn't care. On a Blade component tag like <flux:table.row wire:key="{{ $run->uuid }}">, the component compiler then has to parse a tag whose attribute list contains a raw PHP block, and what comes out the other side is invalid PHP. The endif in the error message isn't yours — it's the compiler losing its place.
That's upstream behaviour, and I'm not interested in papering over it with a workaround inside my package. So the ^3.7 pin stays, but for one specific documented reason with an issue number attached, instead of a vague two-part rumour.
The actual lesson: the suite told me both claims were false
Here's the part that cost the most time.
I switched the installed Livewire to v4, ran the suite, and got 233 passed. Which would mean neither claim was real. I very nearly wrote that up as the correction — same mistake as the original, opposite direction.
Compiled Blade views are cached on disk, and the cache key is derived from the Blade source file, not from the Livewire version that compiled it. Your Blade source didn't change when you swapped majors. So the run happily reused views compiled by the other Livewire, sailed straight past the precompiler that would have produced the invalid PHP, and reported green.
Delete the compiled views and re-run and 233-passed becomes 66-failed.
rm -f vendor/orchestra/testbench-core/laravel/storage/framework/views/*.php
Generalise it, because this isn't a Livewire thing:
Any cache whose key doesn't include the thing you just changed will lie to you.
Compiled Blade views are keyed on Blade source. Route and config caches are keyed on nothing at all — they're just a file. Opcache is keyed on file mtime. Swap a dependency major underneath any of those and you get a result that describes a world that no longer exists. The tell is a suspiciously clean run right after a change you expected to be disruptive.
So the trap went into CONTRIBUTING.md as a rule, not into my head as a memory:
Do not widen
livewire/livewirepast^3.7without running the suite on Livewire 4 with a cleared Blade cache. Compiled views are keyed on the Blade source, not the Livewire version, so switching majors without deleting the compiled views reports a false green.
And the matrix cell, because documentation isn't a gate
The namespace fix is real, but a fix nobody runs is a fix that lasts until the next refactor. This package already had a 2×2 CI matrix (PHP 8.4/8.5 × Laravel 12/13). The Livewire dimension belongs there for the same reason the Laravel dimension does.
That's the rule I keep coming back to on packages: every compatibility claim in your README should map to a cell in your matrix. If you can't point at the cell, you're not supporting that version — you're hoping.
What I'd take away
- Verify a constraint before you ship it. "Unsupported" is a claim with the same weight as "supported"; both need evidence.
- When you support a dependency across a major boundary, capability checks (
method_existson a resolved object) beat static calls, because static calls get type-checked against exactly one of the majors. - A green suite immediately after a disruptive change deserves suspicion, not relief. Ask what's cached and whether the cache key knows about the change you just made.
- Turn the trap into a gate. A note in
CONTRIBUTING.mdcatches a human; a matrix cell catches everyone.
Livewire 4 support is still one upstream bug away. But now it's one bug, it's written down, and the suite that says so is telling the truth.
Top comments (0)