skip to content

In Laravel, which collection methods such as transform() and push() change the collection in place, and what bugs does that cause in shared code?

level: seniorimportance: should knowfreq 35%

answer

  1. the docs' 'unlike most other methods' notes
  2. transform, push, put, forget, prepend
  3. pop, shift, pull return the item
  4. same instance travels by handle
  5. map's new collection holds the same objects

basics

~10 s

transform(), push(), put(), prepend(), forget(), pop(), shift(), pull() and splice() modify the collection itself. Because collections are objects passed by handle, a helper calling them changes the caller's data too.

solid answer

~40 s

Most collection methods return a new instance, but a group mutates `$this`: `transform()` replaces every item, `push()`/`prepend()`/`put()` add, `forget()` removes by key, and `pop()`, `shift()`, `pull()` and `splice()` remove and hand back what they removed. The first group returns `$this`, so it still chains and looks harmless; the removal group returns what was removed — an item, or for `splice()` a new collection of removed items — never the shortened collection, which breaks chains. Since a collection is an object, passing it to a helper or storing it on a service shares one instance, so `transform()` inside a report formatter rewrites the caller's raw data. Prefer `map()`, or copy first with `collect($c)`. And even `map()` is shallow: mutating a model inside its callback changes the shared model.

code

php · 17 lines
php
<?php

use Illuminate\Support\Collection;

function asPercentages(Collection $answers): Collection
{
    // Bug: transform() rewrites the caller's collection
    return $answers->transform(fn (array $a) => [...$a, 'score' => $a['score'] * 20]);
}

$answers = collect([['score' => 4], ['score' => 5]]);
$percent = asPercentages($answers);

echo $answers->sum('score'); // 180, not 9

// Fix: return a new collection
// return $answers->map(fn (array $a) => [...$a, 'score' => $a['score'] * 20]);

go deeper

for a junior

Recall that transform(), push(), put(), forget(), pop(), shift() and pull() change the collection itself, while map() and filter() do not.

for a middle

Explain what each mutating method returns — $this versus the removed item — and why a collection passed to a function is the same object.

for a senior

Diagnose a report that shows wrong totals because a helper mutated a shared collection, and fix it with map(), explicit copies or pure callbacks.

for a principal

Set conventions for collection ownership across services and long-lived workers, so in-place mutation is confined to local data the caller created.

## The default and its exceptions Laravel's collections documentation describes the class as immutable in general: most methods return a new instance and leave the original alone. It then marks a set of methods with notes such as "unlike most other collection methods, `transform` modifies the collection itself" and the same for `forget`. In the source these methods either assign to `$this->items` or call `offsetSet`/`offsetUnset` on the current object. | Method | What it changes | What it returns | |---|---|---| | `transform($cb)` | replaces every item with the callback result | `$this` | | `push(...$values)` | appends to the end | `$this` | | `prepend($value, $key = null)` | adds at the start | `$this` | | `put($key, $value)` | sets one key | `$this` | | `forget($keys)` | removes keys | `$this` | | `pop($count = 1)` | removes from the end | the item (or a collection when `$count > 1`) | | `shift($count = 1)` | removes from the start | the item (or a collection when `$count > 1`) | | `pull($key)` | removes one key | the removed value | | `splice($offset, …)` | removes (and optionally inserts) | a new collection of the removed items | `$collection[] = $x` and `unset($collection[$k])` go through `ArrayAccess` and mutate too. ## Why a mutation escapes its function A collection is a PHP **object**. Passing it to a function, returning it from a service or storing it in a property passes a **handle to the same object**, not a copy. So: 1. A controller loads survey answers into `$answers`. 2. It passes `$answers` to a `PercentageFormatter` that calls `$answers->transform(fn ($a) => …)` to turn scores into percentages. 3. Back in the controller, `$answers->sum('score')` now adds percentages, not raw scores. The formatter "worked", its tests passed in isolation, and the bug appears only where both uses meet. ## Chains that look right but are not - `$answers->push($late)->count()` — correct, `push()` returns `$this`, but the source collection now also contains `$late`. - `$first = $answers->shift()->score` — works, but only because `shift()` returns the **item**; `$answers` has lost its first element as a side effect. - `$answers->pull('draft')->values()` — fails, because `pull()` returns the removed value, not the collection. - `$answers->map(fn ($a) => …);` on its own line — the opposite mistake: the result is thrown away and nothing changes. ## Shallow immutability: map() is not a deep copy Returning a new collection copies the **array of handles**, not the objects. With Eloquent models: ```php <?php $doubled = $answers->map(function ($answer) { $answer->score *= 2; // mutates the shared model return $answer; }); // $answers now shows doubled scores as well ``` Both collections hold the same models, so this callback mutates the originals. Returning a new value — an array, a DTO, or `$answer->replicate()` when you truly need a model copy — keeps the pipeline pure. ## Defensive habits - Use **`map()`** instead of **`transform()`** unless you own the collection and want in-place replacement for memory reasons. - When a function must modify a collection it did not create, copy it first: `collect($answers)` or `$answers->collect()` returns a new base collection; `clone $answers` keeps the subclass. Both are shallow. - Name mutating helpers honestly (`normaliseInPlace`) and avoid them on collections held in long-lived services — under Octane a container singleton outlives the request, so an in-place change leaks into the next one. - In code review, treat `transform`, `push`, `put`, `forget` and `pull` on a parameter as a smell worth a comment. ## Where each group is useful Mutation is not wrong in itself. `push()` inside a loop that builds a local collection, `pull()` to consume an option from a local bag, or `transform()` over a large local collection to avoid a second copy are all fine. The bug class is **mutating a collection that someone else still reads**.

  • Why does calling pull() in the middle of a Laravel collection chain often break it?
    `pull($key)` removes the entry from the collection and returns the removed value, not the collection. The next method in the chain is therefore called on that value — often an array or a model — and fails or does something unrelated, while the collection has silently lost the key.
  • When is transform() the better choice than map() in Laravel?
    When you own the collection and nobody else reads the old values — for example a local collection you are about to return. `transform()` still builds the mapped array internally, but then assigns it to `$this->items`, so only one array stays referenced; with `map()` the source and the result both stay alive while either variable is in scope.

A collection passed around the app is like a shared spreadsheet link: map() is 'make a copy' and edit the copy, while transform() edits the sheet everyone else has open. The copy still links to the same attached files, though, so editing an attachment — a model inside the collection — changes it for everyone.

saying these in an interview costs you the question

  • Believes every collection method returns a new instance.
  • Thinks pop() and pull() return the shortened collection for chaining.
  • Says passing a collection to a function copies it.
  • Assumes map() deep-copies the objects so callbacks cannot affect the source.
  • Treats transform() as a faster alias of map() with no side effects.