May 8th, 2026

Laravel Framework: Skip Array Allocation in merge* Methods When Input Is Empty

Laravel Framework: Skip Array Allocation in merge* Methods When Input Is Empty
Sponsored by
Table of Contents

@olivier-zenchef merged PR #60008 into laravel/framework, adding four early-return guards that skip unnecessary array allocations in mergeFillable, mergeAppends, mergeHidden, and mergeVisible when the input array is empty.


The Regression

PR #59404, released in v13.3.0, fixed a trait initializer collision by replacing direct empty-checks on $this->fillable, $this->appends, $this->hidden, and $this->visible with calls to the corresponding merge* methods. The collision fix itself is correct.

The side effect is that those merge* methods now run on every model construction, including the common case where the input is an empty array. Most models declare no #[Appends] attribute, so static::resolveClassAttribute(Appends::class, 'columns') ?? [] evaluates to [] on virtually every construction.

The previous implementation of each method looks like this:

1public function mergeFillable(array $fillable)
2{
3 $this->fillable = array_values(array_unique(array_merge($this->fillable, $fillable)));
4 return $this;
5}

Even when $fillable is [], PHP still allocates three transient arrays per call: one from array_merge, one from array_unique, one from array_values. Multiply that by four methods and by every model construction in a request, and the cost adds up fast.


Measured Impact

@olivier-zenchef reproduced the regression with the laravel-eloquent-bench suite: a synthetic model with 120 fillable properties, 13 casts, 50 relations, and 4 traits, constructed 100,000 times on PHP 8.4.14.

Version Peak memory Change vs v13.1.1
v13.1.1 148 MB baseline
v13.2.0 148 MB 0
v13.3.0 (PR #59404 lands) 400 MB +252 MB (+170%)
v13.6.0 400 MB +252 MB (+170%)

With the early-return guards in place, peak memory returns to 148 MB. At the scale many production apps operate, a 252 MB jump in peak memory per process is enough to breach PHP's default memory_limit.


The Fix

Each of the four methods gets an identical guard:

1public function mergeFillable(array $fillable)
2{
3 if ($fillable === []) {
4 return $this;
5 }
6 
7 $this->fillable = array_values(array_unique(array_merge($this->fillable, $fillable)));
8 return $this;
9}

The same pattern applies to mergeAppends, mergeHidden, and mergeVisible. Behavior is unchanged for any non-empty input: the merge, dedup, and reindex still run exactly as before. The trait-initializer-collision fix from PR #59404 is fully preserved because that fix only matters when there is something to merge.


Tests

The PR adds four test_merge_*_with_empty_array_is_noop cases in tests/Database/DatabaseEloquentModelAttributesTest.php. Each asserts that calling the method with [] returns $this and leaves the underlying property reference unchanged. Existing tests from PR #59404 continue to pass.


Who Should Care

Any application running Laravel 13.3.0 through 13.6.0 that constructs a high volume of Eloquent models per request should upgrade to pick up this fix. The regression is invisible in development but measurable in production under load, particularly for apps that hydrate large result sets or run queued jobs that process many models in a single process.

If you enjoyed this article, please consider supporting our work for as low as $5 / month.

Sponsor
Marian Pop

Written by

Marian Pop

Writing and maintaining @LaravelMagazine. Host of "The Laravel Magazine Podcast". Pronouns: vi/vim.

Comments

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.