https://github.com/petrknap/zoned-datetime-persistence
Timezone aware date-time persistence
https://github.com/petrknap/zoned-datetime-persistence
datetime doctrine dql eloquent helper java-library jpa jpql laravel orm-library persistence php-library sql summer-time time-saving timezone winter-time zoneddatetime
Last synced: 4 months ago
JSON representation
Timezone aware date-time persistence
- Host: GitHub
- URL: https://github.com/petrknap/zoned-datetime-persistence
- Owner: petrknap
- License: lgpl-3.0
- Created: 2025-10-25T14:10:14.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-01-18T09:12:43.000Z (4 months ago)
- Last Synced: 2026-01-18T11:35:48.270Z (4 months ago)
- Topics: datetime, doctrine, dql, eloquent, helper, java-library, jpa, jpql, laravel, orm-library, persistence, php-library, sql, summer-time, time-saving, timezone, winter-time, zoneddatetime
- Language: PHP
- Homepage:
- Size: 190 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yaml
- License: COPYING
- Authors: AUTHORS
Awesome Lists containing this project
README
# Timezone aware date-time persistence
[](https://github.com/petrknap/zoned-datetime-persistence/releases)
[](https://jitpack.io/#io.github.petrknap/zoned-datetime-persistence)
[](https://packagist.org/packages/petrknap/zoned-datetime-persistence)
Many data storage systems (like MySQL) do not natively support storing timezone information alongside date-time values.
This limitation introduces ambiguity when handling zoned date-times — particularly in applications operating across multiple timezones or even within a single timezone that observes multiple offsets (e.g. due to daylight saving time).
This package addresses the issue by providing tools that treat zoned date-time as a pair consisting of:
- the UTC date-time value, and
- a companion value that explicitly captures the corresponding timezone information.
## Implemented
- [UTC with local date-time](#utc-with-local-date-time)
- [How to use it](#how-to-use-it)
- [UTC with timezone](#utc-with-timezone)
- [UTC with system timezone](#utc-with-system-timezone)
- [UTC date-time converter / type / cast](#utc-date-time-converter--type--cast)
### UTC with local date-time
> `UtcWithLocal`
The **most useful** approach is to store the **UTC date-time together with its local counterpart**.
This dual representation enables **seamless manipulation** of date-time values directly **within storage system**.
The local date-time is ideal for grouping and filtering based on user or business context, while the UTC value ensures consistent and accurate sorting across timezones.
#### How to use it
There is built-in support for
the **Jakarta Persistence API** (see [`Note.java`](./src/test/java/some/Note.java) and [`JpaTest.java`](./src/test/java/io/github/petrknap/persistence/zoneddatetime/JpaTest.java)),
the **Doctrine ORM** (see [`Note.php`](./src/test/php/Some/Note.php) and [`DoctrineTest.php`](./src/test/php/DoctrineTest.php)),
the **Eloquent** (see [`NoteModel.php`](./src/test/php/Some/NoteModel.php) and [`EloquentTest.php`](./src/test/php/EloquentTest.php)),
and, of course, it **can be integrated manually** into any project, giving you full flexibility to adapt it to your specific needs.
```php
namespace PetrKnap\Persistence\ZonedDateTime;
$em = DoctrineTest::prepareEntityManager();
# persist entity
$em->persist(new Some\Note(
createdAt: new \DateTimeImmutable('2025-10-30 23:52'),
content: "It's dark outside...",
));
$em->flush();
# insert data manually (static call)
$now = new \DateTimeImmutable('2025-10-26 02:45', new \DateTimeZone('CEST'));
$em->getConnection()->insert('notes', [
'created_at__utc' => ZonedDateTimePersistence::computeUtcDateTime($now)->format('Y-m-d H:i:s'),
'created_at__local' => $now->format('Y-m-d H:i:s'),
'content' => 'We still have summer time',
]);
# insert data manually (object instance)
$now = new UtcWithLocal(new \DateTimeImmutable('2025-10-26 02:15', new \DateTimeZone('CET')));
$em->getConnection()->insert('notes', [
'created_at__utc' => $now->getUtcDateTime('Y-m-d H:i:s'),
'created_at__local' => $now->getLocalDateTime('Y-m-d H:i:s'),
'content' => 'Now we have winter time',
]);
# select entities
$notes = $em->createQueryBuilder()
->select('note')
->from(Some\Note::class, 'note')
->where('note.createdAt.local BETWEEN :from AND :to')
->orderBy('note.createdAt.utc')
->getQuery()
->execute(['from' => '2025-10-26 00:00', 'to' => '2025-10-26 23:59']);
foreach($notes as $note) {
echo $note->getCreatedAt()->format('Y-m-d H:i T') . ': '. $note->getContent() . PHP_EOL;
}
```
```
2025-10-26 02:45 GMT+0200: We still have summer time
2025-10-26 02:15 GMT+0100: Now we have winter time
```
### UTC with timezone
> `UtcWithTimezone`
If you want to **preserve the original timezone as is**, you cannot use [`UtcWithLocal`](#utc-with-local-date-time), because it works over fixed offsets.
In this case, you need to use this implementation.
```php
namespace PetrKnap\Persistence\ZonedDateTime;
$now = (new \DateTime('2025-03-30 01:45', new \DateTimeZone('Europe/Prague')));
echo 'UtcWithTimezone: ' . (new UtcWithTimezone($now))
->toZonedDateTime()
->modify('+1 hour')
->format('Y-m-d H:i T' . PHP_EOL);
echo 'UtcWithLocal: ' . (new UtcWithLocal($now))
->toZonedDateTime()
->modify('+1 hour')
->format('Y-m-d H:i T' . PHP_EOL);
```
```
UtcWithTimezone: 2025-03-30 03:45 CEST
UtcWithLocal: 2025-03-30 02:45 GMT+0100
```
#### UTC with system timezone
> `UtcWithSystemTimezone`
The **most compact** approach is to store **only the UTC date-time**.
This serves as an alternative to MySQL's `TIMESTAMP`, Postgres's `TIMESTAMP WITH TIMEZONE`, and [custom ORM types](#utc-date-time-converter--type--cast).
It offers full range of `DateTime`, avoids normalization on connection, adds `.utc` into your queries for better readability and didn't need special configuration.
#### UTC date-time converter / type / cast
> `UtcDateTimeConverter` Jakarta Persistence API
This converter transparently manages conversions of `ZonedDateTime`, including JPQL parameters.
That means you **no longer need to worry** about manual timezone adjustments.
For examples, see [the attributes `Note.createdAtUtc` and `Note.deletedAtUtc`](./src/test/java/some/Note.java) and [the `JpaTest`](./src/test/java/io/github/petrknap/persistence/zoneddatetime/JpaTest.java).
> `UtcDateTimeType` Doctrine ORM
In contrast to `UtcDateTimeConverter`, this type does **not** automatically adjust the timezone of DQL parameters.
You must therefore **provide the type when you are calling `setParameter`** on your queries.
Also, you have to **register the type** in your Doctrine configuration manually.
For examples, see [the attributes `Note.createdAtUtc` and `Note.deletedAtUtc`](./src/test/php/Some/Note.php) and [the `DoctrineTest`](./src/test/php/DoctrineTest.php).
> `AsUtcDateTime` Eloquent
In contrast to `UtcDateTimeConverter` and `UtcDateTimeType`, this cast may or may **not** adjust the timezone of any input.
You should therefore **handle timezone conversions explicitly everytime you are providing date-time into Eloquent**.
But the conversion after hydration works well.
For examples, see [the attributes `NoteModel.created_at_utc` and `NoteModel.deleted_at_utc`](./src/test/php/Some/NoteModel.php) and [the `EloquentTest`](./src/test/php/EloquentTest.php).
---
You can [support this project via donation](https://petrknap.github.io/donate.html).
The project is licensed under [the terms of the `LGPL-3.0-or-later`](./COPYING.LESSER).