Every PHP team eventually ships a billing bug that only reproduces twice a year. Mine was an invoice that landed on the wrong day for users in Santiago: the job ran at 00:30 local time, the server stored UTC, Chile shifted its offset, and strtotime('+1 day') cheerfully produced a timestamp 23 hours later. The fix was not a cleverer string function — it was replacing the ad-hoc date handling with a library that models time zones as first-class data.
Choosing that library in 2026 comes down to three real contenders: Carbon (16,585 GitHub stars), Chronos (1,362 stars) and Brick\DateTime (361 stars). All three are actively maintained — Carbon, Chronos and Brick all pushed commits within the last three weeks of October 2026 — and all three are MIT licensed, so license politics will not save you from making a decision.
Here is the decision, made for you, plus the code and the traps that the README files leave out.
TL;DR: The 10-Second Verdict
Use Carbon if you are building an application — it has the widest API surface, first-class locale support, human-readable diffs (“3 hours ago”), test-time freezing, and every Laravel/Symfony tutorial assumes it. Use Chronos if you want immutability by default and a CakePHP-grade, dependency-light core — its ChronosDate/ChronosTime split in the 3.x line is the cleanest domain-date model of the three. Use Brick\DateTime if you are writing a library, a financial system, or anything that must survive an audit — zero runtime dependencies, strict types, explicit value objects, and no magic method that silently mutates a shared instance.
If you only remember one rule: never let mutable date objects cross a service boundary. Carbon’s default object is mutable; that single fact causes more production surprises than every timezone bug combined.
How the Three Libraries Compare
| Dimension | Carbon 3.x | Chronos 3.x | Brick\DateTime 0.8 |
|---|---|---|---|
| GitHub stars | 16,585 | 1,362 | 361 |
| Last push | 2026-10-31 | 2026-10-21 | 2026-10-10 |
| License | MIT | MIT | MIT |
| Default mutability | Mutable (Carbon), immutable variant available | Immutable | Immutable (only option) |
| PHP support | 8.1+ | 8.1+ | 8.0+, 64-bit required |
| Runtime dependencies | symfony/translation-contracts, psr/clock, doctrine types | None beyond psr/clock | None |
| Locale-aware formatting | Yes, via ->locale() and translations | Minimal | No (formatting only) |
| Human-readable diffs | Yes (diffForHumans()) | No | No |
| Separate date / time / duration types | No (all Carbon objects) | Yes (ChronosDate, ChronosTime, ChronosPeriod) | Yes (LocalDate, LocalTime, Duration, Instant, ZonedDateTime) |
| Test clock freezing | Carbon::setTestNow() | Chronos::setTestNow() | Inject Clock |
| DST-safe arithmetic | Yes | Yes | Yes, and explicit |
| Learning curve | Lowest | Medium | Highest |
Two rows deserve emphasis. First, dependencies: Carbon pulls in translation contracts even if you never localize, which matters in Lambda-style deployments where every autoloaded file counts against cold-start. Second, type separation: Brick and Chronos can express “a date with no time and no zone” natively. Carbon cannot — a Carbon instance is always a date-time with a zone, which is exactly the abstraction leak that turns an invoice date into a timestamp at midnight UTC and shifts it a day for half the planet.
Decision Matrix: Pick by Use Case
| Your situation | Recommended | Why |
|---|---|---|
| Laravel or Symfony app with heavy UI date formatting | Carbon | diffForHumans(), locale bundles, and Blade helpers already assume it |
| Multi-tenant SaaS with billing periods | Chronos | Immutable by default; ChronosDate prevents time-of-day contamination |
| Financial ledger, audit trail, or public library | Brick\DateTime | Zero dependencies, strict value objects, no hidden mutation |
Legacy codebase migrating off DateTime | Carbon | Closest API surface, Carbon::instance() converts in one call |
| Date-only domain models (holidays, schedules, birthdays) | Chronos or Brick | Both have a real date type; Carbon does not |
| You need Persian, Arabic, or Chinese month names | Carbon | Bundled translations; the others require custom formatters |
| You need maximum control over DST gaps and overlaps | Brick\DateTime | Explicit ZonedDateTime semantics, no silent normalization you did not ask for |
Carbon: The Batteries-Included Workhorse
Carbon’s pitch is simple: it is DateTime with a human API. Installation is a single Composer line, and the object behaves like the native class it extends, so existing code keeps working while you upgrade incrementally.
| |
| |
Three things make Carbon pleasant in practice. CarbonImmutable gives you value semantics without leaving the family — the setters return new instances, so $a->addDay() never surprises a caller holding $a. CarbonPeriod removes the off-by-one bugs that show up in every hand-written date-range loop. And Carbon::setTestNow() (or CarbonImmutable::setTestNow()) lets the whole test suite travel in time with one line, which is the single best reason to keep Carbon even if you dislike its mutable default.
The cost: Carbon is generous with magic. __get proxies, dynamic property access, and the mutable default mean static analysis tools see less than you would like, and a careless use Carbon\Carbon; in a long-lived service object is a latent bug.
Chronos: Immutability First, CakePHP Roots
Chronos began as CakePHP’s date layer and is now standalone. Its 3.x line makes the immutability decision for you and, more importantly, splits the domain into three explicit types.
| |
| |
The addMonthNoOverflow() method alone justifies Chronos for billing code. PHP’s native month arithmetic turns January 31 into March 3, a bug that hides in production until someone runs a monthly close on the 31st. Chronos makes the overflow behaviour explicit instead of accidental.
Chronos also keeps the dependency graph almost flat: no translation bundles, no third-party contracts beyond the PSR clock interface. The trade-off is a smaller ecosystem — no diffForHumans(), no locale month names — so if your UI shows “vor 3 Stunden”, you either add a formatter or choose Carbon.
Brick\DateTime: The Strict, Zero-Dependency Option
Brick\DateTime is what a Java java.time developer would build in PHP: small, final, immutable value objects with no shared state and no extension of the native DateTime. It requires a 64-bit build, which is a fair trade for exact integer timestamp math.
| |
| |
The verbosity is the feature. When a LocalDate can never carry a zone, a midnight-UTC conversion bug becomes a type error instead of a support ticket. Brick also clamps month arithmetic by default rather than overflowing, matches ISO-8601 parsing rules strictly, and exposes Instant, Duration and Period as separate concepts, which is exactly what you want when modelling loan schedules or retention windows.
It is also the least forgiving option for a team raised on strtotime(). There is no diffForHumans(), no locale formatting, and every conversion is explicit. Budget a day for the first service you convert, and expect your code to get longer before it gets better.
Where Each Library Breaks: Pitfalls and Migration Traps
The mutable-default trap. Carbon::now() returns a mutable object. Pass it to a helper that calls addDay(), and the caller’s variable changes. Fix: import CarbonImmutable everywhere and ban Carbon\Carbon in new code with a static-analysis rule. This is a one-line change that eliminates an entire bug class.
date_default_timezone_get() is not a timezone strategy. All three libraries accept a zone argument; none of them can guess it correctly for a user in another country. Store UTC in the database, store the user’s IANA zone alongside it, and convert at the edges.
DST gaps and overlaps. On 2026-03-08 in America/New_York, 02:30 does not exist; on 2026-11-01, 01:30 happens twice. Carbon and Chronos normalize silently, Brick makes you choose an offset resolution. For anything that bills by the hour, decide explicitly whether a 23-hour day bills as 23 or 24 hours — the library will not decide for you.
Month-end overflow. 2027-01-31 + 1 month is 2027-03-03 with native PHP arithmetic. Use addMonthNoOverflow() in Chronos, or Brick’s clamping plusMonths(), and never assume Carbon’s addMonth() is safe for statement dates.
Serialization drift. Carbon serializes to ISO-8601 with offset by default; Chronos and Brick follow their own contracts. If dates go into JSON APIs consumed by mobile clients, pin the format explicitly in your DTO layer instead of relying on json_encode() defaults.
Mixing families in one codebase. Converting between them is possible (Carbon::instance(), Chronos::instance()) but every conversion is a place where a zone gets dropped. Pick one library for domain logic and convert only at the boundary.
If you are evaluating the surrounding stack too, our PHP validation library comparison and the PHP HTTP client guide follow the same format, and the .NET date-time library comparison covers the same problem from the other side of the fence.
FAQ
Is Carbon faster than Chronos?
Parsing and arithmetic speed is close enough that it will not decide your architecture — differences are in the low microseconds per operation and are dominated by autoloading and timezone database lookups. Carbon’s larger dependency graph costs more at cold start; Chronos and Brick load fewer files. Benchmark against your own workload rather than trusting microbenchmarks, and cache the timezone database if you create many zoned objects per request.
Can I use Carbon and Chronos in the same project?
Yes, and many legacy CakePHP codebases do. Chronos::instance($carbon) and Carbon::instance($chronos) convert cleanly because both wrap the same underlying timestamp. The risk is conceptual, not technical: two date types in one domain layer guarantee that someone eventually passes a mutable Carbon into an immutable pipeline.
Does Chronos really prevent the January 31 bug?
Only if you call addMonthNoOverflow(). Plain addMonth() overflows in Chronos exactly as it does in PHP’s native DateTime, because Chronos follows PHP semantics unless told otherwise. Make the no-overflow variant your default in billing code and grep for addMonth( during review.
Which library should I use for date-only values like birthdays or holidays?
Chronos’ ChronosDate or Brick’s LocalDate. Carbon has no date-only type — a Carbon is always a date-time with a zone, so a birthday stored as Carbon can render as the previous day for users west of UTC. This single mismatch causes a surprising number of “wrong day” bug reports.
Do these libraries handle historical timezone changes?
All three rely on the system or bundled timezone database (typically the tzdata package or PHP’s compiled-in version). Historical offsets are generally correct, but the database must be updated when governments change rules — Chile, Egypt and Morocco have all shifted rules in the last few years. Check your container image’s tzdata version during deploys.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com