diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d91eca83..210a1df1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.6.1 under development -- no changes in this release. +- Enh #709: In date rules use `format` property for formatting dates in error messages when message format properties are not set (@WarLikeLaux) ## 2.6.0 June 02, 2026 diff --git a/src/Rule/Date/BaseDateHandler.php b/src/Rule/Date/BaseDateHandler.php index 03dc33dea..79fd56d94 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 ?int $defaultMessageDateType = null, + private readonly ?int $defaultMessageTimeType = null, ) {} public function validate(mixed $value, RuleInterface $rule, ValidationContext $context): Result @@ -187,14 +189,28 @@ 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) + ?? $this->defaultMessageDateType + ?? ($rule instanceof Time ? IntlDateFormatter::NONE : IntlDateFormatter::SHORT); + $formatterTimeType = $ruleMessageTimeType ?? $this->messageTimeType - ?? $this->getTimeTypeFromRule($rule); + ?? $this->defaultMessageTimeType + ?? ($rule instanceof Date ? IntlDateFormatter::NONE : IntlDateFormatter::SHORT); $format = $rule->getMessageFormat() ?? $this->messageFormat; + if ( + $format === null + && $this->messageDateType === null + && $this->messageTimeType === 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/src/Rule/Date/DateHandler.php b/src/Rule/Date/DateHandler.php index e2d64d706..912ccec3e 100644 --- a/src/Rule/Date/DateHandler.php +++ b/src/Rule/Date/DateHandler.php @@ -14,16 +14,18 @@ final class DateHandler extends BaseDateHandler /** * @psalm-param IntlDateFormatterFormat $dateType * @psalm-param non-empty-string|null $timeZone + * @psalm-param IntlDateFormatterFormat $defaultMessageDateType */ public function __construct( int $dateType = IntlDateFormatter::SHORT, ?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}.', + int $defaultMessageDateType = IntlDateFormatter::SHORT, ) { parent::__construct( $dateType, @@ -32,10 +34,12 @@ public function __construct( $locale, $messageFormat, $messageDateType, - IntlDateFormatter::NONE, + null, $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, + $defaultMessageDateType, + null, ); } } diff --git a/src/Rule/Date/DateTimeHandler.php b/src/Rule/Date/DateTimeHandler.php index da73ad403..d252f82e4 100644 --- a/src/Rule/Date/DateTimeHandler.php +++ b/src/Rule/Date/DateTimeHandler.php @@ -15,6 +15,8 @@ final class DateTimeHandler extends BaseDateHandler * @psalm-param IntlDateFormatterFormat $dateType * @psalm-param IntlDateFormatterFormat $timeType * @psalm-param non-empty-string|null $timeZone + * @psalm-param IntlDateFormatterFormat $defaultMessageDateType + * @psalm-param IntlDateFormatterFormat $defaultMessageTimeType */ public function __construct( int $dateType = IntlDateFormatter::SHORT, @@ -22,11 +24,13 @@ 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}.', + int $defaultMessageDateType = IntlDateFormatter::SHORT, + int $defaultMessageTimeType = IntlDateFormatter::SHORT, ) { parent::__construct( $dateType, @@ -39,6 +43,8 @@ public function __construct( $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, + $defaultMessageDateType, + $defaultMessageTimeType, ); } } diff --git a/src/Rule/Date/TimeHandler.php b/src/Rule/Date/TimeHandler.php index be4ab377a..707a7ceba 100644 --- a/src/Rule/Date/TimeHandler.php +++ b/src/Rule/Date/TimeHandler.php @@ -14,16 +14,18 @@ final class TimeHandler extends BaseDateHandler /** * @psalm-param IntlDateFormatterFormat $timeType * @psalm-param non-empty-string|null $timeZone + * @psalm-param IntlDateFormatterFormat $defaultMessageTimeType */ public function __construct( int $timeType = IntlDateFormatter::SHORT, ?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}.', + int $defaultMessageTimeType = IntlDateFormatter::SHORT, ) { parent::__construct( IntlDateFormatter::NONE, @@ -31,11 +33,13 @@ public function __construct( $timeZone, $locale, $messageFormat, - IntlDateFormatter::NONE, + null, $messageTimeType, $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, + null, + $defaultMessageTimeType, ); } } diff --git a/tests/Rule/Date/DateTest.php b/tests/Rule/Date/DateTest.php index 1fecec330..2fd613767 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 earlier than 1/1/25.']], + ['' => ['Value must be no earlier 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 later than 1/1/24.']], + ['' => ['Value must be no later than 2024-01-01.']], ], 'max-custom-message' => [ ['a' => '2024-03-29'], @@ -104,20 +104,26 @@ 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 later than 01.01.2024.']], + ['' => ['Value must be no later 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 later than 01.01.2024.']], + ['' => ['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), @@ -125,8 +131,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 later than 11/11/23.']], + new Date(format: 'php:d*m*Y', max: '11*11*2023'), + ['' => ['Value must be no later than 11*11*2023.']], [DateHandler::class => new DateHandler(messageDateType: null)], ], 'rule-message-format' => [ @@ -153,6 +159,54 @@ 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)], + ], + '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)], + ], + '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)], + ], + 'default-message-date-type-used-when-unset' => [ + '3/29/24', + new Date(max: '1/1/24'), + ['' => ['Value must be no later than Monday, January 1, 2024.']], + [DateHandler::class => new DateHandler(defaultMessageDateType: IntlDateFormatter::FULL)], + ], + 'default-message-date-type-does-not-override-format' => [ + '29*03*2024', + new Date(format: 'php:d*m*Y', max: '11*11*2023'), + ['' => ['Value must be no later than 11*11*2023.']], + [DateHandler::class => new DateHandler(defaultMessageDateType: IntlDateFormatter::FULL)], + ], + '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 later 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 later than 2099/12/31.']], + ], ]; } diff --git a/tests/Rule/Date/DateTimeTest.php b/tests/Rule/Date/DateTimeTest.php index 2a16c88a6..7a3bdca1d 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; @@ -74,24 +75,90 @@ 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 earlier than 1/1/25, 11:00 AM.']], + ['' => ['Value must be no earlier 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 later than 1/1/24, 12:00 AM.']], + ['' => ['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), - ['' => ['Value must be no earlier than 3/29/24, 9:40 AM.']], + ['' => ['Value must be no earlier 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 later than 11/11/23, 12:35 PM.']], + ['' => ['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-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, + ), + ], + ], + '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, + ), + ], + ], ]; } diff --git a/tests/Rule/Date/TimeTest.php b/tests/Rule/Date/TimeTest.php index cf649f616..b872309e9 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -57,22 +57,28 @@ public static function dataValidationFailed(): array 'min' => [ '15:30', new Time(format: 'HH:mm', min: '15:40'), - ['' => ['Value must be no earlier than 3:40 PM.']], + ['' => ['Value must be no earlier than 15:40.']], ], 'max' => [ '15:30', new Time(format: 'php:H:i', max: '12:00'), - ['' => ['Value must be no later than 12:00 PM.']], + ['' => ['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), - ['' => ['Value must be no earlier than 9:40 AM.']], + ['' => ['Value must be no earlier 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 later than 11:30 AM.']], + ['' => ['Value must be no later than 11*30.']], [TimeHandler::class => new TimeHandler(messageTimeType: null)], ], 'rule-message-format' => [ @@ -82,11 +88,34 @@ public static function dataValidationFailed(): array [TimeHandler::class => new TimeHandler(messageFormat: 'php:H_i')], ], 'handler-message-type' => [ + 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)], + ], + 'handler-message-type-overrides-format' => [ '13*30', - new Time(format: 'php:H*i', max: '11*30', timeType: IntlDateFormatter::SHORT), + new Time(format: 'php:H*i', max: '11*30'), ['' => ['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-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),