midnight / temporal-php
TC39 Temporal API for PHP — immutable dates, times, durations, and time zones with nanosecond precision
Requires
- php: >=8.4
- ext-intl: *
Requires (Dev)
- carthage-software/mago: ^1.15.3
- infection/infection: ^0.32 || ^0.35
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.0
- psalm/plugin-phpunit: ^0.19
- vimeo/psalm: ^6.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.1
- v0.1.0
- dev-release-please--branches--master
- dev-publish/exact-plain-date-addition
- dev-publish/instant-auto-largest-unit
- dev-publish/plain-difference-zero
- dev-publish/duration-time-limit
- dev-publish/spec-visibility-errors
- dev-publish/indexof-conformance
- dev-publish/zoned-calendar-rounding
- dev-docs/iso-fraction-grammar
- dev-fix/calendar-bag-validation-order
- dev-fix/proleptic-calendar-addition
- dev-fix/yearmonth-lower-unit-guard
- dev-fix/zoned-offset-fraction
- dev-fix/zoned-string-components
- dev-fix/plain-date-offset-grammar
- dev-fix/chinese-calendar-boundaries
- dev-fix/chinese-leap-month-inverse
- dev-publish/intl-option-conformance
- dev-fix/intl-option-coercion
- dev-publish/plain-difference-day-parity
- dev-publish/plain-difference-bubbling
- dev-publish/plain-difference-direction
- dev-publish/calendar-year-month-rounding
- dev-perf/intl-pattern-cache
- dev-publish/iso-calendar-totals
- dev-publish/large-calendar-duration-totals
- dev-publish/calendar-duration-anchors
- dev-fix/porcelain-epoch-roundtrip
- dev-perf/start-of-day-resolution
- dev-perf/numeric-native-timestamps
- dev-perf/fixed-month-arithmetic
- dev-fix/bounded-calendar-fields
- dev-publish/zoned-duration-week-totals
- dev-publish/exact-calendar-year-total
- dev-publish/exact-zoned-time-totals
- dev-publish/large-zoned-time-rounding
- dev-publish/exact-instant-differences
- dev-publish/exact-duration-addition
- dev-publish/exact-instant-addition
- dev-perf/locale-alias-index
- dev-fix/intl-locale-list-resolution
- dev-fix/wide-zone-transitions
- dev-fix/zoned-calendar-intermediate-range
- dev-perf/duration-zero-subseconds
- dev-fix/fractional-digits-number-range
- dev-fix/instant-time-zone-grammar
- dev-perf/hebrew-month-arithmetic
- dev-fix/plaintime-large-arithmetic
- dev-fix/time-zone-string-grammar
- dev-publish/calendar-string-validation
- dev-repo-stats
- dev-docs/readme-1.0
- dev-test/calendar-consistency-conformance
- dev-fix/calendar-increment-rounding
- dev-fix/zero-offset-zone-identity
- dev-fix/strict-month-code-grammar
- dev-chore/mutation-gate-scope
- dev-docs/serialization-contract
- dev-fix/difference-conformance
- dev-test/intl-callback-conformance
- dev-test/resolved-zone-conformance
- dev-fix/rounding-increment-string-coercion
- dev-fix/literal-zoned-wall-years
- dev-fix/extended-year-zoned-days
- dev-fix/duration-calendar-tie-parity
- dev-fix/zoned-lower-epoch-boundary
- dev-fix/temporal-time-grammar
- dev-fix/eraless-field-coercion
- dev-test/locale-formatting-conformance
- dev-test/now-instant-conformance
- dev-chore/mago-151-format
- dev-fix/conformance-and-release-readiness
- dev-codex/fix-188-hour-widths
- dev-123-duration-round-sign-loss
- dev-114-duration-float-zero
- dev-rename-to-calendrics
- dev-56-duration-round-bag-validation
- dev-timezone-basic-offset
- dev-docs-release-please-php-updaters
- dev-dependabot-conventional-prefixes
- dev-release-please
- dev-58-duration-total-era-resolution
- dev-59-fix-zoneddatetime-boundary-arithmetic
- dev-transpile-bigint-folding
- dev-tolocalestring-subsecond-and-default
- dev-duration-total-exact
- dev-57-duration-round-float-residue
- dev-epoch-rounding-halfeven-parity
- dev-55-fix-duration-round-overflow
- dev-claude/ambitious-improvements-09ea25
- dev-claude/ambitious-improvements-cc94f6
- dev-claude/ambitious-improvements-6dad6a
- dev-claude/ambitious-improvements-decb68
- dev-claude/ambitious-improvements-1c605a
- dev-claude/ambitious-improvements-c9b261
- dev-claude/porcelain-locale-formatting
- dev-claude/ambitious-improvements-db76f8
- dev-claude/ambitious-improvements-akuund
- dev-claude/ambitious-improvements-f95cc2
- dev-coverage-uplift-spec-fixes
- dev-19-from-datetime
- dev-feature/exception-hierarchy
- dev-relocate-zdt-internal-helpers
- dev-cleanup/uncovered-lines
- dev-fix/test262-spec-deviations
- dev-drop-spec-valueof
- dev-claude-md-init
- dev-test262/toprimitive-observer-passthrough
- dev-fix/pre-1.0-quality-pass
- dev-perf/memoize-hot-paths
- dev-refactor/extract-virtual-property-traits
- dev-mago-cleanup
- dev-refactor/extract-calendar-day-week-helper
- dev-remove-zoned-datetime-skip-guards
- dev-chore/untrack-build-coverage-artifacts
- dev-docs/bc-promise-spec-layer-public
- dev-fix/duration-round-total-docblocks
- dev-readme-update-ecma402-v2
- dev-worktree-intl402
This package is auto-updated.
Last update: 2026-10-03 19:32:15 UTC
README
Dates, times, durations, and time zones for PHP, based on the Temporal API.
Calendrics separates a calendar date from a timestamp and makes time zones explicit. Its immutable values, typed options, and named arguments let application code express whether it means “tomorrow at the same local time” or “24 hours later.”
Quickstart · Choose a type · Usage guide · Compatibility · Contributing
Install
You need PHP 8.4 or newer, Composer, and the intl extension. The project tests on 64-bit PHP 8.4 and 8.5; 32-bit PHP is not covered by CI.
composer require midnight/calendrics
The package is currently pre-1.0. Public APIs may change between minor versions; see the compatibility policy before upgrading.
Quickstart
Save this as example.php in a project with the package installed, then run php example.php:
<?php require __DIR__ . '/vendor/autoload.php'; use Calendrics\Duration; use Calendrics\PlainDate; use Calendrics\PlainTime; $invoiceDate = PlainDate::parse('2026-03-20'); $dueDate = $invoiceDate->add(new Duration(days: 14)); // A calendar date needs a time and zone to identify an instant. $deadline = $dueDate->toZonedDateTime('Europe/Vienna', new PlainTime(17)); echo $invoiceDate, PHP_EOL; // 2026-03-20 (unchanged) echo $dueDate, PHP_EOL; // 2026-04-03 echo $deadline, PHP_EOL; // 2026-04-03T17:00:00+02:00[Europe/Vienna] echo $deadline->toInstant(), PHP_EOL; // 2026-04-03T15:00:00Z
Choose a type
| You have… | Use | Example |
|---|---|---|
| A date without a time zone | PlainDate |
A birthday or invoice date |
| A point on the global timeline | Instant |
An event timestamp |
| A date and time in a named zone | ZonedDateTime |
A meeting in Europe/Vienna |
| A local date and time, with no zone yet | PlainDateTime |
A form's local date/time input |
| A time of day | PlainTime |
A shop's opening time |
| A year and month | PlainYearMonth |
A billing period |
| A recurring month and day | PlainMonthDay |
An anniversary |
| An amount of calendar or elapsed time | Duration |
Two months, or 90 minutes |
Now supplies the current instant and current local values. A plain value never acquires a time zone implicitly: choose one when converting it to a zoned value.
Calendar days and elapsed hours
A calendar day is not always 24 hours. Across a daylight-saving transition, choose the operation that matches your intent:
use Calendrics\Duration; use Calendrics\ZonedDateTime; $start = ZonedDateTime::parse('2026-03-28T12:00:00+01:00[Europe/Vienna]'); echo $start->add(new Duration(days: 1)); // 2026-03-29T12:00:00+02:00[Europe/Vienna] echo $start->add(new Duration(hours: 24)); // 2026-03-29T13:00:00+02:00[Europe/Vienna]
Local times can also be skipped or repeated when clocks change. Pass Disambiguation::Reject when constructing a zoned value if your application should ask the user to resolve that ambiguity. The usage guide shows the options.
Working with values
Use parse() for strings, constructors for explicit values, and fromFields() when you need calendar-specific fields. Operations such as add(), with(), and round() return new values.
Options use enums such as Overflow::Reject, Unit::Month, and RoundingMode::HalfEven. Use compare() or equals() to compare values; PHP's native object comparison operators do not implement Temporal ordering.
The usage guide covers differences and rounding, calendars, localized formatting, JSON, PHP DateTimeInterface conversion, and the public Spec API.
Precision and portability
- Times can represent nine fractional digits. PHP
DateTimeInterfacehas microsecond precision; epoch conversions truncate toward zero to microsecond precision. InstantandZonedDateTimepreserve timestamps beyond the 64-bit nanosecond epoch range when parsing, converting from Spec values, and converting to native PHP date-time objects. Integer nanosecond properties and native date-time input still have range limits; see timestamp range limits.- Duration results can follow JavaScript's floating-point Number semantics. Instant differences retain exact integer fields in some cases where JavaScript rounds them. See PHP and Temporal differences.
- Localized strings and non-ISO calendar behavior depend on ICU data from
ext-intl. Use the ISO-styletoString()/parse()pair for storage and interchange; do not parsetoLocaleString()output.
Calendrics has two public layers: the PHP-oriented Calendrics\ API shown here and the Temporal-shaped Calendrics\Spec\ API. Most applications should start with the first. Neither layer's internal implementation namespace is a public API.
Project status and quality
The project runs PHPUnit, PHPStan, Psalm, and Mago. Its mutation gate covers ten top-level PHP-oriented classes. Temporal conformance is checked with a translated subset of upstream test262; unsupported JavaScript or harness cases are reported as incomplete. Testing scope and development commands explain the checks.
See the changelog for released changes and GitHub issues for defects and planned work.