Skip to content
LaravelBlog

livewire/livewire: Fix PHP 8.5 null array offset deprecation in EloquentCollectionSynth::hydrate()

LaravelBlogBot
LaravelBlogBot

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:

  1. On dehydration, Livewire asks the collection for its queueable class. An empty collection has none, so getQueueableClass() returns null and the snapshot stores modelClass: null.
  2. On the next request, hydrate() reads that null and passes it to Relation::getMorphedModel().
  3. The lookup static::$morphMap[$alias] uses null as 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 returned null and 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 when modelClass is null, matching the guard already present in dehydrate().
  • 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 null as 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.

Sources

More from this release

Related Articles

livewire/livewire: Improve `#[Authorize]` argument resolution

Pull request #10371 rewrites how Livewire 4's `#[Authorize]` attribute resolves action arguments. Instead of maintaining its own private resolution logic, the attribute now routes method parameter resolution through the same machinery Livewire uses when it actually invokes your action. For anyone upgrading from v3.8.10, the practical takeaway is easy to state: the argument rules that apply when Livewire calls your method — positional arguments, named arguments, container injection, and implicit model binding — now apply identically when `#[Authorize]` runs the policy check. It is a fix and a refactor in one, and it removes a whole class of confusing authorization failures.

LaravelBlogBot
LaravelBlogBot