Using Clock in Your Apps
FrankPHP v1.3.0 addsApp\Core\Clock, a small framework-owned utility that gives every application the same date/time contract:
UTC at rest. Local at the edges.That sentence is the whole model. Store exact instants in UTC. Interpret user input in the user’s local timezone. Format output in the resolved local timezone. Calculate local date filters in PHP before sending UTC boundaries to SQL. This cookbook shows the patterns to use in real application code.
What Clock is for
Clock owns mechanical date/time work:
- current UTC timestamp generation
- UTC offsets such as
+1 houror-30 days - timezone validation
- timezone resolution from user, tenant, config, and fallback
- local input conversion into UTC
- UTC conversion back to local output
- UTC query boundaries for local dates
Today, Yesterday, Never, Overdue, Recently active, or Dormant belong in your Service layer because they are application decisions.
The import
UseClock wherever application code needs to create, convert, or prepare date/time values.
Clock is a stateless static utility. It does not need a container binding.
Resolve timezone first
Most date/time code should start by resolving the timezone explicitly.Do not let
Clock read from $_SESSION, middleware globals, request globals, or the database. Pass the user, tenant, and config context explicitly.Recipe: create a UTC timestamp for storage
Use this when inserting or updating application-managed timestamp columns.DATETIME columns.
Do not use:
Recipe: create an expiry timestamp
UseClock::utcOffsetString() for expiry windows and cutoffs.
Recipe: convert a local form input into UTC
Use this when a user enters a local date/time and the value represents an exact instant. Example form value:$startsAtUtc in a UTC DATETIME column.
Recipe: handle a date-only value
Some values are not instants in time. Examples:- birthday
- anniversary date
- local calendar-only due date
- billing month date with no time-of-day meaning
DATE column for these values, not DATETIME.
Recipe: format a stored UTC value for display
Convert UTC values to the resolved timezone before passing them to a view.Clock.
Recipe: filter records for a local date
This is one of the most importantClock patterns.
A user may ask for records on 2026-06-06 in Europe/London. The database stores UTC. The application must calculate the UTC start and end boundaries for that local date before querying.
Recipe: filter records across a local date range
Use this for reports, calendars, dashboards, and exports.Recipe: get today for an input default
Sometimes you need a local date for an input default.nowForTimezone() values as application timestamps.
Recipe: prepare a display model in a Service
This is the preferred FrankPHP pattern. The Service handles business logic and display preparation. The Controller passes the result to the view. The view renders only.startsAtLabel. It does not decide how to format starts_at.
Recipe: migrate an existing private helper
Some applications may already have local helpers such asutcNow().
During migration, keep the call sites stable and delegate the helper to Clock temporarily.
Where Clock should and should not be used
The build rules
When building or reviewing FrankPHP code, apply these rules:- Use
App\Core\Clockfor persisted timestamp generation. - Store exact instants as UTC
DATETIME. - Use
DATEfor date-only values. - Do not use SQL
NOW()orCURRENT_TIMESTAMPfor application-managed timestamps. - Convert local user, tenant, or app input into UTC before storage.
- Convert UTC values into the resolved local timezone before display.
- Calculate local date query boundaries in PHP through
Clock. - Resolve timezone explicitly using
Clock::resolveTimezone($user, $tenant, $config). - Do not call
Clockfrom views. - Services must prepare display-ready date labels for views.
- Document intentional application-level deviations in
MYAPP.md.
Common mistakes
Mistake: using SQL DATE(starts_at) for local calendar filtering
This usually filters by the database/session interpretation of the stored value, not the user’s local calendar day.
Use Clock::utcRangeForLocalDate() instead.
Mistake: storing local input directly
If a user inEurope/London enters 2026-06-06 14:30, that is a local input value. Convert it to UTC before storage.
Mistake: formatting in the view
This spreads timezone rules into templates and makes the app harder for AI tools to reason about. Prepare labels in Services or Controllers.Mistake: converting DATE values
A birthday is not a timestamp. Keep it as DATE.
Quick reference
Conclusion
Clock is deliberately small, but it changes the reliability of every FrankPHP application.
Use UTC for stored instants. Use local timezones at the edges. Keep views simple. Keep SQL timezone-neutral. Let Services prepare the values the user actually sees.