@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.
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.
@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.
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.
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.
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
Written by
Writing and maintaining @LaravelMagazine. Host of "The Laravel Magazine Podcast". Pronouns: vi/vim.