awd-studio/vo-date-time
Immutable PHP 8.3+ date-time value object. Create from strings, compare (equal/greater/less/between), and return new instances for changes like nextDay(), copy(), or modified() with DateTimePeriod offsets (days, minutes, weeks).
Installation:
composer require awd-studio/vo-date-time
Ensure your project uses PHP 8.3+ (check php -v and composer.json constraints).
First Use Case:
Replace a mutable DateTime or Carbon instance in a critical comparison. Example:
// Before (mutable, risky)
$dueDate = new DateTime('2024-12-31');
if ($dueDate > $now) { ... }
// After (immutable, safe)
$dueDate = DateTime::fromString('2024-12-31');
if ($dueDate->isGreaterThan(DateTime::now())) { ... }
Where to Look First:
DateTime class: Core immutable wrapper with comparison methods (isEqual, isBetween).DateTimePeriod: Helper for modifications (e.g., days: 1).README.md examples: Focus on fromString(), modified(), and comparison methods.Encapsulate dates in value objects within domain entities or DTOs:
class Order {
public function __construct(
private DateTime $createdAt,
private DateTime $dueDate,
) {}
public function isOverdue(): bool {
return $this->dueDate->isLessThan(DateTime::now());
}
}
Extend Laravel’s validator to enforce VO constraints:
Validator::extend('after_vo', function ($attr, $value, $params) {
$date = DateTime::fromString($value);
$comparisonDate = DateTime::fromString($params[0]);
return $date->isGreaterThan($comparisonDate);
});
// Usage:
$request->validate(['due_date' => 'required|after_vo:created_at']);
Use DateTimePeriod for business logic:
class SubscriptionService {
public function renew(Subscription $subscription): Subscription {
$renewalDate = $subscription->endsAt->modified(
new DateTimePeriod(months: 1)
);
return $subscription->renew($renewalDate);
}
}
Serialize VOs to ISO strings or timestamps:
class OrderResource extends JsonResource {
public function toArray($request) {
return [
'due_date' => $this->resource->dueDate->toIsoString(),
'is_overdue' => $this->resource->isOverdue(),
];
}
}
Use accessors/casts for Eloquent:
class Order extends Model {
protected $casts = [
'created_at' => DateTime::class,
'due_date' => DateTime::class,
];
public function getDueDateAttribute($value) {
return DateTime::fromTimestamp($value);
}
}
// Before
$date = Carbon::parse($request->input('date'));
// After
$date = DateTime::fromString($request->input('date'));
// Before
if ($date->gt($now)) { ... }
// After
if ($date->isGreaterThan(DateTime::now())) { ... }
// Before
$date->addDays(1);
// After
$date->modified(new DateTimePeriod(days: 1));
Use VOs in domain logic while keeping Carbon for persistence/APIs:
// Domain layer (VO)
$dueDate = DateTime::fromString($request->due_date);
// Persistence layer (Carbon)
$model->due_date = $dueDate->toCarbon()->format('Y-m-d');
$date = DateTime::fromString('2024-01-01', 'UTC');
$now = DateTime::fromString('2024-01-01');
$this->assertTrue($now->isEqual($now->copy()));
DateTimePeriod objects for repeated operations:
$oneDay = new DateTimePeriod(days: 1);
$tomorrow = $date->modified($oneDay);
Immutability Overhead:
nextDay() return new instances, which can bloat memory for chained operations.copy() sparingly.Timezone Defaults:
DateTime::fromString('2024-01-01', 'America/New_York');
No Built-in Serialization:
JsonSerializable or ArrayAccess by default.$date->toArray(); // Requires custom implementation.
Database Quirks:
protected $casts = [
'due_date' => [DateTime::class, 'fromTimestamp'],
];
Edge Cases in Comparisons:
isBetween() is inclusive (unlike some Carbon methods).$date->isBetweenExclusive($start, $end);
Tooling:
toCarbon() for debugging:
dd($date->toCarbon()->format('Y-m-d H:i:sP'));
phpstan to catch invalid VO usage:
// phpstan expects DateTime, not string
$this->assertInstanceOf(DateTime::class, $date);
Common Errors:
InvalidArgumentException: Thrown for malformed date strings. Validate input first:
try {
$date = DateTime::fromString($input);
} catch (InvalidArgumentException $e) {
// Handle invalid date.
}
$date1 = DateTime::fromString('...', 'UTC');
$date2 = DateTime::fromString('...', 'UTC');
$date1->isEqual($date2); // Safe.
Extension Points:
class BusinessDateTime extends DateTime {
public function isBusinessDay(): bool {
return $this->format('N') <= 5; // Mon-Fri
}
}
modified(): For custom period logic:
$date->modified(new CustomPeriod());
Performance:
$tomorrow = $date->nextDay(); // New instance.
$dayAfter = $tomorrow->nextDay(); // Another instance.
copy() Wisely: Only when you need a shallow copy (no new instance).Testing:
DateTime::fromTimestamp() with fixed timestamps:
$fixedDate = DateTime::fromTimestamp(1700000000);
$this->assertTrue($date->isEqual($expectedDate));
Laravel-Specific:
$carbon = $date->toCarbon();
$date = DateTime::fromCarbon($carbon);
public function rules() {
return [
'due_date' => ['required', function ($attr, $value, $fail) {
How can I help you explore Laravel packages today?