digital-craftsman / datetime-parts
A Symfony bundle for more precise date and time handling through value objects
Installs: 3 175
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 8
Type:symfony-bundle
Requires
- php: 8.2.*|8.3.*
- doctrine/dbal: ^3.4.0
- symfony/framework-bundle: ^6.4|^7.0
- symfony/serializer: ^6.4|^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.40
- infection/infection: ^0.26.15
- phpunit/phpunit: ^10.5
- vimeo/psalm: ^5.16
This package is auto-updated.
Last update: 2024-12-07 14:22:14 UTC
README
The only class in the PHP SPL to work with dates and times is DateTime
(and it's immutable counterpart). It represents a specific moment in time in a specific timezone (whether that timezone/offset is explicitly defined or not). Unfortunately dates are complex and there is more than just a moment in time. For example there are
- time of day which is relevant on every day (like business hours).
- a specific date (like Christmas) which isn't bound to a timezone.
- a subset of a date time like time, date, month or year.
Basically, every time you're not talking about a specific moment, the DateTime
classes contain not just more informationen then needed, but even misleading information.
This bundle / package tries to solve this issue by introducing a wrapper value object Moment
for a moment in time and value objects for the more precise sub sets like Time
, Date
, Month
and Year
.
It's only a thin wrapper over DateTime
and uses it internally for all modifications and comparisons. This way you don't have to make sure that your DateTime
is
- at a specific date to compare times.
- at midnight to compare dates.
- at the first of the month to compare months.
- at the first of the year to compare years.
Additionally, the package provides a streamlined way to have the system running in UTC
but still do the modifications in the relevant timezone. The internal DateTime
is always in UTC
and only internally converted to the relevant timezone for modifications. This way you can be sure that you're not missing or receiving an hour due to a switch of summer-time to winter-time in the relevant timezone.
This Symfony bundle includes Symfony normalizers for automatic normalization and denormalization and Doctrine types to store the objects directly in the database.
As it's a central part of an application, it's tested thoroughly (including mutation testing). Currently, more than 80% of the lines of code in this repository are tests.
Installation and configuration
Install package through composer:
composer require digital-craftsman/date-time-precision
⚠️ This bundle can be used (and is being used) in production, but hasn't reached version 1.0 yet. Therefore, there will be breaking changes between minor versions. I'd recommend that you require the bundle only with the current minor version like
composer require digital-craftsman/date-time-precision:0.10.*
. Breaking changes are described in the releases and the changelog. Updates are described in the upgrade guide.
When would I need that?
Basically whenever you use a DateTime
object for something other than a single moment.
Storing more information in those cases just lead to more questions, like "When storing the month, do we store the first day of month at midnight, and if so, in which time zone?" and therefore increases complexity. Additionally, you need mutate or reduce the point in time to be able to compare it. With the package it will be as easy as:
if ($now->isBeforeInTimeZone($facility->openFrom, $facilityTimeZone)) { throw new FacilityIsNotOpenYet(); }
$now
is a Moment
(in UTC) and $facility->openFrom
is a Time
(in the timezone of the facility).
The idea is that your system can run in UTC
and all moments are in the timezone UTC
. But all values that have an implicit time zone like a date or a time of day will be stored with just the data needed. This way we're getting rid of additional data that creates more surface for possible bugs. Through precise value objects and specific comparison functions, the code is more readable than before.
if ($now->isBeforeInTimeZone($facility->earliestDayOfBooking)) { throw new BookingNotPossibleYet(); }
$now
is a Moment
(in UTC) and $facility->earliestDayOfBooking
is a Date
(in the timezone of the facility). The same method isBeforeInTimeZone
that is used previously for the time comparison is the same that is used here. Depending on the type of the second parameter, the comparison is done on the relevant part of the moment.
Modifications work the same way.
$bookingsAllowedFrom = $now->modifyInTimeZone('+ 7 days', $facilityTimeZone);
The resulting $bookingsAllowedFrom
is still a date time with timezone UTC
but the modification is done in the relevant timezone.
Integration
For the best code readability, it's best to use the Moment
provided with the package as a full replacement for \DateTime
or \DateTimeImmutable
when you're speaking about a moment in time and the others value objects for the rest.
The package integrates with the normalizers of digital-craftsman/self-aware-normalizers
and provides Doctrine types (that use those interfaces) for Moment
and all parts.
The Doctrine types are automatically registered with the bundle with the following type names:
dtp_moment
dtp_time
dtp_weekday
dtp_weekdays
dtp_date
dtp_month
dtp_year
The package also contains a clock component consisting of the interface Clock
with the two implementations SystemClock
(for general use) and FrozenClock
(for testing). The SystemClock
will be autowired for the Clock
and automatically replaced with FrozenClock
in the test environment.
Design
Immutability
All mutations on the Moment
and its parts are immutable.
Contribution
The local setup is build with Docker and controlled through Make commands. Run make
to see all available commands and what they do.
Before creating a PR or pushing any code, please run make verify
to run all tests and validations locally.