Skip to content

Latest commit

 

History

History
270 lines (194 loc) · 12.2 KB

File metadata and controls

270 lines (194 loc) · 12.2 KB

Carbon 2 → Carbon 3: Manual Migration Checklist

This checklist covers breaking changes that cannot be fully automated by the Rector rules. After running the Rector rules, work through each item below.


Critical Priority (Silent Behavior Changes)

These won't throw errors but will produce different results at runtime.

[ ] 1. diffIn* — Review Every Wrapped Call

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
  • 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)

[ ] 2. createFromTimestamp Timezone — Verify All Call Sites

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 added date_default_timezone_get() arguments entirely
  • Check for createFromTimestamp calls inside libraries/packages you control that may have been missed

[ ] 3. Indirect diffIn* Usage via Variables

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)

High Priority (Will Cause Errors)

[ ] 4. Strong Typing — Comparison Methods No Longer Accept bool/null

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 null or false:
    // 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

[ ] 5. create() / createFromFormat() Return Type: false → null

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|false annotations 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 $date is used many lines later

[ ] 6. CarbonTimeZone Constructor — No Longer Accepts Zero Arguments

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

[ ] 7. Invalid Timezone Names Now Always Throw

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

Medium Priority (Deprecations & Removals)

[ ] 8. setWeekStartsAt() / setWeekEndsAt() — Manual Migration Required

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

[ ] 9. formatLocalized() Format String Review

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
    %c Preferred date & time LLLL (locale-dependent)
    %x Preferred date L (locale-dependent)
    %X Preferred time LT or LTS
    %r 12-hour time with AM/PM h:mm:ss A
    %R 24-hour time HH:MM HH:mm
    %T 24-hour time HH:MM:SS HH:mm:ss
    %D Date as MM/DD/YY MM/DD/YY
    %F Date 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 setting Carbon::setLocale() instead

[ ] 10. CarbonPeriod Now Extends DatePeriod — Immutable Properties

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->start and $period->end
  • If you modify periods with setStartDate()/setEndDate(), use getStartDate()/getEndDate() instead of ->start/->end to read the current values
  • Consider migrating to CarbonPeriodImmutable for full compatibility
  • Check if you pass CarbonPeriod where DatePeriod is type-hinted (now works natively)

[ ] 11. minValue() / maxValue() — Behavioral Difference

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-01 and 9999-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, not Carbon — verify mutable Carbon code handles the immutable return type

Low Priority (Cleanup)

[ ] 12. addMinutes() / addHours() etc. — String Arguments

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'])

[ ] 13. PHPDoc and Type Annotations

  • Update @return Carbon|false to @return Carbon|null in your wrapper methods
  • Update @param int to @param float|int for variables receiving diffIn* results (if you removed the (int) cast)
  • Update any @var annotations that reference removed methods

[ ] 14. Test Suite

  • 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 false returns from create methods
  • Update test assertions from assertSame(5, $diff) to assertSame(5, (int) abs($diff)) or update to expect the new float behavior

[ ] 15. Third-Party Packages

  • Check if packages you depend on are Carbon 3 compatible
  • Common packages to check:
    • spatie/laravel-permission
    • spatie/period
    • laravel/cashier
    • Any package that type-hints Carbon|false or calls diffIn* methods
  • If a package passes null or bool to Carbon comparison methods, it will break

[ ] 16. Database & Serialization

  • 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
  • CarbonPeriod serialization may have changed due to DatePeriod inheritance

Search Commands

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/

Quick Reference: Carbon 2 vs Carbon 3

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