From c1b5815a1badaccdfb88c415ce8fbc82c2b1cd98 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Sun, 22 Mar 2026 03:38:51 +0600 Subject: [PATCH 01/10] Use format for error message dates when unset (#709) --- CHANGELOG.md | 1 + src/Rule/Date/BaseDateHandler.php | 10 +++++++-- tests/Rule/Date/DateTest.php | 34 +++++++++++++++++++++++-------- tests/Rule/Date/DateTimeTest.php | 8 ++++---- tests/Rule/Date/TimeTest.php | 18 ++++++++++------ 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 983283a7c..2dfc26b1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.5.2 under development +- Chg #789: Use `format` property for formatting dates in error messages when message format properties are not set (@WarLikeLaux) - Enh #787: Explicitly import classes, functions, and constants in "use" section (@mspirkov) ## 2.5.1 December 12, 2025 diff --git a/src/Rule/Date/BaseDateHandler.php b/src/Rule/Date/BaseDateHandler.php index 03dc33dea..9804f848c 100644 --- a/src/Rule/Date/BaseDateHandler.php +++ b/src/Rule/Date/BaseDateHandler.php @@ -187,14 +187,20 @@ private function prepareValueWithIntlFormat( private function formatDate(DateTimeInterface $date, Date|DateTime|Time $rule, ?DateTimeZone $timeZone): string { - $formatterDateType = $this->getMessageDateTypeFromRule($rule) + $ruleMessageDateType = $this->getMessageDateTypeFromRule($rule); + $ruleMessageTimeType = $this->getMessageTimeTypeFromRule($rule); + + $formatterDateType = $ruleMessageDateType ?? $this->messageDateType ?? $this->getDateTypeFromRule($rule); - $formatterTimeType = $this->getMessageTimeTypeFromRule($rule) + $formatterTimeType = $ruleMessageTimeType ?? $this->messageTimeType ?? $this->getTimeTypeFromRule($rule); $format = $rule->getMessageFormat() ?? $this->messageFormat; + if ($format === null && $ruleMessageDateType === null && $ruleMessageTimeType === null) { + $format = $rule->getFormat(); + } if (is_string($format) && str_starts_with($format, 'php:')) { return $date->format(substr($format, 4)); } diff --git a/tests/Rule/Date/DateTest.php b/tests/Rule/Date/DateTest.php index 8bb10937e..f4c079958 100644 --- a/tests/Rule/Date/DateTest.php +++ b/tests/Rule/Date/DateTest.php @@ -77,7 +77,7 @@ public static function dataValidationFailed(): array 'min' => [ '2024-03-29', new Date(format: 'yyyy-MM-dd', min: '2025-01-01'), - ['' => ['Value must be no early than 1/1/25.']], + ['' => ['Value must be no early than 2025-01-01.']], ], 'min-custom-message' => [ ['a' => '2024-03-29'], @@ -88,12 +88,12 @@ public static function dataValidationFailed(): array tooEarlyMessage: 'Prop — {property}. Date — {value}. Min — {limit}.', ), ], - ['a' => ['Prop — a. Date — 3/29/24. Min — 1/1/25.']], + ['a' => ['Prop — a. Date — 2024-03-29. Min — 2025-01-01.']], ], 'max' => [ '2024-03-29', new Date(format: 'php:Y-m-d', max: '2024-01-01'), - ['' => ['Value must be no late than 1/1/24.']], + ['' => ['Value must be no late than 2024-01-01.']], ], 'max-custom-message' => [ ['a' => '2024-03-29'], @@ -104,18 +104,18 @@ public static function dataValidationFailed(): array tooLateMessage: 'Prop — {property}. Date — {value}. Max — {limit}.', ), ], - ['a' => ['Prop — a. Date — 3/29/24. Max — 1/1/24.']], + ['a' => ['Prop — a. Date — 2024-03-29. Max — 2024-01-01.']], ], 'rule-and-handler-locales' => [ '2024-03-29', new Date(format: 'php:Y-m-d', locale: 'ru', max: '2024-01-01'), - ['' => ['Value must be no late than 01.01.2024.']], + ['' => ['Value must be no late than 2024-01-01.']], [DateHandler::class => new DateHandler(locale: 'en')], ], 'handler-locale' => [ '2024-03-29', new Date(format: 'php:Y-m-d', max: '2024-01-01'), - ['' => ['Value must be no late than 01.01.2024.']], + ['' => ['Value must be no late than 2024-01-01.']], [DateHandler::class => new DateHandler(locale: 'ru')], ], 'timestamp' => [ @@ -125,8 +125,8 @@ public static function dataValidationFailed(): array ], 'without-message-date-type' => [ '29*03*2024', - new Date(format: 'php:d*m*Y', max: '11*11*2023', ), - ['' => ['Value must be no late than 11/11/23.']], + new Date(format: 'php:d*m*Y', max: '11*11*2023'), + ['' => ['Value must be no late than 11*11*2023.']], [DateHandler::class => new DateHandler(messageDateType: null)], ], 'rule-message-format' => [ @@ -153,6 +153,24 @@ public static function dataValidationFailed(): array ['' => ['Value must be no late than 10.11.2002.']], [DateHandler::class => new DateHandler(locale: 'en')], ], + 'format-used-for-message' => [ + '01.01.2100', + new Date( + format: 'php:d.m.Y', + min: '19.11.2013', + max: '31.12.2099', + ), + ['' => ['Value must be no late than 31.12.2099.']], + ], + 'format-overridden-by-message-format' => [ + '01.01.2100', + new Date( + format: 'php:d.m.Y', + max: '31.12.2099', + messageFormat: 'php:Y/m/d', + ), + ['' => ['Value must be no late than 2099/12/31.']], + ], ]; } diff --git a/tests/Rule/Date/DateTimeTest.php b/tests/Rule/Date/DateTimeTest.php index cd3883d4d..dea9fc270 100644 --- a/tests/Rule/Date/DateTimeTest.php +++ b/tests/Rule/Date/DateTimeTest.php @@ -74,22 +74,22 @@ public static function dataValidationFailed(): array 'min' => [ '2024-03-29, 12:35', new DateTime(format: 'yyyy-MM-dd, HH:mm', min: '2025-01-01, 11:00'), - ['' => ['Value must be no early than 1/1/25, 11:00 AM.']], + ['' => ['Value must be no early than 2025-01-01, 11:00.']], ], 'max' => [ '2024-03-29, 12:50', new DateTime(format: 'php:Y-m-d, H:i', max: '2024-01-01, 00:00'), - ['' => ['Value must be no late than 1/1/24, 12:00 AM.']], + ['' => ['Value must be no late than 2024-01-01, 00:00.']], ], 'timestamp' => [ 1711705158, new DateTime(format: 'php:d.m.Y, H:i:s', min: 1711705200), - ['' => ['Value must be no early than 3/29/24, 9:40 AM.']], + ['' => ['Value must be no early than 29.03.2024, 09:40:00.']], ], 'without-message-date-and-time-type' => [ '29*03*2024*12*35', new DateTime(format: 'php:d*m*Y*H*i', max: '11*11*2023*12*35'), - ['' => ['Value must be no late than 11/11/23, 12:35 PM.']], + ['' => ['Value must be no late than 11*11*2023*12*35.']], [DateTimeHandler::class => new DateTimeHandler(messageDateType: null, messageTimeType: null)], ], ]; diff --git a/tests/Rule/Date/TimeTest.php b/tests/Rule/Date/TimeTest.php index 56f6a858b..2435e5e41 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -57,22 +57,22 @@ public static function dataValidationFailed(): array 'min' => [ '15:30', new Time(format: 'HH:mm', min: '15:40'), - ['' => ['Value must be no early than 3:40 PM.']], + ['' => ['Value must be no early than 15:40.']], ], 'max' => [ '15:30', new Time(format: 'php:H:i', max: '12:00'), - ['' => ['Value must be no late than 12:00 PM.']], + ['' => ['Value must be no late than 12:00.']], ], 'timestamp' => [ 1711705158, new Time(format: 'php:d.m.Y, H:i:s', min: 1711705200), - ['' => ['Value must be no early than 9:40 AM.']], + ['' => ['Value must be no early than 29.03.2024, 09:40:00.']], ], 'without-message-time-type' => [ '13*30', new Time(format: 'php:H*i', max: '11*30'), - ['' => ['Value must be no late than 11:30 AM.']], + ['' => ['Value must be no late than 11*30.']], [TimeHandler::class => new TimeHandler(messageTimeType: null)], ], 'rule-message-format' => [ @@ -82,11 +82,17 @@ public static function dataValidationFailed(): array [TimeHandler::class => new TimeHandler(messageFormat: 'php:H_i')], ], 'handler-message-type' => [ - '13*30', - new Time(format: 'php:H*i', max: '11*30', timeType: IntlDateFormatter::SHORT), + "1:30\u{202F}PM", + new Time(max: "11:30\u{202F}AM", locale: 'en_US'), ['' => ['Value must be no late than 11:30:00 AM Coordinated Universal Time.']], [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::FULL)], ], + 'format-overrides-handler-message-type' => [ + '13*30', + new Time(format: 'php:H*i', max: '11*30'), + ['' => ['Value must be no late than 11*30.']], + [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::FULL)], + ], 'rule-message-type-override-handler' => [ '13*30', new Time(format: 'php:H*i', max: '11*30', messageTimeType: IntlDateFormatter::SHORT), From fe2f208d7e295566eb07bb8095b7529ef4b6fc2a Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 5 Apr 2026 11:57:12 +0300 Subject: [PATCH 02/10] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/Rule/Date/TimeTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Rule/Date/TimeTest.php b/tests/Rule/Date/TimeTest.php index 2435e5e41..a77c02fef 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -82,8 +82,8 @@ public static function dataValidationFailed(): array [TimeHandler::class => new TimeHandler(messageFormat: 'php:H_i')], ], 'handler-message-type' => [ - "1:30\u{202F}PM", - new Time(max: "11:30\u{202F}AM", locale: 'en_US'), + '1:30 PM', + new Time(max: '11:30 AM', locale: 'en_US'), ['' => ['Value must be no late than 11:30:00 AM Coordinated Universal Time.']], [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::FULL)], ], From 9821a46638318d87e58e72f11a4ba029adc8b846 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 5 Apr 2026 11:57:46 +0300 Subject: [PATCH 03/10] Apply suggestion from @samdark --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dfc26b1b..f2d7cc6b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.5.2 under development -- Chg #789: Use `format` property for formatting dates in error messages when message format properties are not set (@WarLikeLaux) +- Chg #709: Use `format` property for formatting dates in error messages when message format properties are not set (@WarLikeLaux) - Enh #787: Explicitly import classes, functions, and constants in "use" section (@mspirkov) ## 2.5.1 December 12, 2025 From 8b5fc828e0a7c2e450e2d56d9539456909880c02 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Sat, 30 May 2026 18:17:36 +0600 Subject: [PATCH 04/10] Fix `TimeTest` strict time parsing on ICU >= 72 --- tests/Rule/Date/TimeTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Rule/Date/TimeTest.php b/tests/Rule/Date/TimeTest.php index 894a1a871..4e013599f 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -82,8 +82,8 @@ public static function dataValidationFailed(): array [TimeHandler::class => new TimeHandler(messageFormat: 'php:H_i')], ], 'handler-message-type' => [ - '1:30 PM', - new Time(max: '11:30 AM', locale: 'en_US'), + "1:30\u{202F}PM", + new Time(max: "11:30\u{202F}AM", locale: 'en_US'), ['' => ['Value must be no later than 11:30:00 AM Coordinated Universal Time.']], [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::FULL)], ], From 00addacc9ac6f51f2bac88eea324e99e2a117d76 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Sat, 30 May 2026 18:24:39 +0600 Subject: [PATCH 05/10] Use timestamps in `TimeTest` to avoid ICU-dependent time parsing --- tests/Rule/Date/TimeTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Rule/Date/TimeTest.php b/tests/Rule/Date/TimeTest.php index 4e013599f..149d0f662 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -82,8 +82,8 @@ public static function dataValidationFailed(): array [TimeHandler::class => new TimeHandler(messageFormat: 'php:H_i')], ], 'handler-message-type' => [ - "1:30\u{202F}PM", - new Time(max: "11:30\u{202F}AM", locale: 'en_US'), + 1711719000, + new Time(max: 1711711800, locale: 'en_US'), ['' => ['Value must be no later than 11:30:00 AM Coordinated Universal Time.']], [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::FULL)], ], From 2953efbb14368b3edc6ce6d72b5ef5e7de597f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9?= <48706973+WarLikeLaux@users.noreply.github.com> Date: Sun, 31 May 2026 10:20:34 +0600 Subject: [PATCH 06/10] Update CHANGELOG.md Co-authored-by: Sergei Predvoditelev --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3720af75d..2511ffbd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.5.2 under development -- Chg #709: Use `format` property for formatting dates in error messages when message format properties are not set (@WarLikeLaux) +- Enh #709: In date rules use `format` property for formatting dates in error messages when message format properties are not set (@WarLikeLaux) - Enh #787: Explicitly import classes, functions, and constants in "use" section (@mspirkov) - Bug #793: Fix translations, broken link in contributing guide, incorrect imports and grammar in documentation (@evilkarter) - Chg #795: Update Polish translations (@rbrzezinski) From 6c2b672c404265df80f82d805b806af6b9199195 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Mon, 1 Jun 2026 18:39:20 +0600 Subject: [PATCH 07/10] Respect handler-level message types over rule `format` --- src/Rule/Date/BaseDateHandler.php | 8 +++++++- src/Rule/Date/DateHandler.php | 4 ++-- src/Rule/Date/DateTimeHandler.php | 4 ++-- src/Rule/Date/TimeHandler.php | 4 ++-- tests/Rule/Date/DateTest.php | 6 ++++++ tests/Rule/Date/TimeTest.php | 4 ++-- 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Rule/Date/BaseDateHandler.php b/src/Rule/Date/BaseDateHandler.php index 9804f848c..b41bf0b30 100644 --- a/src/Rule/Date/BaseDateHandler.php +++ b/src/Rule/Date/BaseDateHandler.php @@ -198,7 +198,13 @@ private function formatDate(DateTimeInterface $date, Date|DateTime|Time $rule, ? ?? $this->getTimeTypeFromRule($rule); $format = $rule->getMessageFormat() ?? $this->messageFormat; - if ($format === null && $ruleMessageDateType === null && $ruleMessageTimeType === null) { + if ( + $format === null + && $ruleMessageDateType === null + && $ruleMessageTimeType === null + && $this->messageDateType === null + && $this->messageTimeType === null + ) { $format = $rule->getFormat(); } if (is_string($format) && str_starts_with($format, 'php:')) { diff --git a/src/Rule/Date/DateHandler.php b/src/Rule/Date/DateHandler.php index e2d64d706..935f67f73 100644 --- a/src/Rule/Date/DateHandler.php +++ b/src/Rule/Date/DateHandler.php @@ -20,7 +20,7 @@ public function __construct( ?string $timeZone = null, ?string $locale = null, ?string $messageFormat = null, - ?int $messageDateType = IntlDateFormatter::SHORT, + ?int $messageDateType = null, string $incorrectInputMessage = '{Property} must be a date.', string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', string $tooLateMessage = '{Property} must be no later than {limit}.', @@ -32,7 +32,7 @@ public function __construct( $locale, $messageFormat, $messageDateType, - IntlDateFormatter::NONE, + null, $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, diff --git a/src/Rule/Date/DateTimeHandler.php b/src/Rule/Date/DateTimeHandler.php index da73ad403..39409e38c 100644 --- a/src/Rule/Date/DateTimeHandler.php +++ b/src/Rule/Date/DateTimeHandler.php @@ -22,8 +22,8 @@ public function __construct( ?string $timeZone = null, ?string $locale = null, ?string $messageFormat = null, - ?int $messageDateType = IntlDateFormatter::SHORT, - ?int $messageTimeType = IntlDateFormatter::SHORT, + ?int $messageDateType = null, + ?int $messageTimeType = null, string $incorrectInputMessage = '{Property} must be a date.', string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', string $tooLateMessage = '{Property} must be no later than {limit}.', diff --git a/src/Rule/Date/TimeHandler.php b/src/Rule/Date/TimeHandler.php index be4ab377a..e1614216d 100644 --- a/src/Rule/Date/TimeHandler.php +++ b/src/Rule/Date/TimeHandler.php @@ -20,7 +20,7 @@ public function __construct( ?string $timeZone = null, ?string $locale = null, ?string $messageFormat = null, - ?int $messageTimeType = IntlDateFormatter::SHORT, + ?int $messageTimeType = null, string $incorrectInputMessage = '{Property} must be a time.', string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', string $tooLateMessage = '{Property} must be no later than {limit}.', @@ -31,7 +31,7 @@ public function __construct( $timeZone, $locale, $messageFormat, - IntlDateFormatter::NONE, + null, $messageTimeType, $incorrectInputMessage, $tooEarlyMessage, diff --git a/tests/Rule/Date/DateTest.php b/tests/Rule/Date/DateTest.php index 309d26127..d0bcb9ed2 100644 --- a/tests/Rule/Date/DateTest.php +++ b/tests/Rule/Date/DateTest.php @@ -153,6 +153,12 @@ public static function dataValidationFailed(): array ['' => ['Value must be no later than 10.11.2002.']], [DateHandler::class => new DateHandler(locale: 'en')], ], + 'handler-message-type-overrides-format' => [ + '29*03*2024', + new Date(format: 'php:d*m*Y', max: '11*11*2023'), + ['' => ['Value must be no later than Saturday, November 11, 2023.']], + [DateHandler::class => new DateHandler(messageDateType: IntlDateFormatter::FULL)], + ], 'format-used-for-message' => [ '01.01.2100', new Date( diff --git a/tests/Rule/Date/TimeTest.php b/tests/Rule/Date/TimeTest.php index 149d0f662..c72b891aa 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -87,10 +87,10 @@ public static function dataValidationFailed(): array ['' => ['Value must be no later than 11:30:00 AM Coordinated Universal Time.']], [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::FULL)], ], - 'format-overrides-handler-message-type' => [ + 'handler-message-type-overrides-format' => [ '13*30', new Time(format: 'php:H*i', max: '11*30'), - ['' => ['Value must be no later than 11*30.']], + ['' => ['Value must be no later than 11:30:00 AM Coordinated Universal Time.']], [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::FULL)], ], 'rule-message-type-override-handler' => [ From 244ae122da6967a6e91192fa52dbe253b7ab00fd Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Mon, 1 Jun 2026 19:12:15 +0600 Subject: [PATCH 08/10] Keep handler message type defaults via `func_num_args` for BC --- src/Rule/Date/BaseDateHandler.php | 4 ++-- src/Rule/Date/DateHandler.php | 10 +++++++++- src/Rule/Date/DateTimeHandler.php | 15 +++++++++++++-- src/Rule/Date/TimeHandler.php | 10 +++++++++- tests/Rule/Date/DateTest.php | 12 ++++++++++++ tests/Rule/Date/DateTimeTest.php | 6 ++++++ tests/Rule/Date/TimeTest.php | 6 ++++++ 7 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/Rule/Date/BaseDateHandler.php b/src/Rule/Date/BaseDateHandler.php index b41bf0b30..28c529c5c 100644 --- a/src/Rule/Date/BaseDateHandler.php +++ b/src/Rule/Date/BaseDateHandler.php @@ -192,10 +192,10 @@ private function formatDate(DateTimeInterface $date, Date|DateTime|Time $rule, ? $formatterDateType = $ruleMessageDateType ?? $this->messageDateType - ?? $this->getDateTypeFromRule($rule); + ?? ($rule instanceof Time ? IntlDateFormatter::NONE : IntlDateFormatter::SHORT); $formatterTimeType = $ruleMessageTimeType ?? $this->messageTimeType - ?? $this->getTimeTypeFromRule($rule); + ?? ($rule instanceof Date ? IntlDateFormatter::NONE : IntlDateFormatter::SHORT); $format = $rule->getMessageFormat() ?? $this->messageFormat; if ( diff --git a/src/Rule/Date/DateHandler.php b/src/Rule/Date/DateHandler.php index 935f67f73..57950f935 100644 --- a/src/Rule/Date/DateHandler.php +++ b/src/Rule/Date/DateHandler.php @@ -6,6 +6,8 @@ use IntlDateFormatter; +use function func_num_args; + /** * @psalm-import-type IntlDateFormatterFormat from BaseDate */ @@ -20,11 +22,17 @@ public function __construct( ?string $timeZone = null, ?string $locale = null, ?string $messageFormat = null, - ?int $messageDateType = null, + ?int $messageDateType = IntlDateFormatter::SHORT, string $incorrectInputMessage = '{Property} must be a date.', string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', string $tooLateMessage = '{Property} must be no later than {limit}.', ) { + $argumentCount = func_num_args(); + // Keep the public default value for BC, but treat it as unset when omitted. + if ($messageDateType === IntlDateFormatter::SHORT && $argumentCount !== 5) { + $messageDateType = null; + } + parent::__construct( $dateType, IntlDateFormatter::NONE, diff --git a/src/Rule/Date/DateTimeHandler.php b/src/Rule/Date/DateTimeHandler.php index 39409e38c..42b70f028 100644 --- a/src/Rule/Date/DateTimeHandler.php +++ b/src/Rule/Date/DateTimeHandler.php @@ -6,6 +6,8 @@ use IntlDateFormatter; +use function func_num_args; + /** * @psalm-import-type IntlDateFormatterFormat from BaseDate */ @@ -22,12 +24,21 @@ public function __construct( ?string $timeZone = null, ?string $locale = null, ?string $messageFormat = null, - ?int $messageDateType = null, - ?int $messageTimeType = null, + ?int $messageDateType = IntlDateFormatter::SHORT, + ?int $messageTimeType = IntlDateFormatter::SHORT, string $incorrectInputMessage = '{Property} must be a date.', string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', string $tooLateMessage = '{Property} must be no later than {limit}.', ) { + $argumentCount = func_num_args(); + // Keep the public default values for BC, but treat them as unset when omitted. + if ($messageDateType === IntlDateFormatter::SHORT && $argumentCount !== 6) { + $messageDateType = null; + } + if ($messageTimeType === IntlDateFormatter::SHORT && $argumentCount !== 7) { + $messageTimeType = null; + } + parent::__construct( $dateType, $timeType, diff --git a/src/Rule/Date/TimeHandler.php b/src/Rule/Date/TimeHandler.php index e1614216d..14e0116fb 100644 --- a/src/Rule/Date/TimeHandler.php +++ b/src/Rule/Date/TimeHandler.php @@ -6,6 +6,8 @@ use IntlDateFormatter; +use function func_num_args; + /** * @psalm-import-type IntlDateFormatterFormat from BaseDate */ @@ -20,11 +22,17 @@ public function __construct( ?string $timeZone = null, ?string $locale = null, ?string $messageFormat = null, - ?int $messageTimeType = null, + ?int $messageTimeType = IntlDateFormatter::SHORT, string $incorrectInputMessage = '{Property} must be a time.', string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', string $tooLateMessage = '{Property} must be no later than {limit}.', ) { + $argumentCount = func_num_args(); + // Keep the public default value for BC, but treat it as unset when omitted. + if ($messageTimeType === IntlDateFormatter::SHORT && $argumentCount !== 5) { + $messageTimeType = null; + } + parent::__construct( IntlDateFormatter::NONE, $timeType, diff --git a/tests/Rule/Date/DateTest.php b/tests/Rule/Date/DateTest.php index d0bcb9ed2..eb2b21427 100644 --- a/tests/Rule/Date/DateTest.php +++ b/tests/Rule/Date/DateTest.php @@ -118,6 +118,12 @@ public static function dataValidationFailed(): array ['' => ['Value must be no later than 2024-01-01.']], [DateHandler::class => new DateHandler(locale: 'ru')], ], + 'handler-custom-message' => [ + '2024-03-29', + new Date(format: 'php:Y-m-d', max: '2024-01-01'), + ['' => ['Max: 2024-01-01.']], + [DateHandler::class => new DateHandler(tooLateMessage: 'Max: {limit}.')], + ], 'timestamp' => [ 1711705158, new Date(min: 1711705200), @@ -159,6 +165,12 @@ public static function dataValidationFailed(): array ['' => ['Value must be no later than Saturday, November 11, 2023.']], [DateHandler::class => new DateHandler(messageDateType: IntlDateFormatter::FULL)], ], + 'handler-date-type-does-not-affect-message' => [ + 'March 29, 2024', + new Date(max: 'January 1, 2024'), + ['' => ['Value must be no later than 1/1/24.']], + [DateHandler::class => new DateHandler(dateType: IntlDateFormatter::LONG)], + ], 'format-used-for-message' => [ '01.01.2100', new Date( diff --git a/tests/Rule/Date/DateTimeTest.php b/tests/Rule/Date/DateTimeTest.php index 73d0c1a7d..0119fc2f0 100644 --- a/tests/Rule/Date/DateTimeTest.php +++ b/tests/Rule/Date/DateTimeTest.php @@ -81,6 +81,12 @@ public static function dataValidationFailed(): array new DateTime(format: 'php:Y-m-d, H:i', max: '2024-01-01, 00:00'), ['' => ['Value must be no later than 2024-01-01, 00:00.']], ], + 'handler-custom-message' => [ + '2024-03-29, 12:50', + new DateTime(format: 'php:Y-m-d, H:i', max: '2024-01-01, 00:00'), + ['' => ['Max: 2024-01-01, 00:00.']], + [DateTimeHandler::class => new DateTimeHandler(tooLateMessage: 'Max: {limit}.')], + ], 'timestamp' => [ 1711705158, new DateTime(format: 'php:d.m.Y, H:i:s', min: 1711705200), diff --git a/tests/Rule/Date/TimeTest.php b/tests/Rule/Date/TimeTest.php index c72b891aa..772a97cb2 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -64,6 +64,12 @@ public static function dataValidationFailed(): array new Time(format: 'php:H:i', max: '12:00'), ['' => ['Value must be no later than 12:00.']], ], + 'handler-custom-message' => [ + '15:30', + new Time(format: 'php:H:i', max: '12:00'), + ['' => ['Max: 12:00.']], + [TimeHandler::class => new TimeHandler(tooLateMessage: 'Max: {limit}.')], + ], 'timestamp' => [ 1711705158, new Time(format: 'php:d.m.Y, H:i:s', min: 1711705200), From 9c4ec43de9659e12922ba2911ff5dfaed62be9b2 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Mon, 1 Jun 2026 19:29:26 +0600 Subject: [PATCH 09/10] Distinguish unset and explicit-null message types in date handlers --- src/Rule/Date/BaseDateHandler.php | 34 ++++++++- src/Rule/Date/DateHandler.php | 4 ++ src/Rule/Date/DateTimeHandler.php | 7 ++ src/Rule/Date/TimeHandler.php | 5 ++ tests/Rule/Date/DateTest.php | 18 +++++ tests/Rule/Date/DateTimeTest.php | 112 ++++++++++++++++++++++++++++++ tests/Rule/Date/TimeTest.php | 39 +++++++++++ 7 files changed, 217 insertions(+), 2 deletions(-) diff --git a/src/Rule/Date/BaseDateHandler.php b/src/Rule/Date/BaseDateHandler.php index 28c529c5c..21cdfd691 100644 --- a/src/Rule/Date/BaseDateHandler.php +++ b/src/Rule/Date/BaseDateHandler.php @@ -43,6 +43,8 @@ public function __construct( private readonly string $incorrectInputMessage, private readonly string $tooEarlyMessage, private readonly string $tooLateMessage, + private readonly bool $messageDateTypeFallbackToRuleType = true, + private readonly bool $messageTimeTypeFallbackToRuleType = true, ) {} public function validate(mixed $value, RuleInterface $rule, ValidationContext $context): Result @@ -192,10 +194,10 @@ private function formatDate(DateTimeInterface $date, Date|DateTime|Time $rule, ? $formatterDateType = $ruleMessageDateType ?? $this->messageDateType - ?? ($rule instanceof Time ? IntlDateFormatter::NONE : IntlDateFormatter::SHORT); + ?? $this->getMessageDateTypeFallback($rule); $formatterTimeType = $ruleMessageTimeType ?? $this->messageTimeType - ?? ($rule instanceof Date ? IntlDateFormatter::NONE : IntlDateFormatter::SHORT); + ?? $this->getMessageTimeTypeFallback($rule); $format = $rule->getMessageFormat() ?? $this->messageFormat; if ( @@ -247,6 +249,34 @@ private function makeDateTimeFromTimestamp(int $timestamp, ?DateTimeZone $timeZo return (new DateTimeImmutable(timezone: $timeZone))->setTimestamp($timestamp); } + /** + * @psalm-return IntlDateFormatterFormat + */ + private function getMessageDateTypeFallback(Date|DateTime|Time $rule): int + { + if ($rule instanceof Time) { + return IntlDateFormatter::NONE; + } + + return $this->messageDateTypeFallbackToRuleType + ? $this->getDateTypeFromRule($rule) + : IntlDateFormatter::SHORT; + } + + /** + * @psalm-return IntlDateFormatterFormat + */ + private function getMessageTimeTypeFallback(Date|DateTime|Time $rule): int + { + if ($rule instanceof Date) { + return IntlDateFormatter::NONE; + } + + return $this->messageTimeTypeFallbackToRuleType + ? $this->getTimeTypeFromRule($rule) + : IntlDateFormatter::SHORT; + } + /** * @psalm-return IntlDateFormatterFormat */ diff --git a/src/Rule/Date/DateHandler.php b/src/Rule/Date/DateHandler.php index 57950f935..2360d4b3f 100644 --- a/src/Rule/Date/DateHandler.php +++ b/src/Rule/Date/DateHandler.php @@ -28,9 +28,12 @@ public function __construct( string $tooLateMessage = '{Property} must be no later than {limit}.', ) { $argumentCount = func_num_args(); + $messageDateTypeFallbackToRuleType = $messageDateType === null; + // Keep the public default value for BC, but treat it as unset when omitted. if ($messageDateType === IntlDateFormatter::SHORT && $argumentCount !== 5) { $messageDateType = null; + $messageDateTypeFallbackToRuleType = false; } parent::__construct( @@ -44,6 +47,7 @@ public function __construct( $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, + $messageDateTypeFallbackToRuleType, ); } } diff --git a/src/Rule/Date/DateTimeHandler.php b/src/Rule/Date/DateTimeHandler.php index 42b70f028..b19315423 100644 --- a/src/Rule/Date/DateTimeHandler.php +++ b/src/Rule/Date/DateTimeHandler.php @@ -31,12 +31,17 @@ public function __construct( string $tooLateMessage = '{Property} must be no later than {limit}.', ) { $argumentCount = func_num_args(); + $messageDateTypeFallbackToRuleType = $messageDateType === null; + $messageTimeTypeFallbackToRuleType = $messageTimeType === null; + // Keep the public default values for BC, but treat them as unset when omitted. if ($messageDateType === IntlDateFormatter::SHORT && $argumentCount !== 6) { $messageDateType = null; + $messageDateTypeFallbackToRuleType = false; } if ($messageTimeType === IntlDateFormatter::SHORT && $argumentCount !== 7) { $messageTimeType = null; + $messageTimeTypeFallbackToRuleType = false; } parent::__construct( @@ -50,6 +55,8 @@ public function __construct( $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, + $messageDateTypeFallbackToRuleType, + $messageTimeTypeFallbackToRuleType, ); } } diff --git a/src/Rule/Date/TimeHandler.php b/src/Rule/Date/TimeHandler.php index 14e0116fb..4d880e11f 100644 --- a/src/Rule/Date/TimeHandler.php +++ b/src/Rule/Date/TimeHandler.php @@ -28,9 +28,12 @@ public function __construct( string $tooLateMessage = '{Property} must be no later than {limit}.', ) { $argumentCount = func_num_args(); + $messageTimeTypeFallbackToRuleType = $messageTimeType === null; + // Keep the public default value for BC, but treat it as unset when omitted. if ($messageTimeType === IntlDateFormatter::SHORT && $argumentCount !== 5) { $messageTimeType = null; + $messageTimeTypeFallbackToRuleType = false; } parent::__construct( @@ -44,6 +47,8 @@ public function __construct( $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, + true, + $messageTimeTypeFallbackToRuleType, ); } } diff --git a/tests/Rule/Date/DateTest.php b/tests/Rule/Date/DateTest.php index eb2b21427..b96cc356b 100644 --- a/tests/Rule/Date/DateTest.php +++ b/tests/Rule/Date/DateTest.php @@ -124,6 +124,12 @@ public static function dataValidationFailed(): array ['' => ['Max: 2024-01-01.']], [DateHandler::class => new DateHandler(tooLateMessage: 'Max: {limit}.')], ], + 'handler-message-date-type-null-with-handler-custom-message' => [ + 'March 29, 2024', + new Date(dateType: IntlDateFormatter::LONG, max: 'January 1, 2024'), + ['' => ['Max: January 1, 2024.']], + [DateHandler::class => new DateHandler(messageDateType: null, tooLateMessage: 'Max: {limit}.')], + ], 'timestamp' => [ 1711705158, new Date(min: 1711705200), @@ -171,6 +177,18 @@ public static function dataValidationFailed(): array ['' => ['Value must be no later than 1/1/24.']], [DateHandler::class => new DateHandler(dateType: IntlDateFormatter::LONG)], ], + 'handler-message-date-type-null-falls-back-to-rule-date-type' => [ + 'March 29, 2024', + new Date(dateType: IntlDateFormatter::LONG, max: 'January 1, 2024'), + ['' => ['Value must be no later than January 1, 2024.']], + [DateHandler::class => new DateHandler(messageDateType: null)], + ], + 'handler-message-date-type-short-overrides-format' => [ + '2024-03-29', + new Date(format: 'php:Y-m-d', max: '2024-01-01'), + ['' => ['Value must be no later than 1/1/24.']], + [DateHandler::class => new DateHandler(messageDateType: IntlDateFormatter::SHORT)], + ], 'format-used-for-message' => [ '01.01.2100', new Date( diff --git a/tests/Rule/Date/DateTimeTest.php b/tests/Rule/Date/DateTimeTest.php index 0119fc2f0..be9e91a2d 100644 --- a/tests/Rule/Date/DateTimeTest.php +++ b/tests/Rule/Date/DateTimeTest.php @@ -6,6 +6,7 @@ use DateTimeImmutable; use DateTimeZone; +use IntlDateFormatter; use Yiisoft\Validator\Rule\Date\DateTime; use Yiisoft\Validator\Rule\Date\Date; use Yiisoft\Validator\Rule\Date\DateTimeHandler; @@ -87,6 +88,24 @@ public static function dataValidationFailed(): array ['' => ['Max: 2024-01-01, 00:00.']], [DateTimeHandler::class => new DateTimeHandler(tooLateMessage: 'Max: {limit}.')], ], + 'handler-message-date-and-time-types-null-with-handler-custom-message' => [ + 1711719000, + new DateTime( + dateType: IntlDateFormatter::LONG, + timeType: IntlDateFormatter::SHORT, + max: 1711711800, + timeZone: 'UTC', + locale: 'en_US', + ), + ['' => ['Max: March 29, 2024 at 11:30 AM.']], + [ + DateTimeHandler::class => new DateTimeHandler( + messageDateType: null, + messageTimeType: null, + tooLateMessage: 'Max: {limit}.', + ), + ], + ], 'timestamp' => [ 1711705158, new DateTime(format: 'php:d.m.Y, H:i:s', min: 1711705200), @@ -98,6 +117,99 @@ public static function dataValidationFailed(): array ['' => ['Value must be no later than 11*11*2023*12*35.']], [DateTimeHandler::class => new DateTimeHandler(messageDateType: null, messageTimeType: null)], ], + 'handler-date-and-time-types-do-not-affect-message' => [ + 1711719000, + new DateTime( + dateType: IntlDateFormatter::LONG, + timeType: IntlDateFormatter::SHORT, + max: 1711711800, + timeZone: 'UTC', + locale: 'en_US', + ), + ['' => ['Value must be no later than 3/29/24, 11:30 AM.']], + [ + DateTimeHandler::class => new DateTimeHandler( + dateType: IntlDateFormatter::LONG, + timeType: IntlDateFormatter::FULL, + ), + ], + ], + 'handler-message-date-and-time-types-null-falls-back-to-rule-types' => [ + 1711719000, + new DateTime( + dateType: IntlDateFormatter::LONG, + timeType: IntlDateFormatter::SHORT, + max: 1711711800, + timeZone: 'UTC', + locale: 'en_US', + ), + ['' => ['Value must be no later than March 29, 2024 at 11:30 AM.']], + [DateTimeHandler::class => new DateTimeHandler(messageDateType: null, messageTimeType: null)], + ], + 'handler-message-date-type-null-falls-back-to-rule-date-type' => [ + 1711719000, + new DateTime( + dateType: IntlDateFormatter::LONG, + timeType: IntlDateFormatter::SHORT, + max: 1711711800, + timeZone: 'UTC', + locale: 'en_US', + ), + ['' => ['Value must be no later than March 29, 2024 at 11:30:00 AM Coordinated Universal Time.']], + [ + DateTimeHandler::class => new DateTimeHandler( + messageDateType: null, + messageTimeType: IntlDateFormatter::FULL, + ), + ], + ], + 'handler-message-time-type-null-falls-back-to-rule-time-type' => [ + 1711719000, + new DateTime( + dateType: IntlDateFormatter::LONG, + timeType: IntlDateFormatter::FULL, + max: 1711711800, + timeZone: 'UTC', + locale: 'en_US', + ), + [ + '' => [ + 'Value must be no later than Friday, March 29, 2024 at 11:30:00 AM Coordinated Universal Time.', + ], + ], + [ + DateTimeHandler::class => new DateTimeHandler( + messageDateType: IntlDateFormatter::FULL, + messageTimeType: null, + ), + ], + ], + 'handler-message-date-and-time-types-short-override-format' => [ + '2024*03*29*13*30', + new DateTime(format: 'php:Y*m*d*H*i', max: '2024*01*01*00*00', locale: 'en_US'), + ['' => ['Value must be no later than 1/1/24, 12:00 AM.']], + [ + DateTimeHandler::class => new DateTimeHandler( + messageDateType: IntlDateFormatter::SHORT, + messageTimeType: IntlDateFormatter::SHORT, + ), + ], + ], + 'handler-message-date-and-time-types-full-override-format' => [ + '2024*03*29*13*30', + new DateTime(format: 'php:Y*m*d*H*i', max: '2024*01*01*00*00', locale: 'en_US'), + [ + '' => [ + 'Value must be no later than Monday, January 1, 2024 at 12:00:00 AM Coordinated Universal Time.', + ], + ], + [ + DateTimeHandler::class => new DateTimeHandler( + messageDateType: IntlDateFormatter::FULL, + messageTimeType: IntlDateFormatter::FULL, + ), + ], + ], ]; } diff --git a/tests/Rule/Date/TimeTest.php b/tests/Rule/Date/TimeTest.php index 772a97cb2..c56e9c44e 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -70,6 +70,17 @@ public static function dataValidationFailed(): array ['' => ['Max: 12:00.']], [TimeHandler::class => new TimeHandler(tooLateMessage: 'Max: {limit}.')], ], + 'handler-message-time-type-null-with-handler-custom-message' => [ + 1711719000, + new Time( + timeType: IntlDateFormatter::FULL, + max: 1711711800, + timeZone: 'UTC', + locale: 'en_US', + ), + ['' => ['Max: 11:30:00 AM Coordinated Universal Time.']], + [TimeHandler::class => new TimeHandler(messageTimeType: null, tooLateMessage: 'Max: {limit}.')], + ], 'timestamp' => [ 1711705158, new Time(format: 'php:d.m.Y, H:i:s', min: 1711705200), @@ -99,6 +110,34 @@ public static function dataValidationFailed(): array ['' => ['Value must be no later than 11:30:00 AM Coordinated Universal Time.']], [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::FULL)], ], + 'handler-time-type-does-not-affect-message' => [ + 1711719000, + new Time( + timeType: IntlDateFormatter::FULL, + max: 1711711800, + timeZone: 'UTC', + locale: 'en_US', + ), + ['' => ['Value must be no later than 11:30 AM.']], + [TimeHandler::class => new TimeHandler(timeType: IntlDateFormatter::FULL)], + ], + 'handler-message-time-type-null-falls-back-to-rule-time-type' => [ + 1711719000, + new Time( + timeType: IntlDateFormatter::FULL, + max: 1711711800, + timeZone: 'UTC', + locale: 'en_US', + ), + ['' => ['Value must be no later than 11:30:00 AM Coordinated Universal Time.']], + [TimeHandler::class => new TimeHandler(messageTimeType: null)], + ], + 'handler-message-time-type-short-overrides-format' => [ + '15*30', + new Time(format: 'php:H*i', max: '12*00'), + ['' => ['Value must be no later than 12:00 PM.']], + [TimeHandler::class => new TimeHandler(messageTimeType: IntlDateFormatter::SHORT)], + ], 'rule-message-type-override-handler' => [ '13*30', new Time(format: 'php:H*i', max: '11*30', messageTimeType: IntlDateFormatter::SHORT), From 1fedb6bec3383f4c621510b9acba09f759c9687b Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Mon, 1 Jun 2026 19:45:07 +0600 Subject: [PATCH 10/10] Cover `DateTimeHandler` argument-count branches in tests --- tests/Rule/Date/DateTimeTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Rule/Date/DateTimeTest.php b/tests/Rule/Date/DateTimeTest.php index be9e91a2d..470019d0a 100644 --- a/tests/Rule/Date/DateTimeTest.php +++ b/tests/Rule/Date/DateTimeTest.php @@ -210,6 +210,23 @@ public static function dataValidationFailed(): array ), ], ], + 'handler-message-date-type-only-short-overrides-format' => [ + '29*03*2024, 12:50', + new DateTime(format: 'php:d*m*Y, H:i', max: '11*11*2023, 12:35'), + ['' => ['Value must be no later than 11/11/23, 12:35 PM.']], + [DateTimeHandler::class => new DateTimeHandler(messageDateType: IntlDateFormatter::SHORT)], + ], + 'handler-unset-message-time-type-stays-short' => [ + 1704114000, + new DateTime(max: 1704106800), + ['' => ['Value must be no later than Monday, January 1, 2024 at 11:00 AM.']], + [ + DateTimeHandler::class => new DateTimeHandler( + timeType: IntlDateFormatter::LONG, + messageDateType: IntlDateFormatter::FULL, + ), + ], + ], ]; }