nesbot/carbon
Carbon is a popular PHP date/time library that extends DateTime with a fluent, human-readable API for parsing, formatting, comparing, and manipulating dates, plus localization, relative time, intervals, and test helpers. It is widely used in Laravel and standalone PHP projects.
Carbon is already installed in every Laravel app (nesbot/carbon ships as a framework dependency via illuminate/support), so there is nothing to add — the work is learning which of the three classes to type-hint:
Carbon\Carbon — mutable, what Illuminate\Support\Carbon (and therefore now(), model datetime casts) extends.Carbon\CarbonImmutable — every modifier returns a copy; use for value objects, DTOs and anything passed across layers.Carbon\CarbonInterval / Carbon\CarbonPeriod — durations and date ranges, the two most under-used pieces.First look: run composer show nesbot/carbon to see which major you are on, then read the versioned docs (carbon.nesbot.com) for that major, and check vendor/nesbot/carbon/src/Carbon/Lang/<locale>.php when a translation looks wrong — that file is the source of truth for localized output.
First use case worth doing on day one: replace string date math in one feature with Carbon.
// before
$expires = date('Y-m-d H:i:s', strtotime($created) + 7 * 86400);
// after
$expires = CarbonImmutable::parse($created)->addWeek();
$expires->isPast(); // bool
$expires->diffForHumans(); // "in 6 days"
$expires->isoFormat('LLL'); // localized, per-user locale
Then confirm your model casts and config/app.php timezone are what you think they are — almost every Carbon bug report is really a timezone or cast bug.
Casts are the integration point. Prefer immutable_datetime / immutable_date for new columns so $model->starts_at->addDay() can never mutate a loaded attribute (which Laravel will happily persist as dirty). Use datetime:Y-m-d only for presentation-shaped casts, and format in the resource layer instead.
Parse user input explicitly, never with parse().
$date = CarbonImmutable::createFromFormat('!d/m/Y', $request->input('date'), $request->user()->timezone);
abort_unless($date, 422); // 3.x returns null/throws InvalidFormatException depending on method
! (or trailing |) stops missing time components being filled from now — a classic source of "the date saved with the current time" bugs.
Localization is per-instance, not global. Set nothing globally; carry the user's locale:
$date->locale($user->locale)->isoFormat('LLLL'); // Tuesday, 3 September 2024 14:05
$date->locale($user->locale)->diffForHumans(['parts' => 2, 'short' => true]);
$date->locale($user->locale)->calendar(); // "Today at 14:05"
Since 3.9 Carbon also honours Laravel's app.fallback_locale for missing strings, so fr_CA partially falling back to fr→en works without you shimming translations.
Durations belong in CarbonInterval, not raw seconds.
CarbonInterval::seconds($uptime)->cascade()->forHumans(['short' => true]); // "2d 4h"
CarbonInterval::make('1 month 3 days')->totalDays;
Ranges/reports use CarbonPeriod.
foreach (CarbonImmutable::parse($from)->toPeriod($to, '1 week') as $week) { ... }
CarbonPeriod::create($start, '1 day', $end)->filter(fn ($d) => $d->isWeekday());
Business logic: use the built-ins. addWeekdays(), subWeekdays(), isWeekend(), next(Carbon::MONDAY), startOfWeek(Carbon::SUNDAY), isSameDay(), betweenIncluded(), isNowOrPast() (3.9). These beat hand-rolled %w arithmetic and respect locale's weekStartsAt.
App-wide helpers via macros, registered in a service provider:
CarbonImmutable::macro('quarter', fn () => $this->quarter); // careful: ->quarter exists as getter
Illuminate\Support\Carbon::macro('toLocalInput', fn () => $this->format('Y-m-d\TH:i'));
Only register macros in boot() (not register()) and register the same macro on each class family you actually use (Carbon, CarbonImmutable); they do not share a macro registry.
Testing: Laravel's $this->travel(5)->days(), travelTo(), freezeTime() wrap Carbon::setTestNow(). Prefer those over calling setTestNow() directly so teardown happens for you; on 3.11.1+ withTestNow() restores the previous frozen state instead of clearing it, which matters in nested test helpers.
diffInDays() etc. now return floats and are signed by default (negative for past dates), where v2 gave absolute ints. $days === 0 and int type hints break silently. Wrap abs(), (int), or pass the absolute flag explicitly. Also diffInDays() across a DST boundary is not a whole number — for calendar-day counts compare startOfDay() values rather than raw timestamps.Jan 31 ->addMonth() gives March 3. Use addMonthNoOverflow(), or 3.12's overflow: mode / anchorDay when you need "same day-of-month or end of month" semantics for billing cycles.->locale() mutates in the mutable class — now()->locale('fr') changes the instance you may have just handed to a view. Either ->copy()->locale(...) or use CarbonImmutable everywhere and stop thinking about it.(string) $date output is governed by Carbon::setToStringFormat() (and Laravel's Illuminate\Support\Carbon::serializeUsing() for JSON). If your API's date format suddenly changes, look in a service provider, not the model.CarbonPeriod/CarbonInterval serialized payloads changed through 3.10–3.12 (v2 compatibility, inversion, recurrence limits). Don't serialize periods into queues/cache across a version bump — rehydrate from start/end/interval.InvalidFormatException — it carries the expected format and the offending string. Carbon::getLastErrors() returns false (not an empty array) since 3.11.1, which flips naive if (count(...)) checks.plus()/minus() were added, removed in 3.12.1 then restored improved in 3.12.2–3.12.3). Pin with a caret range, commit composer.lock, and read the changelog before bumping if you depend on human-diff strings or locale output in tests.^3.0; on older Laravel you're on the maintained 2.7x line. Check with composer why nesbot/carbon before assuming you can jump majors.Carbon::macro/mixin → custom Carbon subclass → editing src/Carbon/Lang/*.php only as an upstream PR. Don't patch vendor files for translation fixes; the project's release cadence (and 3.9.1's is('02-29') and is-month overflow fixes) shows those land quickly upstream.How can I help you explore Laravel packages today?