php-standard-library/date-time
Immutable, timezone-aware DateTime types for PHP. Provides Duration, Period, and Interval helpers for safer date/time arithmetic and ranges, designed as a standard-library style package with clear docs and contribution links.
Installation
composer require php-standard-library/date-time
Add to composer.json under require if production, or require-dev for testing.
First Use Case: Immutable DateTime Creation
use PhpStandardLibrary\DateTime\DateTime;
$date = DateTime::fromIsoString('2026-05-20T12:00:00+00:00');
// or
$now = DateTime::now('America/Chicago');
Key Classes to Explore
DateTime: Immutable, timezone-aware wrapper (like Carbon but stricter).Duration: Represents a span of time (e.g., Duration::fromHours(2.5)).Period: Fixed intervals (e.g., Period::months(3) for billing cycles).Interval: Dynamic ranges (e.g., Interval::fromStartEnd($start, $end)).Where to Look First
tests/ for examples of timezone-aware operations.Carbon via adapters (e.g., Carbon::instance($dateTime)).Pattern: Use accessors/mutators for Eloquent models.
use PhpStandardLibrary\DateTime\DateTime;
class Order extends Model
{
protected $casts = [
'created_at' => DateTime::class,
'due_date' => DateTime::class,
];
// Accessor for formatted output
public function getDueDateFormattedAttribute(): string
{
return $this->due_date->format('Y-m-d H:i');
}
}
Pattern: Normalize all timestamps to UTC or user’s timezone.
use PhpStandardLibrary\DateTime\DateTime;
public function show(Order $order)
{
$userTimezone = auth()->user()->timezone ?? 'UTC';
$dueDate = $order->due_date->inTimezone($userTimezone);
return response()->json([
'due_date' => $dueDate->toIsoString(),
]);
}
Pattern: Replace CarbonPeriod with Period for subscriptions/billing.
use PhpStandardLibrary\DateTime\Period;
$subscription = Subscription::create([
'period' => Period::months(3), // Immutable, testable
'ends_at' => DateTime::now()->add($subscription['period']),
]);
// Validate renewal
if ($subscription->ends_at->isPast()) {
$subscription->renew();
}
Pattern: Use Duration for dynamic delays (e.g., retries, queues).
use PhpStandardLibrary\DateTime\Duration;
use Illuminate\Support\Facades\Bus;
Bus::dispatch(new ProcessOrder($order))
->delay(Duration::fromMinutes(5)); // Laravel 9+ compatible
Pattern: Extend Laravel’s validation to use Duration/Period.
use PhpStandardLibrary\DateTime\Duration;
use Illuminate\Validation\Rule;
$validator = Validator::make($data, [
'duration' => [
'required',
function ($attribute, $value, $fail) {
if (!Duration::tryFromString($value)) {
$fail('Invalid duration format (e.g., "2 hours").');
}
},
],
]);
Pattern: Mock DateTime in unit tests.
use PhpStandardLibrary\DateTime\DateTime;
use PhpStandardLibrary\DateTime\Duration;
public function test_order_expiry()
{
$fixedTime = DateTime::fromIsoString('2026-01-01T00:00:00+00:00');
$order = new Order(['due_date' => $fixedTime->add(Duration::fromDays(7))]);
$this->assertTrue($order->isDue());
}
Pattern: Convert DateTime to JS-compatible timestamps.
use PhpStandardLibrary\DateTime\DateTime;
Broadcast::channel('orders', function ($user) {
return [
'timezone' => $user->timezone,
'last_updated' => DateTime::now()->getTimestamp(),
];
});
Immutability Overhead
$newDate = $date->add(...)).Duration/Period for bulk operations.
$dates = collect(range(1, 10))->map(fn ($i) =>
$baseDate->add(Duration::fromDays($i))
);
Timezone Defaults
DateTime::now() defaults to UTC (unlike Carbon’s local timezone).$now = DateTime::now('UTC'); // or user’s timezone
Serialization Quirks
JsonSerializable or use Laravel’s toJson():
$date->toJson(); // Returns ISO string
Method Naming Differences
Duration::add() vs. Carbon::addDays().class CarbonAdapter {
public static function fromDateTime(DateTime $dateTime): Carbon
{
return Carbon::instance($dateTime);
}
}
Database Casting
$casts may not auto-convert timestamps.protected $casts = [
'created_at' => [DateTime::class, 'datetime:Y-m-d H:i:s'],
];
Lack of Laravel-Specific Helpers
parse() like Carbon::parse().DateTime::macro('parse', function ($time, $format = null) {
return $format ? DateTime::fromFormat($format, $time)
: DateTime::fromIsoString($time);
});
Performance in Loops
// Slow: Creates 1000 new objects
collect(range(1, 1000))->map(fn ($i) => $date->add(Duration::fromDays($i)));
// Faster: Reuse base object
$base = $date;
$dates = collect(range(1, 1000))->map(fn ($i) => $base->add(Duration::fromDays($i)));
Log Timezones Explicitly
\Log::debug('Timezone:', ['date' => $date->getTimezone()->getName()]);
Use toIsoString() for Consistency
$date->toIsoString(); // Always UTC-based, debug-friendly
Validate with Period/Duration
if (!$duration instanceof Duration) {
throw new \InvalidArgumentException('Expected Duration');
}
Check for Mixed Carbon/DateTime Usage
# phpstan.neon
parameters:
level: 7
rules:
PhpStandardLibrary\DateTime\Rules\NoMixedCarbonDateTime: true
Custom Formatters
DateTime::macro('toLaravelFormat', function () {
return $this->format('Y-m-d H:i:s.uP');
});
Business Logic Extensions
DateTime::macro('isBusinessDay', function () {
$day = (int) $this->format('N');
return $day !== 6 && $day !== 7
How can I help you explore Laravel packages today?