digital-craftsman/date-time-precision
Thin PHP value objects for precise date/time concepts: Moment (UTC-backed) plus Time, Date, Month, Year, Day, Weekday and collections. Avoid misleading DateTime comparisons, handle timezone-safe modifications (DST), with Symfony normalizers and Doctrine types.
Installation:
composer require digital-craftsman/date-time-precision:0.14.*
Pin to a minor version to avoid breaking changes.
Basic Usage:
Replace \DateTime with Moment for precise time handling:
use DigitalCraftsman\DateTimePrecision\Moment;
use DigitalCraftsman\DateTimePrecision\Time;
$now = Moment::now(); // UTC by default
$openingTime = Time::fromString('09:00:00', 'Europe/Berlin');
First Use Case: Validate business hours in a timezone-aware way:
if ($now->isBeforeInTimeZone($openingTime, 'Europe/Berlin')) {
throw new FacilityNotOpenException();
}
Moment: Immutable wrapper for \DateTime (always UTC internally).Time, Date, Month, Year, Day, Days.SystemClock (default) or FrozenClock (testing).// Compare a moment (UTC) with a time in a facility's timezone
if ($appointment->scheduledAt->isBeforeInTimeZone($facility->closingTime, $facility->timezone)) {
// Valid appointment
}
// Add 7 days in the facility's timezone (result remains UTC internally)
$newDeadline = $now->modifyInTimeZone('+7 days', 'Europe/Berlin');
#[ORM\Entity]
class Booking {
#[ORM\Column(type: Moment::class)]
public Moment $createdAt;
#[ORM\Column(type: Time::class)]
public Time $scheduledTime;
}
// In tests, replace SystemClock with FrozenClock
$clock = new FrozenClock(Moment::fromString('2023-01-01'));
$now = $clock->now(); // Always returns 2023-01-01
use DigitalCraftsman\DateTimePrecision\Date;
class DateRule extends AbstractRule {
public function passes($attribute, $value) {
return Date::fromString($value)->isAfter(Date::today());
}
}
return response()->json([
'opening_time' => $facility->openingTime->format('H:i'),
]);
Days or Weekdays for bulk operations:
$holidays = Days::fromArray(['2023-12-25', '2023-12-26']);
if ($date->isIn($holidays)) { /* ... */ }
$dates = $startDate->datesUntil($endDate);
Timezone Assumptions:
Moment objects are UTC internally. Modifications in other timezones (e.g., modifyInTimeZone) return UTC Moments.// ❌ Avoid: Assumes UTC!
$now->isBefore($facility->openingTime); // May fail due to timezone mismatch
Fix: Always pass the correct timezone:
$now->isBeforeInTimeZone($facility->openingTime, $facility->timezone);
Immutability:
// ❌ Silent no-op
$now->modifyInTimeZone('+1 day', 'UTC');
// ✅ Correct
$tomorrow = $now->modifyInTimeZone('+1 day', 'UTC');
Doctrine Quirks:
DigitalCraftsman\DateTimePrecision\Moment) in ORM mappings.requiresSQLCommentHint()) are required for Doctrine 3.x compatibility.Deprecations:
isDateAfterInTimeZone() are deprecated in favor of isAfterInTimeZone($date, $timezone).FrozenClock in tests to isolate time-dependent logic:
$this->app->bind(Clock::class, fn() => new FrozenClock(Moment::fromString('2023-01-01')));
\Log::debug('Timezone used:', ['timezone' => $facility->timezone]);
digital-craftsman/self-aware-normalizers is installed for API responses:
composer require digital-craftsman/self-aware-normalizers
Custom Comparisons:
Extend Moment or precision objects for domain-specific logic:
class BusinessHours {
public static function isWithin(Moment $moment, Time $open, Time $close, string $timezone) {
return $moment->isBetweenInTimeZone($open, $close, $timezone);
}
}
Doctrine Types: Create custom types for legacy systems:
#[ORM\Column(type: 'string')] // Store as ISO string
private string $legacyDateTime;
// Convert on get/set
public function getScheduledAt(): Moment {
return Moment::fromString($this->legacyDateTime);
}
Clock Strategies:
Implement Clock for custom time sources (e.g., API-based time):
class ApiClock implements Clock {
public function now(): Moment {
return Moment::fromString(file_get_contents('https://api.time.com'));
}
}
Moment over DateTime for UTC consistency, but benchmark storage size (e.g., TIMESTAMP vs. string).DateTimeZone objects:
private DateTimeZone $berlinTz = new DateTimeZone('Europe/Berlin');
date.timezone in PHP.ini—all Moments are UTC.config/packages/doctrine.yaml includes:
doctrine:
orm:
mappings:
datetime_precision:
type: attribute
prefix: 'DigitalCraftsman\DateTimePrecision'
dir: '%kernel.project_dir%/vendor/digital-craftsman/date-time-precision/src/Doctrine'
How can I help you explore Laravel packages today?