Skip to content

[6.x] Fix last login errors when the app uses CarbonImmutable - #15793

Open
jhhazelaar wants to merge 1 commit into
statamic:6.xfrom
jhhazelaar:fix-immutable-last-login
Open

jhhazelaar wants to merge 1 commit into
statamic:6.xfrom
jhhazelaar:fix-immutable-last-login

Conversation

@jhhazelaar

Copy link
Copy Markdown
Contributor

Apps that call Date::use(CarbonImmutable::class) get CarbonImmutable instances from now() and from Eloquent's datetime casts. The Laravel starter kits do this by default. Two places in the auth code only accept a mutable Carbon\Carbon, so they break in those apps:

Eloquent users: the Control Panel crashes right after logging in

When the user model casts last_login to datetime, Statamic\Auth\Eloquent\User::lastLogin() receives a CarbonImmutable. That fails the instanceof Illuminate\Support\Carbon check, so the date is cast to a string and parsed again with the model's date format (Y-m-d H:i:s). In the Control Panel, the Localize middleware sets the to-string format to ISO 8601, and the parse throws:

Carbon\Exceptions\InvalidFormatException: Unexpected data found. Trailing data

The first Control Panel page after a login renders the user's augmented data, so the error shows up as soon as last_login has been filled.

Passkeys: a TypeError on login and when saving an Eloquent passkey

Passkey::setLastLogin() is typed null|int|Carbon. It receives a CarbonImmutable in two places: when WebAuthn records a passkey login with now(), and when Eloquent\Passkey::setModel() passes the datetime cast from WebAuthnModel (which also runs after save()):

Statamic\Auth\WebAuthn\Passkey::setLastLogin(): Argument #1 ($time) must be of type Carbon\Carbon|int|null, Carbon\CarbonImmutable given

The fix

Like #3499, #11244 and #13760, this accepts CarbonInterface:

  • Eloquent\User::lastLogin() returns the date as it is when it is any Carbon instance, and only parses strings.
  • Passkey::setLastLogin() (the contract and the abstract class) accepts null|int|CarbonInterface and converts the date with Carbon::instance(). lastLogin(): ?Carbon stays the same, so File\Passkey and any code reading the value are unaffected. One caveat: a class that implements the Passkey contract directly (rather than extending Statamic\Auth\WebAuthn\Passkey) and declares setLastLogin(null|int|Carbon $time) would no longer be compatible with the contract. If you'd rather avoid that, the contract can stay as it is and only the abstract class gets widened. That works too, because an implementation may accept a wider type than its interface.

The tests cover an Eloquent user whose model casts last_login while the app uses immutable dates, setLastLogin() with a CarbonImmutable for both the file and Eloquent passkeys, and saving an Eloquent passkey with a last login. All of them fail without the fix.

Apps that call Date::use(CarbonImmutable::class) get immutable dates
from now() and from datetime casts. The Eloquent user's lastLogin()
then parsed the date again as a string, which fails in the Control
Panel, and Passkey::setLastLogin() threw a TypeError on passkey login
and when saving an Eloquent passkey.
@jhhazelaar
jhhazelaar marked this pull request as ready for review November 5, 2026 10:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant