diff --git a/CHANGELOG.md b/CHANGELOG.md index 612f323d3..2511ffbd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.5.2 under development +- 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) diff --git a/src/Rule/Date/BaseDateHandler.php b/src/Rule/Date/BaseDateHandler.php index 03dc33dea..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 @@ -187,14 +189,26 @@ 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->getMessageDateTypeFallback($rule); + $formatterTimeType = $ruleMessageTimeType ?? $this->messageTimeType - ?? $this->getTimeTypeFromRule($rule); + ?? $this->getMessageTimeTypeFallback($rule); $format = $rule->getMessageFormat() ?? $this->messageFormat; + 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:')) { return $date->format(substr($format, 4)); } @@ -235,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 e2d64d706..2360d4b3f 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 */ @@ -25,6 +27,15 @@ public function __construct( string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', 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( $dateType, IntlDateFormatter::NONE, @@ -32,10 +43,11 @@ public function __construct( $locale, $messageFormat, $messageDateType, - IntlDateFormatter::NONE, + null, $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, + $messageDateTypeFallbackToRuleType, ); } } diff --git a/src/Rule/Date/DateTimeHandler.php b/src/Rule/Date/DateTimeHandler.php index da73ad403..b19315423 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 */ @@ -28,6 +30,20 @@ public function __construct( string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', 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( $dateType, $timeType, @@ -39,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 be4ab377a..4d880e11f 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 */ @@ -25,17 +27,28 @@ public function __construct( string $tooEarlyMessage = '{Property} must be no earlier than {limit}.', 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( IntlDateFormatter::NONE, $timeType, $timeZone, $locale, $messageFormat, - IntlDateFormatter::NONE, + null, $messageTimeType, $incorrectInputMessage, $tooEarlyMessage, $tooLateMessage, + true, + $messageTimeTypeFallbackToRuleType, ); } } diff --git a/tests/Rule/Date/DateTest.php b/tests/Rule/Date/DateTest.php index 1fecec330..b96cc356b 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,32 @@ 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}.')], + ], + '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), @@ -125,8 +137,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 +165,48 @@ 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-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( + 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..470019d0a 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,158 @@ 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}.')], + ], + '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), - ['' => ['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-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, + ), + ], + ], + '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..c56e9c44e 100644 --- a/tests/Rule/Date/TimeTest.php +++ b/tests/Rule/Date/TimeTest.php @@ -57,22 +57,39 @@ 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}.')], + ], + '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), - ['' => ['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 +99,45 @@ 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-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),