Kronika is a PHP library that provides date-time value objects such as "Date", "Time", "LocalDateTime", "ZonedDateTime", etc.
The recommended way to install Kronika is through Composer.
composer require frostealth/kronika| Version | Status | Branch | PHP Version |
|---|---|---|---|
| 0.4 | Dev | 0.x | ^8.4 |
| 0.3 | Latest | - | ^8.4 |
| 0.1 | EOL | 0.1 | >=8.3,<=8.5 |
- Date
- Time
- LocalDateTime
- ZonedDateTime
- Clock
- Range:
- Extensions:
Kronika\Date represents a date without specifying a time of day.
The following units of date are available:
YearMonthDayOfMonthDayOfWeekDayOfYear
// creating the "Date" instance
$date = Date::of(year: 2025, month: 12, day:31);
// or from "\DateTimeInterface"
$date = Date::ofDateTime(new \DateTimeImmutable('2025-12-31'));
// or from a date string
$date = Date::parse('2025-12-31');
// formatting the "Date"
echo $date->format('Y-m-d'); // '2025-12-31'
echo $date->format('l, F jS, Y.'); // 'Wednesday, December 31st, 2025.'
$year = $date->year(); // Year::of(2025)
$month = $date->month(); // Month::of(12)
$day = $date->day(); // DayOfMonth::of(31)
$dayOfWeek = $date->dayOfWeek(); // DayOfWeek::of(3)
// changing the year, month, day, weekday
$date = $date->with(Year::of(2026));
echo $date->format('l, F jS, Y.'); // 'Thursday, December 31st, 2026.'
echo $date->is(Year::of(2026)); // true
$date = $date->with(Month::January)->with(DayOfMonth::of(12));
echo $date->format('l, F jS, Y.'); // 'Monday, January 12th, 2026.'
echo $date->is(DayOfMonth::of(12)); // true
$date = $date->startOfMonth();
echo $date->format('l, F jS, Y.'); // 'Thursday, January 1st, 2026.'
// changing the day of week
$date = $date->with(DayOfWeek::Friday);
echo $date->format('l, F jS, Y.'); // 'Friday, January 2nd, 2026.'
echo $date->is(DayOfWeek::Friday); // true
// adding an amount of days
$date = $date->add(Duration::of(days: 3));
echo $date->format('l, F jS, Y.'); // 'Monday, January 5th, 2026.'
// subtracting an amount of days
$date = $date->sub(Duration::of(hours: 48));
echo $date->format('l, F jS, Y.'); // 'Saturday, January 3rd, 2026.'
// getting the duration from one date to another one
$duration = $date->until(Date::of(year: 2026, month: 1, day: 5));
echo $duration->days(); // 2
echo $duration->hours(); // 0
echo $duration->minutes(); // 0Kronika\Time represents a time of day without specifying a date.
The following units of time are available:
HourMinuteSecond
// creating the "Time" instance
$time = Time::of(hour: 9, minutes: 10, seconds:30);
// or from "\DateTimeInterface"
$time = Time::ofDateTime(new \DateTimeImmutable('09:10:30'));
// or from time string
$time = Time::parse('09:10:30.000000');
// formatting the "Time"
echo $time->format('H:i:s'); // '09:10:30'
echo $time->format('H:i:s.u'); // '09:10:30.000000'
$hour = $time->hour(); // Hour::of(9)
$minute = $time->minute(); // Minute::of(10)
$second = $time->second(); // Second::of(30)
// changing the hour, minute and second
$time = $time->with(Hour::of(12));
echo $time->format('H:i:s'); // '12:10:30'
echo $time->is(Hour::of(12)); // true
$time = $time->with(Minute::of(30))->with(Second::zero());
echo $time->format('H:i:s'); // '12:30:00'
// comparing time or its unit to another one
$other = $time->with(Second::of(0, micro: 999999));
echo $time->is($other); // false
echo $time->is($other, Precision::Second); // true
echo $time->is( // true
$other->with(Second::of(59)),
Precision::Minute,
);
// adding an amount of hours, minutes, seconds
$time = $time->add(Duration::of(hours: 3, minutes: 30, seconds: 30));
echo $time->format('H:i:s'); // '16:00:30'
// subtracting an amount of hours, minutes, seconds
$time = $time->sub(Duration::of(hours: 2, minutes: 120, seconds: 30));
echo $time->format('H:i:s'); // '12:00:00'
// getting the duration from one time to another one
$duration = $time->until(Time::of(hour: 18, minute: 30, second: 30));
echo $duration->hours(); // 6
echo $duration->minutes(); // 30
echo $duration->second(); // 30
echo $duration->inMinutes(); // 390Kronika\LocalDateTime represents a local date-time without time-zone.
// creating the "LocalDateTime" instance
$date = Date::of(year: 2025, month: 12, day: 31);
$time = Time::midday();
$datetime = LocalDateTime::of($date, $time);
// or
$datetime = $date->at($time);
// or from "\DateTimeInterface"
$datetime = LocalDateTime::ofDateTime(new \DateTimeImmutable('2025-12-31 12:00:00'));
// or from a date-time string
$datetime = LocalDateTime::parse('2025-12-31 12:00:00');
// formatting the "LocalDateTime"
echo $datetime->format('Y-m-d H:i:s'); // '2025-12-31 12:00:00'
$year = $datetime->year(); // Year::of(2025)
$month = $datetime->month(); // Month::of(12)
$day = $datetime->day(); // DayOfMonth::of(31)
$hour = $datetime->hour(); // Hour::of(12)
$minute = $datetime->minute(); // Minute::zero()
$second = $datetime->second(); // Second::zero()
$date = $datetime->date(); // Date::of(2025, 12, 31)
$time = $datetime->time(); // Time::of(12, 0, 0)
// changing the year, month, day, hour, minute and second is similar to "Date" and "Time"
$datetime = $datetime->with(Hour::of(18))->with(Minute::of(30));
echo $datetime->format('Y-m-d H:i:s'); // '2025-12-31 18:30:00'
echo $datetime->is(Hour::of(18)); // true
// adding and subtracting an amount of days, hours,
// minutes and seconds are similar to "Date" and "Time"
$datetime = $datetime->add(Duration::of(hours: 6, minutes: 30, seconds: 30));
echo $datetime->format('Y-m-d H:i:s'); // '2026-01-01 01:00:30'
$datetime = $datetime->sub(Duration::of(hours: 12, minutes: 60, seconds: 30));
echo $datetime->format('Y-m-d H:i:s'); // '2025-12-31 12:00:00'
// getting the duration from one "LocalDateTime" to another one
$duration = $datetime->until(
LocalDateTime::of(Date::of(year: 2026, month: 1, day: 14), Time::midnight()),
);
echo $duration->days(); // 13
echo $duration->hours(); // 12
echo $duration->minutes(); // 0
echo $duration->second(); // 0
echo $duration->inHours(); // 324
// getting "\DateTimeImmutable" and "\DateTime"
$immutable = $datetime->toNative(new \DateTimeZone('UTC')); // "\DateTimeImmutable"
$mutable = $datetime->toNativeMutable(new \DateTimeZone('UTC')); // "\DateTime"Kronika\ZonedDateTime represents a date-time with time-zone.
The API of Kronika\ZonedDateTime is similar to Kronika\LocalDateTime.
Kronika\ZonedDateTime class extends \DateTimeImmutable.
// creating the "ZonedDateTime" instance
$date = Date::of(year: 2025, month: 12, day: 31);
$time = Time::midday();
$timezone = new \DateTimeZone('UTC')
$datetime = ZonedDateTime::of($date, $time, $timezone);
// or
$datetime = $date->at($time)->at($timezone);
// or
$datetime = ZonedDateTime::ofLocal(LocalDateTime::of($date, $time), $timezone);
// or
$datetime = LocalDateTime::of($date, $time)->at($timezone);
// or
$datetime = ZonedDateTime::utcOf($date, $time);
// or with current time and specified time-zone
$datetime = now($timezone);
// or from "\DateTimeInterface"
$datetime = ZonedDateTime::ofDateTime(new \DateTimeImmutable('2025-12-31 12:00:00 UTC'));
// or from a date-time string with time-zone
$datetime = ZonedDateTime::parse('2025-12-31 12:00:00 UTC');
// or from a date-time string without time-zone
$datetime = ZonedDateTime::parse('2025-12-31 12:00:00', new \DateTimeZone('UTC'));
// formatting the "ZonedDateTime"
echo $datetime->format(\DateTimeInterface::ATOM); // '2025-12-31T12:00:00+00:00'
$year = $datetime->year(); // Year::of(2025)
$month = $datetime->month(); // Month::of(12)
$day = $datetime->day(); // DayOfMonth::of(31)
$hour = $datetime->hour(); // Hour::of(12)
$minute = $datetime->minute(); // Minute::zero()
$second = $datetime->second(); // Second::zero()
$date = $datetime->date(); // Date::of(2025, 12, 31)
$time = $datetime->time(); // Time::of(12, 0, 0)
$timezone = $datetime->timezone(); // \DateTimeZone('UTC')
$timestamp = $datetime->timestamp(); // float(1767182400.001234)
// changing the year, month, day, hour, minute and second is similar to "LocalDateTime"
$datetime = $datetime->with(Hour::of(18))->with(Minute::of(30));
echo $datetime->format(\DateTimeInterface::ATOM); // '2025-12-31T18:30:00+00:00'
echo $datetime->is(Hour::of(18)); // true
// changing the time-zone doesn't shift the time,
// to shift the time use "shift()" method
echo $datetime->with(new \DateTimeZone('+01:00'))
->format(\DateTimeInterface::ATOM); // '2025-12-31T18:30:00+01:00
// adding and subtracting an amount of days, hours, minutes
// and seconds are similar to "LocalDateTime"
$datetime = $datetime->add(Duration::of(hours: 6, minutes: 30, seconds: 30));
echo $datetime->format(\DateTimeInterface::ATOM); // '2026-01-01T01:00:30+00:00'
$datetime = $datetime->sub(Duration::of(hours: 12, minutes: 60, seconds: 30));
echo $datetime->format(\DateTimeInterface::ATOM); // '2025-12-31T12:00:00+00:00'
// shifting the timezone
$datetime = $datetime->shift(new \DateTimeZone('+01:00'));
echo $datetime->format(\DateTimeInterface::ATOM); // '2025-12-31T13:00:00+01:00'
// getting the duration from one "ZonedDateTime" to another one
$duration = $datetime->until(new \DateTime('2026-01-14T12:30:15+00:00'));
$days = $duration->days(); // 14
$hours = $duration->hours(); // 0
$minutes = $duration->minutes(); // 30
$seconds = $duration->second(); // 15
$inHours = $duration->inHours(); // 336
// getting "\DateTimeImmutable" and "\DateTime"
$immutable = $datetime->toNative(); // "\DateTimeImmutable"
$mutable = $datetime->toNativeMutable(); // "\DateTime"Kronika\Clock decouples your code from the system clock
and has the following implementations:
SystemClockreturns the current time, this is the same as doingnew \DateTime().InaccurateClockignores a second or microsecond of the current time.PsrClockimplements PSR-20: Clock.FrozenClockdoesn't move forward on its own, useful in tests.MutableClockallows to manipulate with clock, useful in tests.
Kronika\Range\TimeRange represents a range between two moments of day.
// creating "TimeRange"
$range = TimeRange::of(
from: Time::midday(), // inclusive
to: Time::of(13, 30), // exclusive
);
echo $range->from()->format('H:i:s'); // 12:00:00
echo $range->to()->format('H:i:s'); // 13:30:00
echo $range->contains(Time::midday()); // true
echo $range->contains(Time::of(13, 30)); // false
// getting each item of this range with the specified step
foreach ($range->each(Duration::of(minutes: 30)) as $item) {
echo $item->format('H:i:s');
}
// 12:00:00
// 12:30:00
// 13:00:00
// splitting the range into smaller ranges
foreach ($range->split(Duration::ofHour()) as $item) {
echo $item->from() . ' - ' . $item->to();
}
// 12:00:00 - 13:00:00
// 13:00:00 - 13:30:00
// getting an intersection between ranges
$foo = TimeRange::of(Time::of(13, 0), Time::of(14, 0));
if ($range->overlaps($foo)) {
$bar = $range->intersection($foo); // 13:00:00 - 13:30:00
}
// getting a gap between ranges
$foo = TimeRange::of(Time::of(14, 0), Time::of(15, 0));
if (! $range->overlaps($foo)) {
$bar = $range->gap($foo); // 13:30:00 - 14:00:00
}Kronika\Range\DateRange represents a range between two dates.
// creating "DateRange"
$range = DateRange::of(
from: Date::of(2025, 12, 15), // inclusive
to: Date::of(2025, 12, 18), // exclusive
);
echo $range->from()->format('Y-m-d'); // 2025-12-15
echo $range->to()->format('Y-m-d'); // 2025-12-18
echo $range->contains(Date::of(2025, 12, 15)); // true
echo $range->contains(Date::of(2025, 12, 18)); // false
// getting each item of this range with the specified step
// minimal step is 1 day
foreach ($range->each(Duration::of(days: 2)) as $item) {
echo $item->format('Y-m-d');
}
// 2025-12-15
// 2025-12-17
// splitting the range into smaller ranges
foreach ($range->split(Duration::of(days: 2)) as $item) {
echo $item->from() . ' - ' . $item->to();
}
// 2025-12-15 - 2025-12-17
// 2025-12-17 - 2025-12-18
// getting an intersection between ranges
$foo = DateRange::of(Date::of(2025, 12, 17), Date::of(2025, 12, 20));
if ($range->overlaps($foo)) {
$bar = $range->intersection($foo); // 2025-12-17 - 2025-12-18
}
// getting a gap between ranges
$foo = DateRange::of(Date::of(2025, 12, 20), Date::of(2025, 12, 22));
if (! $range->overlaps($foo)) {
$bar = $range->gap($foo); // 2025-12-18 - 2025-12-20
}Kronika\Range\LocalDateTimeRangerepresents a range between two instances ofLocalDateTime.Kronika\Range\ZonedDateTimeRangerepresents a range between two instances ofZonedDateTime.
// creating "ZonedDateTimeRange"
$range = ZonedDateTimeRange::of(
from: ZonedDateTime::parse('2025-12-15 12:30:45 +01:00'), // inclusive
to: ZonedDateTime::parse('2025-12-18 10:00:30 +01:00'), // exclusive
);
echo $range->from()->format('Y-m-d H:i:s P'); // 2025-12-15 12:30:45 +01:00
echo $range->to()->format('Y-m-d H:i:s P'); // 2025-12-18 10:00:30 +01:00
echo $range->contains(ZonedDateTime::parse('2025-12-15 12:30:45 +01:00')); // true
echo $range->contains(ZonedDateTime::parse('2025-12-18 10:00:30 +01:00')); // false
// getting each item of this range with the specified step
// the type of each item will be the same as "since"
foreach ($range->each(Duration::ofDay()) as $item) {
echo $item->format('Y-m-d H:i:s P');
}
// 2025-12-15 12:30:45 +01:00
// 2025-12-16 12:30:45 +01:00
// 2025-12-17 12:30:45 +01:00
// splitting the range into smaller ranges
foreach ($range->split(Duration::ofDay()) as $item) {
echo $item->from() . ' - ' . $item->to();
}
// 2025-12-15 12:30:45 +01:00 - 2025-12-16 12:30:45 +01:00
// 2025-12-16 12:30:45 +01:00 - 2025-12-17 12:30:45 +01:00
// 2025-12-17 12:30:45 +01:00 - 2025-12-18 10:00:30 +01:00
// getting an intersection between ranges
$foo = ZonedDateTimeRange::of(
ZonedDateTime::parse('2025-12-17 20:15:10 +01:00'),
ZonedDateTime::parse('2025-12-20 12:00:00 +01:00'),
);
if ($range->overlaps($foo)) {
$bar = $range->intersection($foo);
// 2025-12-17 20:15:10 +01:00 - 2025-12-18 10:00:30 +01:00
}
// getting a gap between ranges
$foo = ZonedDateTimeRange::of(
ZonedDateTime::parse('2025-12-20 12:00:00 +01:00'),
ZonedDateTime::parse('2025-12-12 10:00:00 +01:00'),
);
if (! $range->overlaps($foo)) {
$bar = $range->gap($foo);
// 2025-12-18 10:00:30 +01:00 - 2025-12-20 12:00:00 +01:00
}