In Laravel, which collection methods such as transform() and push() change the collection in place, and what bugs does that cause in shared code?
answer
- the docs' 'unlike most other methods' notes
- transform, push, put, forget, prepend
- pop, shift, pull return the item
- same instance travels by handle
- map's new collection holds the same objects
basics
~10 stransform(), 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 sMost 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
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
Recall that transform(), push(), put(), forget(), pop(), shift() and pull() change the collection itself, while map() and filter() do not.
Explain what each mutating method returns — $this versus the removed item — and why a collection passed to a function is the same object.
Diagnose a report that shows wrong totals because a helper mutated a shared collection, and fix it with map(), explicit copies or pure callbacks.
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.