diff --git a/CHANGELOG.md b/CHANGELOG.md index c016680cf..985f62bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - New #665: Add methods `addErrorWithFormatOnly()` and `addErrorWithoutPostProcessing()` to `Result` object (@vjik) - Enh #668: Clarify psalm types in `Result` (@vjik) -- New #670, #680: Add `Image` validation rule (@vjik, @arogachev) +- New #670, #677, #680: Add `Image` validation rule (@vjik, @arogachev) - New #678: Add `Date`, `DateTime` and `Time` validation rules (@vjik) ## 1.2.0 February 21, 2024 diff --git a/src/Rule/Image/Image.php b/src/Rule/Image/Image.php index a9695949a..f24d5bcc8 100644 --- a/src/Rule/Image/Image.php +++ b/src/Rule/Image/Image.php @@ -6,6 +6,7 @@ use Attribute; use Closure; +use InvalidArgumentException; use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait; use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait; use Yiisoft\Validator\Rule\Trait\WhenTrait; @@ -36,6 +37,7 @@ final class Image implements RuleWithOptionsInterface, SkipOnErrorInterface, Whe * @param int|null $minHeight Expected minimum height of validated image file. * @param int|null $maxWidth Expected maximum width of validated image file. * @param int|null $maxHeight Expected maximum height of validated image file. + * @param ImageAspectRatio|null $aspectRatio Expected aspect ratio of validated image file. * @param string $notImageMessage A message used when the validated value is not valid image file. * * You may use the following placeholders in the message: @@ -89,6 +91,20 @@ final class Image implements RuleWithOptionsInterface, SkipOnErrorInterface, Whe * * - `{attribute}`: the translated label of the attribute being validated. * - `{limit}`: expected maximum height of validated image file. + * + * @param string $invalidAspectRatioMessage A message used when aspect ratio of validated image file is different + * than {@see ImageAspectRatio::$width}:{@see ImageAspectRatio::$height} with correction based on + * {@see ImageAspectRatio::$margin}. + * + * You may use the following placeholders in the message: + * + * - `{attribute}`: the translated label of the attribute being validated. + * - `{aspectRatioWidth}`: expected width part for aspect ratio. For example, for `4:3` aspect ratio, it will be + * `4`. + * - `{aspectRatioHeight}`: expected height part for aspect ratio. For example, for `4:3` aspect ratio, it will be + * `3`. + * - `{aspectRatioMargin}`: expected margin for aspect ratio in percents. + * * @param bool|callable|null $skipOnEmpty Whether to skip this rule if the value validated is empty. * See {@see SkipOnEmptyInterface}. * @param bool $skipOnError Whether to skip this rule if any of the previous rules gave an error. @@ -105,17 +121,26 @@ public function __construct( private ?int $minHeight = null, private ?int $maxWidth = null, private ?int $maxHeight = null, + private ?ImageAspectRatio $aspectRatio = null, private string $notImageMessage = 'The value must be an image.', - private string $notExactWidthMessage = 'The width of image "{attribute}" must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', - private string $notExactHeightMessage = 'The height of image "{attribute}" must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', - private string $tooSmallWidthMessage = 'The width of image "{attribute}" cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', - private string $tooSmallHeightMessage = 'The height of image "{attribute}" cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', - private string $tooLargeWidthMessage = 'The width of image "{attribute}" cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', - private string $tooLargeHeightMessage = 'The height of image "{attribute}" cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + private string $notExactWidthMessage = 'The width must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', + private string $notExactHeightMessage = 'The height must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', + private string $tooSmallWidthMessage = 'The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + private string $tooSmallHeightMessage = 'The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + private string $tooLargeWidthMessage = 'The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + private string $tooLargeHeightMessage = 'The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + private string $invalidAspectRatioMessage = 'The aspect ratio must be {aspectRatioWidth, number}:{aspectRatioHeight, number} with margin {aspectRatioMargin, number}%.', private mixed $skipOnEmpty = null, private bool $skipOnError = false, private Closure|null $when = null, ) { + if ($this->width !== null && ($this->minWidth !== null || $this->maxWidth !== null)) { + throw new InvalidArgumentException('Exact width and min / max width can\'t be specified together.'); + } + + if ($this->height !== null && ($this->minHeight !== null || $this->maxHeight !== null)) { + throw new InvalidArgumentException('Exact width and min / max height can\'t be specified together.'); + } } public function getWidth(): ?int @@ -148,6 +173,11 @@ public function getMaxHeight(): ?int return $this->maxHeight; } + public function getAspectRatio(): ?ImageAspectRatio + { + return $this->aspectRatio; + } + public function getNotImageMessage(): string { return $this->notImageMessage; @@ -183,6 +213,11 @@ public function getTooLargeHeightMessage(): string return $this->tooLargeHeightMessage; } + public function getInvalidAspectRatioMessage(): string + { + return $this->invalidAspectRatioMessage; + } + public function getName(): string { return 'image'; @@ -202,6 +237,9 @@ public function getOptions(): array 'minHeight' => $this->minHeight, 'maxWidth' => $this->maxWidth, 'maxHeight' => $this->maxHeight, + 'aspectRatioWidth' => $this->getAspectRatio()?->getWidth(), + 'aspectRatioHeight' => $this->getAspectRatio()?->getHeight(), + 'aspectRatioMargin' => $this->getAspectRatio()?->getMargin(), 'notExactWidthMessage' => [ 'template' => $this->notExactWidthMessage, 'parameters' => [ @@ -242,6 +280,14 @@ public function getOptions(): array 'template' => $this->notImageMessage, 'parameters' => [], ], + 'invalidAspectRatioMessage' => [ + 'template' => $this->invalidAspectRatioMessage, + 'parameters' => [ + 'aspectRatioWidth' => $this->getAspectRatio()?->getWidth(), + 'aspectRatioHeight' => $this->getAspectRatio()?->getHeight(), + 'aspectRatioMargin' => $this->getAspectRatio()?->getMargin(), + ], + ], 'skipOnEmpty' => $this->getSkipOnEmptyOption(), 'skipOnError' => $this->skipOnError, ]; diff --git a/src/Rule/Image/ImageAspectRatio.php b/src/Rule/Image/ImageAspectRatio.php new file mode 100644 index 000000000..95eab92cf --- /dev/null +++ b/src/Rule/Image/ImageAspectRatio.php @@ -0,0 +1,45 @@ +width; + } + + public function getHeight(): int + { + return $this->height; + } + + public function getMargin(): float + { + return $this->margin; + } +} diff --git a/src/Rule/Image/ImageHandler.php b/src/Rule/Image/ImageHandler.php index 759d6c98a..395c5b04b 100644 --- a/src/Rule/Image/ImageHandler.php +++ b/src/Rule/Image/ImageHandler.php @@ -91,6 +91,8 @@ public function validate(mixed $value, object $rule, ValidationContext $context) ]); } + $this->validateAspectRatio($width, $height, $rule, $context, $result); + return $result; } @@ -101,7 +103,8 @@ private function shouldValidateDimensions(Image $rule): bool || $rule->getMinHeight() !== null || $rule->getMinWidth() !== null || $rule->getMaxHeight() !== null - || $rule->getMaxWidth() !== null; + || $rule->getMaxWidth() !== null + || $rule->getAspectRatio() !== null; } private function getImageFilePath(mixed $value): ?string @@ -138,4 +141,35 @@ private function getFilePath(mixed $value): ?string } return is_string($value) ? $value : null; } + + private function validateAspectRatio( + int $validatedWidth, + int $validatedHeight, + Image $rule, + ValidationContext $context, + Result $result, + ): void { + if ($rule->getAspectRatio() === null) { + return; + } + + $validatedAspectRatio = $validatedWidth / $validatedHeight; + $expectedAspectRatio = $rule->getAspectRatio()->getWidth() / $rule->getAspectRatio()->getHeight(); + $absoluteMargin = $rule->getAspectRatio()->getMargin() / 100; + + if ( + ($validatedAspectRatio < $expectedAspectRatio - $absoluteMargin) || + ($validatedAspectRatio > $expectedAspectRatio + $absoluteMargin) + ) { + $result->addError( + $rule->getInvalidAspectRatioMessage(), + [ + 'attribute' => $context->getTranslatedAttribute(), + 'aspectRatioWidth' => $rule->getAspectRatio()->getWidth(), + 'aspectRatioHeight' => $rule->getAspectRatio()->getHeight(), + 'aspectRatioMargin' => $rule->getAspectRatio()->getMargin(), + ], + ); + } + } } diff --git a/src/RuleHandlerResolver/SimpleRuleHandlerContainer.php b/src/RuleHandlerResolver/SimpleRuleHandlerContainer.php index a5cee333d..4af1dc59d 100644 --- a/src/RuleHandlerResolver/SimpleRuleHandlerContainer.php +++ b/src/RuleHandlerResolver/SimpleRuleHandlerContainer.php @@ -41,8 +41,9 @@ final class SimpleRuleHandlerContainer implements RuleHandlerResolverInterface */ public function __construct( /** - * @var array A storage of rule handlers' instances - a mapping where keys are - * strings (the rule handlers' class names by default) and values are corresponding rule handlers' instances. + * @var RuleHandlerInterface[] A storage of rule handlers' instances - a mapping where keys are strings (the + * rule handlers' class names by default) and values are corresponding rule handlers' instances. + * @psalm-var array */ private array $instances = [], ) { diff --git a/tests/Rule/Image/ImageTest.php b/tests/Rule/Image/ImageTest.php index 7957a206f..9123d0b29 100644 --- a/tests/Rule/Image/ImageTest.php +++ b/tests/Rule/Image/ImageTest.php @@ -5,13 +5,18 @@ namespace Yiisoft\Validator\Tests\Rule\Image; use GuzzleHttp\Psr7\UploadedFile; +use InvalidArgumentException; use Yiisoft\Validator\Rule\Image\Image; +use Yiisoft\Validator\Rule\Image\ImageAspectRatio; use Yiisoft\Validator\Rule\Image\ImageHandler; +use Yiisoft\Validator\Rule\Image\ImageInfo; +use Yiisoft\Validator\RuleHandlerResolver\SimpleRuleHandlerContainer; use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait; use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase; use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait; use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait; use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait; +use Yiisoft\Validator\Validator; final class ImageTest extends RuleTestCase { @@ -20,6 +25,38 @@ final class ImageTest extends RuleTestCase use SkipOnErrorTestTrait; use WhenTestTrait; + public function dataConfigurationError(): array + { + return [ + 'width and min width' => [ + ['width' => 800, 'minWidth' => 800], + 'Exact width and min / max width can\'t be specified together.', + ], + 'width and max width' => [ + ['width' => 800, 'maxWidth' => 800], + 'Exact width and min / max width can\'t be specified together.', + ], + 'heifht and min height' => [ + ['height' => 600, 'minHeight' => 600], + 'Exact width and min / max height can\'t be specified together.', + ], + 'heifht and max height' => [ + ['height' => 600, 'maxHeight' => 600], + 'Exact width and min / max height can\'t be specified together.', + ], + ]; + } + + /** + * @dataProvider dataConfigurationError + */ + public function testConfigurationError(array $arguments, string $expectedExceptionMessage): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + new Image(...$arguments); + } + public function testGetName(): void { $rule = new Image(); @@ -45,6 +82,60 @@ public function dataValidationPassed(): array ]; } + public function dataValidationPassedAspectRatio(): array + { + return [ + 'default margin' => [ + new ImageInfo(800, 600), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3)), + ], + 'boundary width, min' => [ + new ImageInfo(794, 600), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ], + 'boundary width, max' => [ + new ImageInfo(806, 600), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ], + 'boundary height, min' => [ + new ImageInfo(800, 596), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ], + 'boundary height, max' => [ + new ImageInfo(800, 604), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ], + 'width within margin, smaller value' => [ + new ImageInfo(795, 600), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ], + 'width within margin, bigger value' => [ + new ImageInfo(805, 600), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ], + 'height within margin, smaller value' => [ + new ImageInfo(800, 597), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ], + 'height within margin, bigger value' => [ + new ImageInfo(800, 603), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ], + ]; + } + + /** + * @dataProvider dataValidationPassedAspectRatio + */ + public function testValidationPassedAspectRatio(ImageInfo $imageInfo, Image $rules): void + { + $ruleHandlerResolver = new SimpleRuleHandlerContainer([ + ImageHandler::class => new ImageHandler(new StubImageInfoProvider($imageInfo)), + ]); + $result = (new Validator($ruleHandlerResolver))->validate(__DIR__ . '/16x18.jpg', $rules); + $this->assertSame([], $result->getErrorMessagesIndexedByPath()); + } + public function dataValidationFailed(): array { $notImageResult = ['' => ['The value must be an image.']]; @@ -79,32 +170,140 @@ public function dataValidationFailed(): array new Image(width: 24, height: 32), [ '' => [ - 'The width of image "" must be exactly 24 pixels.', - 'The height of image "" must be exactly 32 pixels.', + 'The width must be exactly 24 pixels.', + 'The height must be exactly 32 pixels.', + ], + ], + ], + 'not-exactly-with-custom-message' => [ + ['a' => __DIR__ . '/16x18.jpg'], + [ + 'a' => new Image( + width: 24, + height: 32, + notExactWidthMessage: 'Attribute - {attribute}, exactly - {exactly}.', + notExactHeightMessage: 'Attribute - {attribute}, exactly - {exactly}.', + ), + ], + [ + 'a' => [ + 'Attribute - a, exactly - 24.', + 'Attribute - a, exactly - 32.', ], ], ], 'too-small-width' => [ __DIR__ . '/16x18.jpg', new Image(minWidth: 17), - ['' => ['The width of image "" cannot be smaller than 17 pixels.']], + ['' => ['The width cannot be smaller than 17 pixels.']], + ], + 'too-small-width-with-custom-message' => [ + ['a' => __DIR__ . '/16x18.jpg'], + ['a' => new Image(minWidth: 17, tooSmallWidthMessage: 'Attribute - {attribute}, limit - {limit}.')], + ['a' => ['Attribute - a, limit - 17.']], ], 'too-small-height' => [ __DIR__ . '/16x18.jpg', new Image(minHeight: 19), - ['' => ['The height of image "" cannot be smaller than 19 pixels.']], + ['' => ['The height cannot be smaller than 19 pixels.']], + ], + 'too-small-height-with-custom-message' => [ + ['a' => __DIR__ . '/16x18.jpg'], + ['a' => new Image(minHeight: 19, tooSmallHeightMessage: 'Attribute - {attribute}, limit - {limit}.')], + ['a' => ['Attribute - a, limit - 19.']], ], 'too-large-width' => [ __DIR__ . '/16x18.jpg', new Image(maxWidth: 15), - ['' => ['The width of image "" cannot be larger than 15 pixels.']], + ['' => ['The width cannot be larger than 15 pixels.']], + ], + 'too-large-width-with-custom-message' => [ + ['a' => __DIR__ . '/16x18.jpg'], + ['a' => new Image(maxWidth: 15, tooLargeWidthMessage: 'Attribute - {attribute}, limit - {limit}.')], + ['a' => ['Attribute - a, limit - 15.']], ], 'too-large-height' => [ __DIR__ . '/16x18.jpg', new Image(maxHeight: 17), - ['' => ['The height of image "" cannot be larger than 17 pixels.']], + ['' => ['The height cannot be larger than 17 pixels.']], + ], + 'too-large-height-with-custom-message' => [ + ['a' => __DIR__ . '/16x18.jpg'], + ['a' => new Image(maxHeight: 17, tooLargeHeightMessage: 'Attribute - {attribute}, limit - {limit}.')], + ['a' => ['Attribute - a, limit - 17.']], + ], + ]; + } + + public function dataValidationFailedAspectRatio(): array + { + return [ + 'default aspect ratio margin , smaller height' => [ + new ImageInfo(800, 599), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3)), + ['' => ['The aspect ratio must be 4:3 with margin 0%.']], + ], + 'default aspect ratio margin, bigger height' => [ + new ImageInfo(800, 601), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3)), + ['' => ['The aspect ratio must be 4:3 with margin 0%.']], ], + 'width, off by 1, smaller value' => [ + new ImageInfo(593, 600), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ['' => ['The aspect ratio must be 4:3 with margin 1%.']], + ], + 'width, off by 1, bigger value' => [ + new ImageInfo(807, 600), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ['' => ['The aspect ratio must be 4:3 with margin 1%.']], + ], + 'height, off by 1, smaller value' => [ + new ImageInfo(800, 593), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ['' => ['The aspect ratio must be 4:3 with margin 1%.']], + ], + 'height, off by 1, bigger value' => [ + new ImageInfo(800, 607), + new Image(aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1)), + ['' => ['The aspect ratio must be 4:3 with margin 1%.']], + ], + 'absolute margin calculation mutant, / 100 => / 99' => [ + new ImageInfo(801, 600), + new Image(aspectRatio: new ImageAspectRatio(width: 1, height: 3, margin: 100)), + ['' => ['The aspect ratio must be 1:3 with margin 100%.']], + ], + ]; + } + + /** + * @dataProvider dataValidationFailedAspectRatio + */ + public function testValidationFailedAspectRatio(ImageInfo $imageInfo, Image $rules, array $errors): void + { + $ruleHandlerResolver = new SimpleRuleHandlerContainer([ + ImageHandler::class => new ImageHandler(new StubImageInfoProvider($imageInfo)), + ]); + $result = (new Validator($ruleHandlerResolver))->validate(__DIR__ . '/16x18.jpg', $rules); + $this->assertSame($errors, $result->getErrorMessagesIndexedByPath()); + } + + public function testValidationFailedAspectRatioWithCustomMessage(): void + { + $ruleHandlerResolver = new SimpleRuleHandlerContainer([ + ImageHandler::class => new ImageHandler(new StubImageInfoProvider(new ImageInfo(800, 599))), + ]); + $rules = [ + 'a' => new Image( + aspectRatio: new ImageAspectRatio(width: 4, height: 3), + invalidAspectRatioMessage: 'Attribute - {attribute}.', + ), ]; + $result = (new Validator($ruleHandlerResolver))->validate(['a' => __DIR__ . '/16x18.jpg'], $rules); + $this->assertSame( + ['a' => ['Attribute - a.']], + $result->getErrorMessagesIndexedByPath(), + ); } protected function getDifferentRuleInHandlerItems(): array @@ -115,7 +314,7 @@ protected function getDifferentRuleInHandlerItems(): array public function dataOptions(): array { return [ - [ + 'default' => [ new Image(), [ 'width' => null, @@ -124,38 +323,105 @@ public function dataOptions(): array 'minHeight' => null, 'maxWidth' => null, 'maxHeight' => null, + 'aspectRatioWidth' => null, + 'aspectRatioHeight' => null, + 'aspectRatioMargin' => null, 'notExactWidthMessage' => [ - 'template' => 'The width of image "{attribute}" must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', + 'template' => 'The width must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', 'parameters' => [ 'exactly' => null, ], ], 'notExactHeightMessage' => [ - 'template' => 'The height of image "{attribute}" must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', + 'template' => 'The height must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', 'parameters' => [ 'exactly' => null, ], ], 'tooSmallWidthMessage' => [ - 'template' => 'The width of image "{attribute}" cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'template' => 'The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'limit' => null, + ], + ], + 'tooSmallHeightMessage' => [ + 'template' => 'The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'limit' => null, + ], + ], + 'tooLargeWidthMessage' => [ + 'template' => 'The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'limit' => null, + ], + ], + 'tooLargeHeightMessage' => [ + 'template' => 'The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'limit' => null, + ], + ], + 'notImageMessage' => [ + 'template' => 'The value must be an image.', + 'parameters' => [], + ], + 'invalidAspectRatioMessage' => [ + 'template' => 'The aspect ratio must be {aspectRatioWidth, number}:{aspectRatioHeight, number} with margin {aspectRatioMargin, number}%.', + 'parameters' => [ + 'aspectRatioWidth' => null, + 'aspectRatioHeight' => null, + 'aspectRatioMargin' => null, + ], + ], + 'skipOnEmpty' => false, + 'skipOnError' => false, + ], + ], + 'exact width and height' => [ + new Image(width: 800, height: 600), + [ + 'width' => 800, + 'height' => 600, + 'minWidth' => null, + 'minHeight' => null, + 'maxWidth' => null, + 'maxHeight' => null, + 'aspectRatioWidth' => null, + 'aspectRatioHeight' => null, + 'aspectRatioMargin' => null, + 'notExactWidthMessage' => [ + 'template' => 'The width must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'exactly' => 800, + ], + ], + 'notExactHeightMessage' => [ + 'template' => 'The height must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'exactly' => 600, + ], + ], + 'tooSmallWidthMessage' => [ + 'template' => 'The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', 'parameters' => [ 'limit' => null, ], ], 'tooSmallHeightMessage' => [ - 'template' => 'The height of image "{attribute}" cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'template' => 'The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', 'parameters' => [ 'limit' => null, ], ], 'tooLargeWidthMessage' => [ - 'template' => 'The width of image "{attribute}" cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'template' => 'The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', 'parameters' => [ 'limit' => null, ], ], 'tooLargeHeightMessage' => [ - 'template' => 'The height of image "{attribute}" cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'template' => 'The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', 'parameters' => [ 'limit' => null, ], @@ -164,6 +430,84 @@ public function dataOptions(): array 'template' => 'The value must be an image.', 'parameters' => [], ], + 'invalidAspectRatioMessage' => [ + 'template' => 'The aspect ratio must be {aspectRatioWidth, number}:{aspectRatioHeight, number} with margin {aspectRatioMargin, number}%.', + 'parameters' => [ + 'aspectRatioWidth' => null, + 'aspectRatioHeight' => null, + 'aspectRatioMargin' => null, + ], + ], + 'skipOnEmpty' => false, + 'skipOnError' => false, + ], + ], + 'min and max height with aspect ratio' => [ + new Image( + minWidth: 700, + minHeight: 550, + maxWidth: 900, + maxHeight: 750, + aspectRatio: new ImageAspectRatio(width: 4, height: 3, margin: 1), + ), + [ + 'width' => null, + 'height' => null, + 'minWidth' => 700, + 'minHeight' => 550, + 'maxWidth' => 900, + 'maxHeight' => 750, + 'aspectRatioWidth' => 4, + 'aspectRatioHeight' => 3, + 'aspectRatioMargin' => 1.0, + 'notExactWidthMessage' => [ + 'template' => 'The width must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'exactly' => null, + ], + ], + 'notExactHeightMessage' => [ + 'template' => 'The height must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'exactly' => null, + ], + ], + 'tooSmallWidthMessage' => [ + 'template' => 'The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'limit' => 700, + ], + ], + 'tooSmallHeightMessage' => [ + 'template' => 'The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'limit' => 550, + ], + ], + 'tooLargeWidthMessage' => [ + 'template' => 'The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'limit' => 900, + ], + ], + 'tooLargeHeightMessage' => [ + 'template' => 'The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'parameters' => [ + 'limit' => 750, + ], + ], + 'notImageMessage' => [ + 'template' => 'The value must be an image.', + 'parameters' => [], + ], + 'invalidAspectRatioMessage' => [ + 'template' => 'The aspect ratio must be {aspectRatioWidth, number}:{aspectRatioHeight, number} with margin {aspectRatioMargin, number}%.', + 'parameters' => [ + 'aspectRatioWidth' => 4, + 'aspectRatioHeight' => 3, + 'aspectRatioMargin' => 1.0, + ], + ], 'skipOnEmpty' => false, 'skipOnError' => false, ],