{"id":39269103,"url":"https://github.com/petrknap/zoned-datetime-persistence","last_synced_at":"2026-01-25T10:02:28.799Z","repository":{"id":320917390,"uuid":"1083184512","full_name":"petrknap/zoned-datetime-persistence","owner":"petrknap","description":"Timezone aware date-time persistence","archived":false,"fork":false,"pushed_at":"2026-01-18T09:12:43.000Z","size":195,"stargazers_count":0,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-18T11:35:48.270Z","etag":null,"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"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/petrknap.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yaml","license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS","dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"custom":"https://petrknap.github.io/donate.html"}},"created_at":"2025-10-25T14:10:14.000Z","updated_at":"2026-01-17T09:32:44.000Z","dependencies_parsed_at":"2025-10-26T19:35:18.875Z","dependency_job_id":null,"html_url":"https://github.com/petrknap/zoned-datetime-persistence","commit_stats":null,"previous_names":["petrknap/zoned-datetime-persistence"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/petrknap/zoned-datetime-persistence","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petrknap%2Fzoned-datetime-persistence","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petrknap%2Fzoned-datetime-persistence/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petrknap%2Fzoned-datetime-persistence/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petrknap%2Fzoned-datetime-persistence/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/petrknap","download_url":"https://codeload.github.com/petrknap/zoned-datetime-persistence/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petrknap%2Fzoned-datetime-persistence/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28751061,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-25T09:58:17.166Z","status":"ssl_error","status_checked_at":"2026-01-25T09:55:56.104Z","response_time":113,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["datetime","doctrine","dql","eloquent","helper","java-library","jpa","jpql","laravel","orm-library","persistence","php-library","sql","summer-time","time-saving","timezone","winter-time","zoneddatetime"],"created_at":"2026-01-18T00:43:16.610Z","updated_at":"2026-01-25T10:02:28.792Z","avatar_url":"https://github.com/petrknap.png","language":"PHP","funding_links":["https://petrknap.github.io/donate.html"],"categories":[],"sub_categories":[],"readme":"# Timezone aware date-time persistence\n\n[![GitHub](https://img.shields.io/github/v/release/petrknap/zoned-datetime-persistence?include_prereleases\u0026label=GitHub\u0026style=flat)](https://github.com/petrknap/zoned-datetime-persistence/releases)\n[![JitPack](https://img.shields.io/jitpack/version/io.github.petrknap/zoned-datetime-persistence?label=JitPack\u0026style=flat)](https://jitpack.io/#io.github.petrknap/zoned-datetime-persistence)\n[![Packagist](https://img.shields.io/packagist/v/petrknap/zoned-datetime-persistence?label=Packagist\u0026style=flat)](https://packagist.org/packages/petrknap/zoned-datetime-persistence)\n\nMany data storage systems (like MySQL) do not natively support storing timezone information alongside date-time values.\nThis 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).\n\nThis package addresses the issue by providing tools that treat zoned date-time as a pair consisting of:\n- the UTC date-time value, and\n- a companion value that explicitly captures the corresponding timezone information.\n\n\n\n## Implemented\n\n- [UTC with local date-time](#utc-with-local-date-time)\n  - [How to use it](#how-to-use-it)\n- [UTC with timezone](#utc-with-timezone)\n  - [UTC with system timezone](#utc-with-system-timezone)\n  - [UTC date-time converter / type / cast](#utc-date-time-converter--type--cast)\n\n\n### UTC with local date-time\n\n\u003e `UtcWithLocal`\n\nThe **most useful** approach is to store the **UTC date-time together with its local counterpart**.\nThis dual representation enables **seamless manipulation** of date-time values directly **within storage system**.\nThe 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.\n\n#### How to use it\n\nThere is built-in support for\nthe **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)),\nthe **Doctrine ORM** (see [`Note.php`](./src/test/php/Some/Note.php) and [`DoctrineTest.php`](./src/test/php/DoctrineTest.php)),\nthe **Eloquent** (see [`NoteModel.php`](./src/test/php/Some/NoteModel.php) and [`EloquentTest.php`](./src/test/php/EloquentTest.php)),\nand, of course, it **can be integrated manually** into any project, giving you full flexibility to adapt it to your specific needs.\n\n```php\nnamespace PetrKnap\\Persistence\\ZonedDateTime;\n\n$em = DoctrineTest::prepareEntityManager();\n\n# persist entity\n$em-\u003epersist(new Some\\Note(\n    createdAt: new \\DateTimeImmutable('2025-10-30 23:52'),\n    content: \"It's dark outside...\",\n));\n$em-\u003eflush();\n\n# insert data manually (static call)\n$now = new \\DateTimeImmutable('2025-10-26 02:45', new \\DateTimeZone('CEST'));\n$em-\u003egetConnection()-\u003einsert('notes', [\n    'created_at__utc' =\u003e ZonedDateTimePersistence::computeUtcDateTime($now)-\u003eformat('Y-m-d H:i:s'),\n    'created_at__local' =\u003e $now-\u003eformat('Y-m-d H:i:s'),\n    'content' =\u003e 'We still have summer time',\n]);\n\n# insert data manually (object instance)\n$now = new UtcWithLocal(new \\DateTimeImmutable('2025-10-26 02:15', new \\DateTimeZone('CET')));\n$em-\u003egetConnection()-\u003einsert('notes', [\n    'created_at__utc' =\u003e $now-\u003egetUtcDateTime('Y-m-d H:i:s'),\n    'created_at__local' =\u003e $now-\u003egetLocalDateTime('Y-m-d H:i:s'),\n    'content' =\u003e 'Now we have winter time',\n]);\n\n# select entities\n$notes = $em-\u003ecreateQueryBuilder()\n    -\u003eselect('note')\n    -\u003efrom(Some\\Note::class, 'note')\n    -\u003ewhere('note.createdAt.local BETWEEN :from AND :to')\n    -\u003eorderBy('note.createdAt.utc')\n    -\u003egetQuery()\n    -\u003eexecute(['from' =\u003e '2025-10-26 00:00', 'to' =\u003e '2025-10-26 23:59']);\nforeach($notes as $note) {\n    echo $note-\u003egetCreatedAt()-\u003eformat('Y-m-d H:i T') . ': '. $note-\u003egetContent() . PHP_EOL;\n}\n```\n```\n2025-10-26 02:45 GMT+0200: We still have summer time\n2025-10-26 02:15 GMT+0100: Now we have winter time\n```\n\n\n### UTC with timezone\n\n\u003e `UtcWithTimezone`\n\nIf you want to **preserve the original timezone as is**, you cannot use [`UtcWithLocal`](#utc-with-local-date-time), because it works over fixed offsets.\nIn this case, you need to use this implementation.\n\n```php\nnamespace PetrKnap\\Persistence\\ZonedDateTime;\n\n$now = (new \\DateTime('2025-03-30 01:45', new \\DateTimeZone('Europe/Prague')));\n\necho 'UtcWithTimezone: ' . (new UtcWithTimezone($now))\n    -\u003etoZonedDateTime()\n    -\u003emodify('+1 hour')\n    -\u003eformat('Y-m-d H:i T' . PHP_EOL);\necho 'UtcWithLocal:    ' . (new UtcWithLocal($now))\n    -\u003etoZonedDateTime()\n    -\u003emodify('+1 hour')\n    -\u003eformat('Y-m-d H:i T' . PHP_EOL);\n```\n```\nUtcWithTimezone: 2025-03-30 03:45 CEST\nUtcWithLocal:    2025-03-30 02:45 GMT+0100\n```\n\n#### UTC with system timezone\n\n\u003e `UtcWithSystemTimezone`\n\nThe **most compact** approach is to store **only the UTC date-time**.\nThis serves as an alternative to MySQL's `TIMESTAMP`, Postgres's `TIMESTAMP WITH TIMEZONE`, and [custom ORM types](#utc-date-time-converter--type--cast).\nIt offers full range of `DateTime`, avoids normalization on connection, adds `.utc` into your queries for better readability and didn't need special configuration.\n\n#### UTC date-time converter / type / cast\n\n\u003e `UtcDateTimeConverter` \u003csup\u003e\u003csmall\u003eJakarta Persistence API\u003c/small\u003e\u003c/sup\u003e\n\nThis converter transparently manages conversions of `ZonedDateTime`, including JPQL parameters.\nThat means you **no longer need to worry** about manual timezone adjustments.\n\nFor 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).\n\n\u003e `UtcDateTimeType` \u003csup\u003e\u003csmall\u003eDoctrine ORM\u003c/small\u003e\u003c/sup\u003e\n\nIn contrast to `UtcDateTimeConverter`, this type does **not** automatically adjust the timezone of DQL parameters.\nYou must therefore **provide the type when you are calling `setParameter`** on your queries.\nAlso, you have to **register the type** in your Doctrine configuration manually.\n\nFor examples, see [the attributes `Note.createdAtUtc` and `Note.deletedAtUtc`](./src/test/php/Some/Note.php) and [the `DoctrineTest`](./src/test/php/DoctrineTest.php).\n\n\u003e `AsUtcDateTime` \u003csup\u003e\u003csmall\u003eEloquent\u003c/small\u003e\u003c/sup\u003e\n\nIn contrast to `UtcDateTimeConverter` and `UtcDateTimeType`, this cast may or may **not** adjust the timezone of any input.\nYou should therefore **handle timezone conversions explicitly everytime you are providing date-time into Eloquent**.\nBut the conversion after hydration works well.\n\nFor 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).\n\n\n---\n\nYou can [support this project via donation](https://petrknap.github.io/donate.html).\nThe project is licensed under [the terms of the `LGPL-3.0-or-later`](./COPYING.LESSER).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpetrknap%2Fzoned-datetime-persistence","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpetrknap%2Fzoned-datetime-persistence","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpetrknap%2Fzoned-datetime-persistence/lists"}