The free-form parser rolls 30 February into March, which is wrong for user input. createFromFormat() parses an exact format, a leading ! resets unspecified fields to the epoch instead of "now", and a round trip through format() rejects overflow. Since PHP 8.3 bad strings throw DateMalformedStringException.
<?php
function parseDate(string $input, string $format = 'Y-m-d'): ?DateTimeImmutable
{
$d = DateTimeImmutable::createFromFormat('!' . $format, $input, new DateTimeZone('UTC'));
return $d !== false && $d->format($format) === $input ? $d : null; // exact round trip
}
echo implode(', ', array_map(fn($in) => "$in: " . (parseDate($in)?->format('D j M') ?? 'no'),
['2026-09-23', '2026-02-30', '23/09/2026', '2026-9-3'])), "\n";
$loose = DateTimeImmutable::createFromFormat('Y-m-d', '2026-02-30'); // no "!": time is now
$errors = DateTimeImmutable::getLastErrors(); // false if none
echo $loose->format('Y-m-d H:i:s'), ' ', json_encode($errors['warnings']), ' ',
var_export(checkdate(2, 29, 2027), true), "\n";
try { new DateTimeImmutable('next blursday'); } // PHP 8.3+ exception
catch (DateMalformedStringException $e) { echo get_class($e), PHP_EOL; }
$a = new DateTimeImmutable('2026-09-23 14:00', new DateTimeZone('Asia/Kuala_Lumpur'));
$b = new DateTimeImmutable('2026-09-23 06:00', new DateTimeZone('UTC'));
echo json_encode([$a == $b, $a < $b->modify('+1 second'), $a <=> $b]), ' ',
max($a->modify('+2 days'), $b, $a->modify('-1 day'))->format('M j'), "\n";2026-09-23: Wed 23 Sep, 2026-02-30: no, 23/09/2026: no, 2026-9-3: no
2026-03-02 07:48:09 {"10":"The parsed date was invalid"} false
DateMalformedStringException
[true,true,0] Sep 25Without !, the loose parse returned 2 March with the time of the run; only a warning in getLastErrors() betrays it. checkdate($month, $day, $year) validates separate form fields. Date objects compare with ==, <, <=> and max() by instant, so 14:00 in Kuala Lumpur equals 06:00 UTC.
For heavier date work, Carbon 16,596 (https://github.com/briannesbitt/Carbon 16,596 ) (composer require nesbot/carbon; 3.14.0 of 12 September 2026, PHP 8.1 or later) extends DateTime and DateTimeImmutable with helpers such as addWeekdays() and diffForHumans(). Laravel 2,157 's dates are Carbon objects (Laravel).