A date parser can return an object after normalizing an impossible calendar date. That behavior is useful for date arithmetic, but it is usually wrong for a form field that promises to accept an exact date. This example requires the precise YYYY-MM-DD spelling and rejects parser warnings.
Prerequisites are PHP 8.3 or a compatible supported PHP 8 release with the date extension. The example uses UTC explicitly and represents a calendar date at midnight. It does not interpret a user appointment time or convert between local time zones.
Complete example
Save as date.php:
<?php
function strictDate(string $text): DateTimeImmutable {
if (!preg_match('/\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/', $text)) {
throw new InvalidArgumentException('Use YYYY-MM-DD');
}
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $text, new DateTimeZone('UTC'));
$errors = DateTimeImmutable::getLastErrors();
if ($date === false || ($errors !== false &&
($errors['warning_count'] > 0 || $errors['error_count'] > 0)) ||
$date->format('Y-m-d') !== $text || $date->format('Y') === '0000') {
throw new InvalidArgumentException('Invalid calendar date');
}
return $date;
}
if (strictDate('2024-02-29')->format('H:i:s') !== '00:00:00') {
throw new RuntimeException('Unexpected time');
}
$count = 0;
foreach (['2023-02-29', '2026-04-31', '2026-1-01', "2026-01-01\n", '0000-01-01'] as $s) {
try { strictDate($s); throw new RuntimeException('Accepted invalid date'); }
catch (InvalidArgumentException $e) { $count++; }
}
echo "leap date accepted; rejected=$count\n";
Run
php date.php
Expected output:
leap date accepted; rejected=5
The regular expression requires exactly four year digits and two digits for both month and day. Its absolute end anchor also rejects a trailing newline. The exclamation mark in the parse format resets unspecified fields, making the midnight result independent of the current clock time.
Read parser diagnostics immediately after parsing. On modern PHP, getLastErrors() can return false when no warning or error occurred, so indexing it unconditionally is incorrect. The final round-trip comparison ensures that the returned object's date still has exactly the requested spelling.
Edge cases and troubleshooting
This field deliberately excludes year zero even though PHP's calendar facilities can represent it. That is an application policy, not a limitation of the parser. Add business rules such as an earliest booking date after the syntactic and calendar validation succeeds.
If valid user input is rejected, inspect whether the client sends a localized date, omitted leading zeros or invisible whitespace. Decide whether normalization belongs in a separate interface step rather than quietly expanding the accepted storage format. Avoid returning raw parser internals to end users; a stable field-level message is easier to handle.
These tests cover leap-day acceptance, impossible dates, spelling, trailing data and year-zero policy. They do not test a web form, database column or daylight-saving-time scheduling behavior.
Primary references
Read PHP's documentation for createFromFormat and getLastErrors. Find more examples in the PHP category.