apie/date-value-objects
Date-related value objects for PHP/Apie that model only the date/time parts you need (LocalDate, Time, HourAndMinutes, UnixTimestamp, DateWithTimezone). Helps validate expected formats without relying on full DateTimeImmutable.
Install the Package
composer require apie/date-value-objects
Ensure your composer.json targets PHP 8.1+ and Laravel 9+.
First Use Case: LocalDate for User-Facing Dates
Replace a Carbon instance with a LocalDate in a Laravel controller:
use Apie\DateValueObjects\LocalDate;
public function showEvent(DateTimeInterface $eventDate)
{
$localDate = LocalDate::fromDateTime($eventDate);
return view('events.show', ['date' => $localDate]);
}
Key Starting Points
LocalDate: For dates without time (e.g., birthdays, event dates).
$date = LocalDate::fromString('2023-12-31');
$nextMonth = $date->nextMonth(); // Handles Feb 28/29 automatically
DateWithTimezone: For timezone-aware operations (e.g., scheduling).
$tzDate = DateWithTimezone::fromAtom('2023-12-31T14:30:00+02:00');
$utcTime = $tzDate->inTimezone('UTC');
UnixTimestamp: For immutable event timestamps (e.g., logs).
$timestamp = UnixTimestamp::fromDateTime(new DateTime());
Where to Look First
WorksWithDays, WorksWithMonths for domain-specific methods.DateTime in domain entities with value objects.
// Before: Ambiguous
class Event {
public function __construct(public DateTimeImmutable $date) {}
}
// After: Explicit
class Event {
public function __construct(public LocalDate $date) {}
}
class Event extends Model {
public function scheduledAt(): DateWithTimezone {
return DateWithTimezone::fromDateTime($this->attributes['scheduled_at']);
}
}
use Apie\DateValueObjects\LocalDate;
use Illuminate\Validation\Rule;
public function rules(): array {
return [
'event_date' => ['required', Rule::function('date')->make(function ($attribute, $value) {
return LocalDate::fromString($value); // Fails on invalid dates
})],
];
}
public function toArray($request) {
return [
'date' => $this->event->date->toIsoString(),
];
}
DateWithTimezone for user-provided times.
$userTimezone = 'America/New_York';
$eventTime = DateWithTimezone::fromAtom('2023-12-31T14:00:00-05:00');
$utcEvent = $eventTime->inTimezone('UTC'); // Convert for storage
config('app.timezone'):
$localTime = $utcEvent->inTimezone(config('app.timezone'));
$date = LocalDate::fromString('2023-01-31');
$nextMonth = $date->nextMonth(); // Auto-adjusts for Feb 28/29
$nextYear = $date->nextYear();
UnixTimestamp as BIGINT).
// Migration
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->bigInteger('scheduled_at'); // UnixTimestamp
});
// Model
public function setScheduledAtAttribute($value) {
$this->attributes['scheduled_at'] = UnixTimestamp::fromDateTime($value)->value();
}
protected $casts = [
'scheduled_at' => DateWithTimezone::class,
];
CastsAttributes for complex logic:
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class DateWithTimezoneCaster implements CastsAttributes {
public function get($model, string $key, $value, array $attributes) {
return DateWithTimezone::fromDateTime($value);
}
public function set($model, string $key, $value, array $attributes) {
return $value->toDateTime();
}
}
public function __construct(
private LocalDate $eventDate,
private HourAndMinutes $startTime
) {
$this->eventDate = LocalDate::fromString($this->request->event_date);
$this->startTime = HourAndMinutes::fromString($this->request->start_time);
}
toIsoString() or toAtom() for consistency.
return response()->json([
'event' => [
'date' => $event->date->toIsoString(),
'time' => $event->time->toString(),
],
]);
public function handle() {
$timestamp = UnixTimestamp::fromDateTime(new DateTime());
// Store $timestamp->value() in DB or queue
}
public function testNextMonth() {
$date = LocalDate::fromString('2023-01-31');
$next = $date->nextMonth();
$this->assertEquals('2023-02-28', $next->toIsoString());
}
$response = $this->get('/events/1');
$response->assertJsonStructure([
'event' => [
'date' => 'string', // ISO format
'time' => 'string', // HH:MM format
],
]);
DateWithTimezone assumes ATOM format (YYYY-MM-DDTHH:MM:SS±HH:MM). Invalid formats (e.g., YYYY-MM-DD HH:MM:SS) will throw exceptions.DateWithTimezone::fromDateTime() with a pre-parsed DateTime:
$dateTime = DateTime::createFromFormat('Y-m-d H:i:s', '2023-12-31 14:30:00');
$tzDate = DateWithTimezone::fromDateTime($dateTime->setTimezone(new DateTimeZone('UTC')));
nextMonth() on LocalDate with 2023-01-31 returns 2023-02-28 (correct), but chaining nextMonth() repeatedly may not handle year boundaries intuitively.$date = LocalDate::fromString('2023-01-31');
$date = $date->nextMonth()->nextMonth(); // 2023-03-31 (invalid)
$date = $date->normalize(); // 2023-03-31 → 2023-04-01 (if supported)
How can I help you explore Laravel packages today?