skip to content

In Laravel, why does $returnBy = $placedAt->addDays(30) also move $placedAt, and how does Date::use(CarbonImmutable::class) prevent it?

level: seniorimportance: should knowfreq 30%

answer

  1. Illuminate\Support\Carbon is mutable
  2. addDays() returns $this
  3. copy() or toImmutable()
  4. DateFactory: class, callable or factory
  5. type-hint CarbonInterface

basics

~10 s

Laravel's default Carbon class is mutable: addDays() changes the object and returns it, so both variables hold one moved date. Date::use(CarbonImmutable::class) makes now(), today() and Eloquent dates immutable, so arithmetic returns new objects.

solid answer

~40 s

By default `now()`, `today()`, `Date::parse()` and Eloquent's date attributes create `Illuminate\Support\Carbon`, which extends the **mutable** `Carbon\Carbon`: `addDays()` changes the instance and returns `$this`. So after `$placedAt = $order->created_at; $returnBy = $placedAt->addDays(30);` both variables point at one object, and the page shows the order as placed on the return deadline. The model itself is safe — each read of `$order->created_at` builds a fresh Carbon — but any variable you reuse is not. Locally you write `->copy()->addDays(30)` or `->toImmutable()`. Globally, `Date::use(CarbonImmutable::class)` in `AppServiceProvider::boot()` makes `DateFactory` create `Carbon\CarbonImmutable` for the helpers and for Eloquent, so every `add*`/`sub*` returns a new object. The migration cost: unassigned arithmetic becomes a silent no-op, and hints on `Illuminate\Support\Carbon` must widen to `CarbonInterface`.

code

php · 15 lines
php
<?php

use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Date;

$placed = now();
$due = $placed->addDays(14);
var_dump($placed === $due);          // bool(true): same mutable object

Date::use(CarbonImmutable::class);   // normally in AppServiceProvider::boot()

$placed = now();
$due = $placed->addDays(14);
var_dump($placed === $due);          // bool(false): a new instance
var_dump($placed instanceof CarbonImmutable); // bool(true)

go deeper

for a junior

Remember that Laravel's default Carbon is mutable, so addDays() changes the variable you called it on unless you copy it first.

for a middle

Explain how now(), today() and Eloquent all build dates through the Date factory, and what Date::use(CarbonImmutable::class) changes.

for a senior

Diagnose wrong dates caused by one mutable object shared between variables or bindings, and plan the switch to immutable dates: unassigned calls, type hints, tests.

for a principal

Decide whether immutability is an application-wide rule and how packages and teams type-hint dates so the rule holds across boundaries.

