paulund

Immutable Dates

Immutable Dates

Date handling is a frequent source of bugs. Laravel defaults to using Carbon, which is mutable — calling a method like addDay() modifies the original object in place. This can lead to hard-to-track errors when the same date instance is reused. Laravel's support for CarbonImmutable solves this cleanly.

Why Mutable Dates Cause Problems

With standard Carbon, every modification mutates the original:

$start = Carbon::parse('2024-06-01');
$end   = $start->addDays(7); // $start is now also 2024-06-08

echo $start->format('Y-m-d'); // 2024-06-08 — not what you expected
echo $end->format('Y-m-d');   // 2024-06-08

This is a classic source of bugs, especially when a date variable is shared across multiple calculations.

CarbonImmutable Returns New Instances

CarbonImmutable works identically to Carbon, but every method that would normally mutate the instance returns a brand new one instead:

use Carbon\CarbonImmutable;

$start = CarbonImmutable::parse('2024-06-01');
$end   = $start->addDays(7); // $start is untouched

echo $start->format('Y-m-d'); // 2024-06-01 — safe
echo $end->format('Y-m-d');   // 2024-06-08

Configuring Laravel to Use CarbonImmutable

Laravel provides a helper to switch the default date class application-wide. Add this to your AppServiceProvider:

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

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Date::use(CarbonImmutable::class);
    }
}

After this, Laravel's now(), model timestamps, and the date() helper all return CarbonImmutable instances.

Laravel's Date Helpers

Laravel ships with several helper functions that respect the configured date class:

// Returns the configured date class (Carbon or CarbonImmutable)
$now = now();

// Parse a date string using the configured class
$date = Date::parse('2024-12-25');

// Create from a format
$date = Date::createFromFormat('d/m/Y', '25/12/2024');

Prefer these helpers over calling Carbon::now() or CarbonImmutable::now() directly. They ensure your application stays consistent if you ever change the default date class.

Practical Example

A common pattern is calculating date ranges for reports:

// ❌ With mutable Carbon — the bug
$rangeStart = Carbon::now()->startOfMonth();
$rangeEnd   = $rangeStart->endOfMonth(); // Mutates $rangeStart too!

$query = Order::whereBetween('created_at', [$rangeStart, $rangeEnd]);

// ✅ With CarbonImmutable — safe and clear
$rangeStart = now()->startOfMonth();
$rangeEnd   = $rangeStart->endOfMonth(); // $rangeStart unchanged

$query = Order::whereBetween('created_at', [$rangeStart, $rangeEnd]);

When Mutability Is Actually Fine

There are cases where you intentionally want to build up a date step by step. In those situations, mutable Carbon is perfectly acceptable — just be explicit about it:

// Fine — you are deliberately mutating a local variable
$deadline = Carbon::now();
$deadline->addDays(7);
$deadline->setTime(23, 59, 59);

The key rule: if you pass a date to another function or reuse it after modification, use CarbonImmutable.

Summary

  • Switch your Laravel app to CarbonImmutable via Date::use() in AppServiceProvider.
  • Use now() and Date::parse() instead of calling Carbon directly.
  • Treat immutability as the safe default; reach for mutable Carbon only when you have a deliberate reason.