{"id":3092,"date":"2026-05-27T15:11:58","date_gmt":"2026-05-27T13:11:58","guid":{"rendered":"https:\/\/nyamsprod.com\/blog\/?p=3092"},"modified":"2026-05-27T15:11:58","modified_gmt":"2026-05-27T13:11:58","slug":"tokei-a-time-handling-library-for-php","status":"publish","type":"post","link":"https:\/\/nyamsprod.com\/blog\/tokei-a-time-handling-library-for-php\/","title":{"rendered":"Tokei, a Time handling library for PHP"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A couple of years ago I released\u00a0<a href=\"https:\/\/period.thephpleague.com\/\">league\/period<\/a>, a package designed to fill a long-standing gap in PHP\u2019s date API: a proper interval object. Its\u00a0<code>Period<\/code>\u00a0class represents a half-open interval between two\u00a0<code>DateTimeImmutable<\/code>\u00a0instances, with one important constraint: the starting date must always be before or equal to the ending date. The package is still actively maintained and remains highly relevant today.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Recently, however, I ran into a completely different problem domain:&nbsp;<strong>local time<\/strong>.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No dates.<\/li>\n\n\n\n<li>No timezones.<\/li>\n\n\n\n<li>Just time-of-day values and circular time ranges.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">That was the moment I realized PHP still lacks the right abstractions for handling local time properly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So today, I\u2019d like to introduce&nbsp;<a href=\"https:\/\/github.com\/bakame-php\/tokei\/\">bakame\/tokei<\/a>: a small but robust PHP package dedicated to local time handling.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; title: ; notranslate\" title=\"\">\ncomposer require bakame\/tokei\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">Back to basics<a href=\"https:\/\/gist.github.com\/#back-to-basics\"><\/a><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tokei<\/strong>&nbsp;revolves around two core classes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Time<\/code><\/li>\n\n\n\n<li><code>Duration<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Alongside them, a small set of enums helps make time manipulation expressive and safe.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: php; title: ; notranslate\" title=\"\">\n&lt;?php\n\nuse Bakame\\Tokei\\Time;\nuse Bakame\\Tokei\\Duration;\nuse Bakame\\Tokei\\SubSecondDisplay;\n\n$dateTime = new DateTimeImmutable(&#039;2026-04-23 22:08:56.000456&#039;);\n$time = Time::fromDate($dateTime);\necho $time-&gt;toString(); \n\/\/ &quot;22:08:56.000456&quot;\n\n$duration = Duration::of(minutes: 25, hours: 10)-&gt;negated();\necho $duration-&gt;toClockFormat(); \n\/\/ &quot;-10:25:00&quot;\n\n$newTime = $time-&gt;add($duration);\necho $newTime-&gt;toString(); \n\/\/ &quot;11:43:56.000456&quot;\n\necho $newTime-&gt;toString(SubSecondDisplay::Never);\n\/\/ &quot;11:43:56&quot;\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\"><code>Time<\/code>&nbsp;and&nbsp;<code>Duration<\/code>&nbsp;both operate with microsecond precision. Durations can be added to or subtracted from times, and all operations correctly wrap around the 24-hour clock.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The library also provides developer-friendly formatting utilities to simplify displaying times and durations in multiple formats. But that is only the beginning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can also:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>compare times and durations,<\/li>\n\n\n\n<li>truncate or round values,<\/li>\n\n\n\n<li>clamp values within bounds,<\/li>\n\n\n\n<li>compute distances and differences,<\/li>\n\n\n\n<li>and convert durations to&nbsp;<code>ISO-8601<\/code>&nbsp;representations.<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: php; title: ; notranslate\" title=\"\">\n&lt;?php\n\n$noon = Time::noon();\n$endOfDay = Time::endOfDay();\n$midnight = Time::midnight();\n\n$midnight-&gt;isBefore($endOfDay); \/\/ true\n$noon-&gt;isAfter($midnight);      \/\/ true\n\n$morningDuration = $midnight-&gt;distance($noon);\n$eveningDuration = $noon-&gt;distance($endOfDay);\n$roundedEveningDuration = $eveningDuration-&gt;roundTo(Unit::Second);\n\n$morningDuration-&gt;isLongerThan($eveningDuration);         \/\/ true\n$morningDuration-&gt;isLongerThan($roundedEveningDuration);  \/\/ false\n\necho $morningDuration-&gt;toIso8601();\n\/\/ &#039;PT12H&#039;\n\necho $eveningDuration-&gt;toIso8601();\n\/\/ &#039;PT11H59M59.999999S&#039;\n\necho $roundedEveningDuration-&gt;toIso8601();\n\/\/ &#039;PT12H&#039;\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">At first glance, these results may seem counterintuitive. But in a circular 24-hour system, a day does not end at midnight \u2014 the next day begins there. That subtle distinction matters when performing precise calculations.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Modeling Circular Time<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/gist.github.com\/#modeling-circular-time\"><\/a>To properly model circular time ranges,&nbsp;<strong>Tokei<\/strong>&nbsp;provides two additional classes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Interval<\/code><\/li>\n\n\n\n<li><code>IntervalSet<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">An&nbsp;<code>Interval<\/code>&nbsp;combines a starting time with a duration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An&nbsp;<code>IntervalSet<\/code>&nbsp;is a specialized collection designed to work efficiently with groups of intervals and their interactions.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: php; title: ; notranslate\" title=\"\">\n&lt;?php\n\n$interval = Interval::since(\n  start: Time::noon(),\n  duration: Duration::of(hours: 6),\n);\n$interval-&gt;format(IntervalFormat::Iso80000);\n\/\/ &quot;&#x5B;12:00:00,18:00:00)&quot;\n\n$interval-&gt;format(IntervalFormat::Iso8601StartDuration);\n\/\/ &quot;12:00:00\/PT6H&quot;\n\n$interval-&gt;start;    \/\/ returns a Time instance\n$interval-&gt;end;      \/\/ returns a Time instance\n$interval-&gt;duration; \/\/ returns a Duration instance\n$interval-&gt;type;     \/\/ returns an IntervalType Enum case\n\n$openHours = new IntervalSet(\n  Interval::between(\n    start: Time::at(hour: 10),\n    end: Time::at(hour: 12, minute: 30)\n  ),\n  Interval::between(\n      start: Time::at(hour: 14),\n      end: Time::at(hour: 18)\n  ),\n  Interval::between(\n      start: Time::at(hour: 22),\n      end: Time::at(hour: 6)\n  ),\n);\n\n$now = Time::now();\n$isOpen = $openHour-&gt;any(\n    fn (Interval $interval): bool =&gt; $interval-&gt;includes($now)\n);\n\necho $isOpen\n    ? &#039;The shop is currently open&#039;\n    : &#039;The shop is currently closed&#039;;\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Unlike traditional date intervals,&nbsp;<code>Interval<\/code>&nbsp;works on a circular timeline.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That means the starting time may be:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>before the ending time,<\/li>\n\n\n\n<li>equal to the ending time,<\/li>\n\n\n\n<li>or after the ending time.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">To make interval semantics explicit, each interval exposes a type property backed by the&nbsp;<code>IntervalType<\/code>&nbsp;enum.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Possible values are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>IntervalType::Linear<\/code>&nbsp;: The start time is before the end time.<\/li>\n\n\n\n<li><code>IntervalType::Overflow<\/code>&nbsp;: The interval crosses midnight.<\/li>\n\n\n\n<li><code>IntervalType::Circular<\/code>&nbsp;: The interval spans the full 24 hours.<\/li>\n\n\n\n<li><code>IntervalType::Collapsed<\/code>&nbsp;: The interval has zero duration.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">These distinctions are important because interval behavior changes depending on its topology, especially for comparison, containment, intersection, and normalization operations.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Matters<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/gist.github.com\/#why-this-matters\"><\/a>Local time handling sounds deceptively simple until you encounter real-world problems such as:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>business opening hours,<\/li>\n\n\n\n<li>recurring schedules,<\/li>\n\n\n\n<li>night shifts,<\/li>\n\n\n\n<li>cron-like systems,<\/li>\n\n\n\n<li>media programming,<\/li>\n\n\n\n<li>circular availability windows,<\/li>\n\n\n\n<li>or any logic crossing midnight.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Most date APIs model time linearly. But clocks are circular.&nbsp;<strong>Tokei<\/strong>&nbsp;embraces that reality directly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Use it today<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/gist.github.com\/#use-it-today\"><\/a><strong>Tokei<\/strong>&nbsp;is currently in the&nbsp;<code>0.x.x<\/code>&nbsp;phase, though I already consider the API close to stable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I could likely tag a&nbsp;<code>1.0.0<\/code>&nbsp;release today, but I would first like to gather more real-world feedback and usage experience before freezing the API surface permanently.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The package already offers far more than what is covered in this introduction, but I hope this overview gives you a good sense of its goals and capabilities.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Inspirations<a href=\"https:\/\/gist.github.com\/#inspirations\"><\/a><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For readers familiar with the topic,&nbsp;<strong>Tokei<\/strong>&nbsp;takes inspiration from the JavaScript Temporal proposal, while adapting only the concepts that make sense within PHP\u2019s ecosystem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">During development, I initially wanted to name the package&nbsp;<code>Clock<\/code>. Unfortunately, that name has already been heavily overloaded within PHP and now refers to something entirely different. Reusing it would likely have created confusion and harmed adoption. So instead, I chose the Japanese word for \u201cclock\u201d:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tokei (\u6642\u8a08)<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Last but not least<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/gist.github.com\/#last-but-not-least\"><\/a>If you work with recurring schedules, opening hours, circular intervals, or simply want better local time abstractions in PHP, give&nbsp;<strong>Tokei<\/strong>&nbsp;a try. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And if you find a bug, have suggestions, want to support the work or simply find the package useful, feel free to open an issue or drop a note on&nbsp;<a href=\"https:\/\/github.com\/bakame-php\/tokei\/\" title=\"\">its repository<\/a>.&nbsp;The package is open-source under the MIT License,&nbsp;and contributions of all kinds are&nbsp;welcome and fully credited. Your support makes a real difference.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Happy coding!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Tokei is a new PHP library dedicated to time-of-day handling, circular intervals, durations, and recurring schedules \u2014 all with microsecond precision and a developer-friendly API. It brings proper abstractions for modeling opening hours, night shifts, and any logic that crosses midnight.<\/p>\n","protected":false},"author":1,"featured_media":3095,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[5],"tags":[881,880,882,883,841,777,412,775,879],"class_list":["post-3092","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web","tag-circular-range","tag-clock","tag-duration","tag-interval","tag-oss","tag-period","tag-php","tag-range","tag-time"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/posts\/3092","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/comments?post=3092"}],"version-history":[{"count":3,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/posts\/3092\/revisions"}],"predecessor-version":[{"id":3096,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/posts\/3092\/revisions\/3096"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/media\/3095"}],"wp:attachment":[{"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/media?parent=3092"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/categories?post=3092"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/tags?post=3092"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}