Download the PHP package digital-craftsman/datetime-parts without Composer
On this page you can find all versions of the php package digital-craftsman/datetime-parts. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download digital-craftsman/datetime-parts
More information about digital-craftsman/datetime-parts
Files in digital-craftsman/datetime-parts
Package datetime-parts
Short Description A Symfony bundle for more precise date and time handling through value objects
License MIT
Informations about the package datetime-parts
Date Time Precision
Use only as much date and time as you actually need.
PHP has exactly one type for anything related to time: DateTime (or DateTimeImmutable). It always contains a date, a time down to microseconds and a timezone – even if you only mean "08:00", "Christmas" or "March 2026".
This package replaces it with small value objects that contain exactly the information you're talking about. Nothing more.
| You mean … | Use | Contains |
|---|---|---|
| a specific point in time | Moment |
date + time, always in UTC |
| a date like a birthday or a holiday | Date |
year, month, day |
| a time of day like opening hours | Time |
hour, minute, second, µs |
| a month like a billing month | Month |
year, month |
| a year | Year |
year |
| a calendar week | Week |
ISO year, week number |
| a day of the week | Weekday |
Monday … Sunday |
| a day of the month | Day |
1 … 31 |
There are also value objects for durations, calendar periods, ranges and recurrences (see below).
Why?
Every piece of information you don't need is a source of bugs. When a DateTime is used for something that isn't a point in time, you suddenly have to answer questions that have nothing to do with your domain:
- Opening hours are stored as
DateTime. On which date? In which timezone? Do I need to move it to today before comparing? - A billing month is stored as
DateTime. Is it the first of the month at midnight? Midnight in which timezone? - A due date is stored as
DateTime. Does$dueDate < $nowalready fail at 00:00 UTC or at 00:00 local time?
Every developer answers those questions slightly differently, and every variant needs its own normalization before it can be compared. That's where the bugs come from.
With precise value objects these questions simply don't exist. A Time has no date, a Month has no day and a Date has no timezone. The code describes what's meant and nothing else:
Before
After
$now is a Moment and $facility->openFrom is a Time. The moment is reduced to the time of day in the facility's timezone and only then compared. You don't have to think about it.
In short:
- Less data – every value only contains what it represents.
- Less variance – there is exactly one way to represent a date, a month or a time of day.
- Fewer bugs and more readable code – no normalization before comparisons, no hidden timezone or date.
- No new date logic – everything is calculated by PHP's own
DateTimeImmutable.
A thin wrapper, not a reimplementation
This package doesn't reinvent date and time handling. The value objects are only a thin layer over DateTimeImmutable. Every calculation – modifications, differences, timezone conversions and parsing of arbitrary formats – is delegated to it. Only trivial operations without any calendar logic are done directly for performance, like comparing two dates by their numbers or reading the normalized format 2026-03-01 (validated with PHP's checkdate).
That's intentional. Date and time calculations are full of edge cases: switches between summer and winter time, leap years, months of different length, timezones that changed their offset over the years. PHP's implementation handles them and has been hardened over many years in countless applications. Writing that logic again would only mean new bugs.
So with this package:
- edge cases behave exactly like they do in native PHP, there are no surprises.
- there is no custom calendar logic that could break.
- you can reason about the result with your existing knowledge of
DateTimeImmutable.
This also applies where PHP isn't intuitive. For example 31.01. + 1 month is 03.03. like with DateTimeImmutable::modify('+1 month'). The package doesn't "fix" this, as that would mean a second, different behaviour.
The value objects add precision and readability – the calculations stay where they belong.
Installation
The package follows semantic versioning. Changes are described in upgrade guide.
The basic rule: Moments are UTC, everything else has no timezone
- A
Momentis always stored in UTC. ADateTimein another timezone is converted to UTC when theMomentis created (the point in time stays the same). Date,Time,Month,Yearetc. don't have a timezone at all. They're just the values.- Whenever a
Momentmeets one of them, you tell it in which timezone to look at it. That's what the*InTimeZonemethods are for.
Comparing
The same comparison methods work with all types. The type of the parameter decides which part of the moment is compared:
Values of the same type are compared directly:
Every comparison has an assertion counterpart which throws an exception. You can pass your own exception:
Modifying
All value objects are immutable. Modifications on a Moment that depend on the calendar are done in the given timezone, the result is still in UTC:
This way you don't gain or lose an hour when there is a switch between summer and winter time in that timezone.
As the value objects are immutable, a modification without using the result has no effect. All methods that return a new instance are marked with #[\NoDiscard], so PHP warns you about it:
Durations and calendar periods
"6 hours" and "1 day" sound similar but aren't. Therefore there are two different value objects:
Durationis elapsed time like 90 minutes. It's always exact and independent of any timezone (microsecond precision).CalendarPeriodis a movement in the calendar like 1 day, 2 weeks, 3 months, 1 quarter or 1 year. Its length depends on the calendar, so it's applied in a timezone.
On the night of the time switch $expiresAt is exactly 6 hours later, while $sameTimeTomorrow is 23 or 25 hours later – but at the same local time.
Values without a time don't need a timezone. They only accept units that fit their precision (e.g. a Month can't be moved by days). A Time accepts a Duration and wraps around midnight.
The distance between two values is returned in the same way:
As all calculations are done by DateTimeImmutable (see A thin wrapper, not a reimplementation), they follow its behaviour, even where it's not intuitive. For example 31.01. + 1 month is 03.03. and from 31.01. until 01.03. is 0 full months.
Ranges
| Range | Type | Example |
|---|---|---|
DateRange |
closed – start and end are included | 01.01. until 03.01. are 3 days |
MomentRange |
half-open – the end isn't included | 10:00–12:00 doesn't overlap with 12:00–14:00 |
TimeRange |
half-open, may wrap around midnight, 00:00 as end = 24:00 |
21:00–03:00 is a night shift |
Whether start and end count for contains can be changed with a PeriodLimit:
Stricter rules can be enforced with guards, e.g. in the constructor of your own value object:
Weeks and recurrences
Week is a calendar week according to ISO 8601. It starts on Monday and belongs to the year of its Thursday, so around new year the year of a week can differ from the year of its dates.
Recurrence describes on which dates something happens: daily, on specific weekdays or on specific days of the month (days which don't exist in a month are skipped). It contains neither a time nor a start or end.
Collections
Every value object has a collection with unique values (e.g. Dates, Days, Weekdays, Months, TimeRanges). The collections are countable, iterable and provide methods like filter, map, sort, first, last, min and max.
Symfony and Doctrine
The bundle integrates everything automatically:
- Serializer – all value objects are normalized and denormalized through
digital-craftsman/self-aware-normalizers. -
Doctrine – there is a Doctrine type for every value object, registered automatically. The class name is the type name:
- Clock – inject the
Clockinterface to get the currentMoment.SystemClockis used by default and replaced withFrozenClockin thetestenvironment, so you can freeze the time in tests:
Quality
As time handling is a central part of every application, the package is tested thoroughly, including mutation testing with 100% MSI. More than 80% of the code in this repository are tests.
Contribution
The local setup is built with Docker and controlled through Make commands. Run make to see all available commands.
Before creating a PR, please run make verify to run all tests and validations locally.
All versions of datetime-parts with dependencies
digital-craftsman/self-aware-normalizers Version ^1.4.0
doctrine/dbal Version ^4.5
symfony/framework-bundle Version ^7.4|^8.0
symfony/serializer Version ^7.4|^8.0