This checklist covers breaking changes that cannot be fully automated by the Rector rules. After running the Rector rules, work through each item below.
These won't throw errors but will produce different results at runtime.
What changed: diffIn* methods now return signed floats instead of positive integers.
What Rector did: Wrapped every diffIn* call in (int) abs(...).
What you need to do:
- Review each
(int) abs(...)wrapper that Rector added - Determine if the sign (positive/negative) actually matters for your logic
- If you're doing
$deadline->diffInDays($now)and checking if it's > 0 to mean "in the future", the sign is now meaningful and you might want the signed float - If you're just displaying "5 days ago", the abs+int wrapper is correct
- If you're doing
- Determine if you need float precision (sub-second accuracy)
- Financial calculations, rate limiting, or precise timing may benefit from the new float behavior
- Remove the
(int) abs(...)wrapper where the new Carbon 3 behavior is actually preferred - Consider whether DST-aware vs timestamp-based diff matters for your use case (Carbon 3
diffIn*ignores DST by using timestamps)
What changed: Default timezone changed from date_default_timezone_get() to "UTC".
What Rector did: Added date_default_timezone_get() as the second argument.
What you need to do:
- Verify that
date_default_timezone_get()is actually what you want at each call site - If some call sites should actually use UTC (e.g., processing API timestamps that are already UTC), change them to
'UTC'explicitly - If your app already sets
date_default_timezone_set('UTC')globally, you can remove the addeddate_default_timezone_get()arguments entirely - Check for
createFromTimestampcalls inside libraries/packages you control that may have been missed
What Rector can't catch: If diffIn* results are stored in variables and used elsewhere:
$diff = $a->diffInHours($b); // Rector wraps this
$result = someFunction($diff); // But what about code expecting an int here?
- Search for variables that receive
diffIn*results and trace their usage - Check function signatures and type hints that receive diff values
- Check database columns that store diff values (int columns will reject floats)
What changed: Methods like eq(), gt(), lt(), gte(), lte(), ne() etc. now require DateTimeInterface|string — passing bool or null throws TypeError.
What Rector can't do: Can't determine at static analysis time what values flow into comparison methods.
What you need to do:
- Search for comparison method calls:
->eq(,->gt(,->lt(,->gte(,->lte(,->ne(,->equalTo(,->greaterThan(,->lessThan( - Check if any of these receive values that could be
nullorfalse:// This will now throw TypeError in Carbon 3: $date->gt($nullableDate); // if $nullableDate is null $date->eq($someFlag); // if $someFlag is bool
- Add null checks before comparisons:
if ($otherDate !== null && $date->gt($otherDate)) { ... }
- Check for comparisons in conditional chains where variables might not be initialized
What changed: These methods return null instead of false on failure.
What Rector did: Converted === false to === null for direct comparisons with Carbon create calls.
What Rector can't catch:
- Variables assigned from
create()/createFromFormat()that are checked for falsiness elsewhere - PHPDoc
@return Carbon|falseannotations on methods that wrap Carbon creates - Type-hinted function parameters that expect
Carbon|false - Ternary expressions:
$date ?: 'default'(still works for null, but semantics differ) - Code that checks
if ($date)— this still works for both false and null, so it's fine - Stored results compared later:
$date = Carbon::createFromFormat(...); ... if ($date === false)where$dateis used many lines later
What changed: new CarbonTimeZone() without arguments now throws. A timezone name is required.
What you need to do:
- Search for
new CarbonTimeZone()with no arguments - Replace with
new CarbonTimeZone(date_default_timezone_get())or an explicit timezone
What changed: Creating a timezone with an invalid name always throws an exception, even with strict mode disabled.
What you need to do:
- If your code relies on muting timezone errors via
Carbon::useStrictMode(false), this no longer suppresses timezone errors - Add try-catch blocks around user-provided timezone strings
- Validate timezone strings before passing them to Carbon
What changed: These global static setters were removed.
What Rector did: Removed the calls and added TODO comments.
What you need to do:
- Find all
startOfWeek()/endOfWeek()calls that relied on the global setting - Pass the day explicitly:
->startOfWeek(\Carbon\WeekDay::Monday) - Or use locale-aware behavior:
->locale('en_US')->startOfWeek() - If you need a custom global default, use custom locale translations:
\Carbon\Translator::get('en_US@Custom')->setTranslations([ 'first_day_of_week' => Carbon::MONDAY, ]);
- Update any tests that depend on week start/end behavior
What changed: formatLocalized() (OS-dependent strftime()) replaced by isoFormat() (Carbon's embedded translations).
What Rector did: Renamed the method and converted common strftime tokens.
What you need to do:
- Search for
TODO: [Carbon 3 migration]comments left by Rector - Verify converted format strings produce the expected output
- Manually convert any tokens Rector flagged as unconvertible:
strftime Meaning isoFormat equivalent %cPreferred date & time LLLL(locale-dependent)%xPreferred date L(locale-dependent)%XPreferred time LTorLTS%r12-hour time with AM/PM h:mm:ss A%R24-hour time HH:MM HH:mm%T24-hour time HH:MM:SS HH:mm:ss%DDate as MM/DD/YY MM/DD/YY%FDate as YYYY-MM-DD YYYY-MM-DD - Test that locale-dependent formatting matches your expectations (isoFormat uses Carbon's translations, not OS locale)
- If you were relying on
setlocale()for formatting, ensure you're settingCarbon::setLocale()instead
What changed: CarbonPeriod->start and CarbonPeriod->end are now immutable (inherited from DatePeriod). setStartDate()/setEndDate() still work but don't update ->start/->end properties.
What you need to do:
- Search for direct property access:
$period->startand$period->end - If you modify periods with
setStartDate()/setEndDate(), usegetStartDate()/getEndDate()instead of->start/->endto read the current values - Consider migrating to
CarbonPeriodImmutablefor full compatibility - Check if you pass
CarbonPeriodwhereDatePeriodis type-hinted (now works natively)
What Rector did: Replaced with CarbonImmutable::startOfTime() / endOfTime().
What you need to verify:
- Old methods returned system-dependent DateTime min/max; new methods return fixed dates (
0001-01-01and9999-12-31) - If you were comparing dates against these boundaries, verify the fixed dates work for your use case
- New methods only exist on
CarbonImmutable, notCarbon— verify mutable Carbon code handles the immutable return type
What changed: Addition/subtraction methods may no longer accept string arguments silently.
What you need to do:
- Check for
add*()/sub*()calls where the value comes from config, env, or user input (may be string) - Cast to int explicitly:
$date->addMinutes((int) $config['minutes'])
- Update
@return Carbon|falseto@return Carbon|nullin your wrapper methods - Update
@param intto@param float|intfor variables receivingdiffIn*results (if you removed the(int)cast) - Update any
@varannotations that reference removed methods
- Run your full test suite after applying Rector rules
- Pay special attention to:
- Date comparison tests
- Tests involving timestamps and timezone conversion
- Tests that check diff values (now float, potentially negative)
- Tests involving week start/end
- Tests that check for
falsereturns from create methods
- Update test assertions from
assertSame(5, $diff)toassertSame(5, (int) abs($diff))or update to expect the new float behavior
- Check if packages you depend on are Carbon 3 compatible
- Common packages to check:
spatie/laravel-permissionspatie/periodlaravel/cashier- Any package that type-hints
Carbon|falseor callsdiffIn*methods
- If a package passes
nullorboolto Carbon comparison methods, it will break
- Check if
diffIn*results are stored in integer database columns - Check if serialized Carbon objects are stored (e.g., in cache, sessions) — clear them after upgrading
-
CarbonPeriodserialization may have changed due toDatePeriodinheritance
Run these to find potential issues:
# Find all diffIn* calls (verify the wrapping is correct)
grep -rn '->diffIn' src/ app/
# Find createFromTimestamp without explicit timezone
grep -rn 'createFromTimestamp(' src/ app/ | grep -v 'date_default_timezone_get\|UTC'
# Find comparison methods that might receive null
grep -rn '->eq(\|->gt(\|->lt(\|->gte(\|->lte(\|->ne(' src/ app/
# Find CarbonPeriod property access
grep -rn '\$.*->start\b\|\$.*->end\b' src/ app/ | grep -i period
# Find remaining false comparisons
grep -rn '=== false\|!== false' src/ app/ | grep -i carbon
# Find CarbonTimeZone constructor
grep -rn 'new CarbonTimeZone' src/ app/
# Find addMinutes etc. with string args (from config)
grep -rn "->add\(Minutes\|Hours\|Days\|Weeks\|Months\|Years\)" src/ app/
# Find all TODOs left by Rector
grep -rn 'TODO: \[Carbon 3 migration\]' src/ app/
# Find remaining formatLocalized calls
grep -rn 'formatLocalized\|setUtf8\|setWeekStartsAt\|setWeekEndsAt' src/ app/| Feature | Carbon 2 | Carbon 3 |
|---|---|---|
createFromTimestamp($ts) timezone |
date_default_timezone_get() |
"UTC" |
diffInSeconds($other) return |
int (positive) |
float (signed) |
createFromFormat() failure |
Carbon|false |
Carbon|null |
isSameDay() (no args) |
Compare to "now" | Throws exception |
minValue() / maxValue() |
Available | Removed |
formatLocalized() |
Available | Removed |
setUtf8() |
Available | Removed |
setWeekStartsAt() |
Deprecated | Removed |
setWeekEndsAt() |
Deprecated | Removed |
Named arg tz: |
Accepted | Renamed to timezone: |
eq(null) / gt(false) |
Returns arbitrary result | TypeError |
new CarbonTimeZone() |
Allowed | Throws (name required) |
CarbonPeriod extends |
Nothing | DatePeriod |
| PHP minimum | 7.1.8 | 8.1 |