livewire/livewire: Fix PHP 8.5 null array offset deprecation in EloquentCollectionSynth::hydrate()
Contributed by JulianGlueck
Part of the livewire v4.4.7 release
Patch releases rarely get simpler than this one. Pull request #10380 removes a PHP 8.5 deprecation warning that Livewire triggered whenever it rehydrated a component holding an empty Eloquent collection. The entire change is a one-line guard inside EloquentCollectionSynth::hydrate(), the class responsible for turning snapshots back into Eloquent collections between requests. If you are upgrading from v3.8.10, there is no API change, no configuration to touch, and no template to edit — this is a drop-in bug fix that matters mostly if you run PHP 8.5, or plan to.
What changed
In src/Features/SupportModels/EloquentCollectionSynth.php, the call that resolves a possible morph-map alias now checks the model class first:
// Before (v3.8.10)
$modelAlias = Relation::getMorphedModel($modelClass);
// After (this fix)
$modelAlias = $modelClass ? Relation::getMorphedModel($modelClass) : null;
That is the whole fix. It mirrors a guard that already existed on the dehydrate side of the same class — a hint that hydration simply missed the same edge case. When $modelClass is null (exactly what happens for empty collections), Livewire now skips the alias lookup instead of handing null to Relation::getMorphedModel(), which internally reads static::$morphMap[$alias] and therefore used null as an array offset.
Why a null offset matters on PHP 8.5
PHP has long been forgiving here: reading $array[$key] with a null key silently coerces it to an empty string, so Relation::getMorphedModel(null) quietly returned null on every PHP version up to 8.4. PHP 8.5 deprecates that coercion, so the same lookup now emits a deprecation warning each time it runs.
The catch is frequency. Livewire dehydrates and rehydrates component properties on every subsequent request — $refresh, wire:poll, action calls, lazy loading. An empty collection in a typed property is common: a filtered table with no matching rows, a dashboard widget before data loads, an optional relationship. Any of those produced a deprecation on essentially every request cycle — log noise, noisy error trackers, and hard failures in strict setups that promote deprecations to exceptions.
The failing path, step by step
Here is a component that triggered the warning before the fix:
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
class PendingOrders extends Component
{
public Collection $orders;
public function mount()
{
// No matching rows for the current filters:
$this->orders = new Collection();
}
public function render()
{
return view('livewire.pending-orders');
}
}
The sequence behind the scenes:
- On dehydration, Livewire asks the collection for its queueable class. An empty collection has none, so
getQueueableClass()returnsnulland the snapshot storesmodelClass: null. - On the next request,
hydrate()reads thatnulland passes it toRelation::getMorphedModel(). - The lookup
static::$morphMap[$alias]usesnullas an offset, and PHP 8.5 flags it.
Note that no Relation::morphMap() registration is needed to trigger this — the lookup happens whether or not you have aliases configured.
Regression coverage you can borrow
The PR adds a test that converts deprecations into exceptions, hydrates an empty collection via $refresh, and asserts the component still renders:
set_error_handler(function ($severity, $message) {
throw new \ErrorException($message, 0, $severity);
}, E_DEPRECATED | E_USER_DEPRECATED);
try {
$component->call('$refresh')->assertSee('count: 0');
} finally {
restore_error_handler();
}
Even if you never touch Livewire's internals, this is a handy pattern: temporarily promote E_DEPRECATED to exceptions in a test, run the suspect code path, and restore the handler afterwards. It would have caught this deprecation long before PHP 8.5 shipped.
Upgrade impact from v3.8.10
Practically none, in the best sense:
- No method signature, config, or view changes; the fix is invisible unless you were seeing the deprecation.
- On PHP 8.1–8.4, runtime behavior is effectively identical, because
getMorphedModel(null)already returnednulland the guard short-circuits to the same result. - On PHP 8.5, the deprecation warning for empty-collection hydration disappears.
- If your app or CI promotes deprecations to exceptions — a strict custom error handler, or tooling like Sentry or Flare capturing deprecations — requests that hydrated empty collections will stop tripping.
Pull the patch release as you would any other bug fix. Nothing in your application code needs to change.
Takeaways
- One-line fix:
EloquentCollectionSynth::hydrate()now skips the morph-map lookup whenmodelClassisnull, matching the guard already present indehydrate(). - Trigger: hydrating an empty Eloquent collection on PHP 8.5, on every rehydrating request — no morph map required.
- Why now: PHP 8.5 deprecates using
nullas an array offset, and the$morphMap[$alias]lookup hit that exact path. - Upgrade effort: zero code changes; a drop-in patch for apps currently on v3.8.10.
- Bonus: the PR's deprecation-to-exception test pattern is worth borrowing for your own regression suites.