## The bug ```php <?php $placedAt = $order->created_at; $deliverBy = $placedAt->addDays(5); // intends a new date $returnBy = $placedAt->addDays(30); // intends another new date // $placedAt, $deliverBy and $returnBy are now the same object: placed + 35 days ``` `Illuminate\Support\Carbon` extends **`Carbon\Carbon`**, which is **mutable**: `addDays()` changes the instance and returns `$this`. Every variable above holds a handle to the same object, so the order page shows the order placed, delivered and returnable on one wrong date. Two details make the bug easy to misdiagnose: - **The model is not changed.** Eloquent builds a fresh Carbon from the stored value each time you read `$order->created_at`, so a second read looks correct and `save()` does not persist the shift. The damage is to your local variables. - **Query bindings share it too.** `->whereBetween('created_at', [$from, $from->addDay()])` puts the same object in both slots, so the range collapses to a single instant. ## Local fixes - `->copy()->addDays(14)` — clone first, then mutate the clone; - `->toImmutable()->addDays(14)` — convert to `CarbonImmutable`, whose methods return new instances; - avoid arithmetic on values you do not own; compute from `now()` or a fresh parse. These work, but depend on every developer remembering them on every line. ## The global switch: Date::use() Laravel creates dates through **`Illuminate\Support\DateFactory`**, exposed as the `Date` facade. The helpers `now()` and `today()` call `Date::now()` and `Date::today()`, and Eloquent's attribute casting builds dates with `Date::instance()`, `Date::parse()` and `Date::createFromTimestamp()`. `DateFactory::use($handler)` changes what all of those produce: | Handler passed to `Date::use()` | Effect | |---|---| | a class name, e.g. `CarbonImmutable::class` | dates are created as that class | | a callable | each date is created as `Illuminate\Support\Carbon`, then passed through the callable | | a `Carbon\Factory` instance | the factory creates dates, with its own settings | | anything else | `InvalidArgumentException` | `Date::useDefault()` restores the default class. ```php <?php namespace App\Providers; use Carbon\CarbonImmutable; use Illuminate\Support\Facades\Date; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Date::use(CarbonImmutable::class); } } ``` After this, `$placedAt->addDays(30)` returns a **new** `CarbonImmutable` and leaves `$placedAt` alone, and model attributes such as `created_at` come back as `CarbonImmutable` too. ## What changes when you switch 1. **Unassigned arithmetic stops working.** `$date->addDay();` on its own line used to move `$date`; with immutable dates it computes a new value and discards it. Search for such statements before switching. 2. **Type hints.** Code or packages that type-hint `Illuminate\Support\Carbon` or `Carbon\Carbon` receive a `CarbonImmutable` and throw a `TypeError`. Hint `Carbon\CarbonInterface` instead, which both implement. 3. **Tests and helpers** that relied on mutating a shared "now" need to reassign. 4. **Consistency.** Every date in the app now behaves the same way, which is the point: no more guessing whether a given instance is safe to modify. ## Per-attribute alternatives If a global switch is too big a step, the model layer can make individual attributes immutable through its cast types; that is the casts topic. `Date::use()` is the **application-wide** choice, and it also covers dates that never touch a model, such as values from `now()` in services and jobs. ## Spotting the bug in review Signs that mutation is about to bite: 1. a date stored in a variable and then used as the **base for several** calculations (`$start->addWeek()` for one value, `$start->addMonth()` for another); 2. the same date object passed twice in one expression, such as a `whereBetween` range built from `$from` and `$from->addDay()`; 3. dates held on **long-lived objects** — a singleton service, a static property, a queued job's constructor argument reused across attempts; 4. helper methods that take a date parameter and "adjust" it before using it. Each is safe with immutable dates and needs `copy()` with mutable ones. A test that asserts the original value is unchanged after a calculation catches the regression cheaply. ## When to recommend it - new applications: switch on day one, when there is no mutating code to migrate; - existing applications: switch after a search for unassigned `add*`/`sub*`/`set*` calls and widening type hints, with tests around billing and scheduling logic; - libraries and packages: do not assume either class; type-hint `CarbonInterface` and always use return values.

  • After Date::use(CarbonImmutable::class) in Laravel, why might a package throw a TypeError?
    Dates are now `Carbon\CarbonImmutable`, which is not a subclass of `Carbon\Carbon` or `Illuminate\Support\Carbon`. Any method type-hinted on those mutable classes rejects the immutable instance. The fix is to type-hint `Carbon\CarbonInterface`, which both classes implement.
  • Does Date::use() in Laravel affect Eloquent timestamps, or only the now() helper?
    Both. Eloquent turns stored values into dates through the `Date` factory — `Date::instance()`, `Date::parse()` and `Date::createFromTimestamp()` — so the configured class applies to `created_at`, `updated_at` and date-cast attributes as well as to `now()` and `today()`.

A mutable date is a single wall calendar with one pin: moving the pin to mark the return deadline also moves the 'order placed' marker, because there is only one pin. Immutable dates hand you a fresh printout for each marker.

saying these in an interview costs you the question

  • Believes addDays() always returns a new Carbon instance in Laravel.
  • Thinks Date::use() only changes the now() helper, not model dates.
  • Switches to immutable dates without checking for unassigned add*/sub* calls.
  • Type-hints Illuminate\Support\Carbon in shared code after going immutable.
  • Builds a whereBetween range from $from and $from->addDay() with mutable dates.