Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aspect ratio support to Image rule #681

Merged
merged 18 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 52 additions & 6 deletions src/Rule/Image/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -183,6 +213,11 @@ public function getTooLargeHeightMessage(): string
return $this->tooLargeHeightMessage;
}

public function getInvalidAspectRatioMessage(): string
{
return $this->invalidAspectRatioMessage;
}

public function getName(): string
{
return 'image';
Expand All @@ -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' => [
Expand Down Expand Up @@ -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,
];
Expand Down
45 changes: 45 additions & 0 deletions src/Rule/Image/ImageAspectRatio.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Validator\Rule\Image;

/**
* {@link https://en.wikipedia.org/wiki/Aspect_ratio_(image)}
arogachev marked this conversation as resolved.
Show resolved Hide resolved
*/
final class ImageAspectRatio
{
/**
* @param int $width Expected width part for aspect ratio. For example, for `4:3` aspect ratio, it will be `4`.
* @param int $height Expected height part for aspect ratio. For example, for `4:3` aspect ratio, it will be `3`.
* @param float $margin Expected margin for aspect ratio in percents. For example, with value `1` and `4:3` aspect
* ratio:
*
* - If the validated image has height of 600 pixels, the allowed width range is 794 - 806 pixels.
* - If the validated image has width of 800 pixels, the allowed height range is 596 - 604 pixels.
*
* Defaults to `0` meaning no margin is allowed. For example, image with size 800 x 600 pixels and aspect ratio
* expected to be `4:3` will meet this requirement.
*/
public function __construct(

Check warning on line 24 in src/Rule/Image/ImageAspectRatio.php

View check run for this annotation

Codecov / codecov/patch

src/Rule/Image/ImageAspectRatio.php#L24

Added line #L24 was not covered by tests
private int $width,
private int $height,
private float $margin = 0,
) {
}

Check warning on line 29 in src/Rule/Image/ImageAspectRatio.php

View check run for this annotation

Codecov / codecov/patch

src/Rule/Image/ImageAspectRatio.php#L29

Added line #L29 was not covered by tests

public function getWidth(): int
{
return $this->width;
}

public function getHeight(): int
{
return $this->height;
}

public function getMargin(): float
{
return $this->margin;
}
}
36 changes: 35 additions & 1 deletion src/Rule/Image/ImageHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
]);
}

$this->validateAspectRatio($width, $height, $rule, $context, $result);

return $result;
}

Expand All @@ -101,7 +103,8 @@
|| $rule->getMinHeight() !== null
|| $rule->getMinWidth() !== null
|| $rule->getMaxHeight() !== null
|| $rule->getMaxWidth() !== null;
|| $rule->getMaxWidth() !== null
|| $rule->getAspectRatio() !== null;
}

private function getImageFilePath(mixed $value): ?string
Expand Down Expand Up @@ -138,4 +141,35 @@
}
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(),
[

Check warning on line 166 in src/Rule/Image/ImageHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "ArrayItemRemoval": --- Original +++ New @@ @@ $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()]); + $result->addError($rule->getInvalidAspectRatioMessage(), ['aspectRatioWidth' => $rule->getAspectRatio()->getWidth(), 'aspectRatioHeight' => $rule->getAspectRatio()->getHeight(), 'aspectRatioMargin' => $rule->getAspectRatio()->getMargin()]); } } }
'attribute' => $context->getTranslatedAttribute(),
'aspectRatioWidth' => $rule->getAspectRatio()->getWidth(),
'aspectRatioHeight' => $rule->getAspectRatio()->getHeight(),
'aspectRatioMargin' => $rule->getAspectRatio()->getMargin(),
],
);
}
}
}
5 changes: 3 additions & 2 deletions src/RuleHandlerResolver/SimpleRuleHandlerContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ final class SimpleRuleHandlerContainer implements RuleHandlerResolverInterface
*/
public function __construct(
/**
* @var array<string, 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.
* @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<string, RuleHandlerInterface>
*/
private array $instances = [],
) {
Expand Down
Loading
Loading