Skip to content

Commit

Permalink
Add methods addErrorWithFormatOnly() and `addErrorWithoutPostProces…
Browse files Browse the repository at this point in the history
…sing()` to `Result` object (#665)
  • Loading branch information
vjik committed Feb 26, 2024
1 parent 0eef6dc commit 150eb8a
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 77 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Yii Validator Change Log

## 1.2.1 under development
## 1.3.0 under development

- no changes in this release.
- New #665: Add methods `addErrorWithFormatOnly()` and `addErrorWithoutPostProcessing()` to `Result` object (@vjik)

## 1.2.0 February 21, 2024

Expand Down
21 changes: 21 additions & 0 deletions src/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
final class Error
{
public const MESSAGE_NONE = 0;
public const MESSAGE_FORMAT = 1;
public const MESSAGE_TRANSLATE = 2;

/**
* @param string $message The raw validation error message. Can be a simple text or a template with placeholders enclosed
* in curly braces (`{}`). In the end of the validation it will be translated using configured translator.
Expand Down Expand Up @@ -60,12 +64,19 @@ final class Error
*
* A value without nested structure won't have a path at all (it will be an empty array).
*
* @param int $messageProcessing Message processing type:
* - `Error::MESSAGE_NONE` - without post-processing;
* - `Error::MESSAGE_FORMAT` - format message;
* - `Error::MESSAGE_TRANSLATE` - translate message (translator do formatting also).
*
* @psalm-param list<int|string> $valuePath
* @psalm-param self::MESSAGE_* $messageProcessing
*/
public function __construct(
private string $message,
private array $parameters = [],
private array $valuePath = [],
private int $messageProcessing = self::MESSAGE_TRANSLATE,
) {
}

Expand Down Expand Up @@ -124,4 +135,14 @@ public function getValuePath(bool|string|null $escape = false): array
$this->valuePath,
);
}

/**
* Returns error message processing type.
*
* @psalm-return self::MESSAGE_*
*/
public function getMessageProcessing(): int
{
return $this->messageProcessing;
}
}
47 changes: 47 additions & 0 deletions src/Helper/MessageProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Validator\Helper;

use Yiisoft\Translator\MessageFormatterInterface;
use Yiisoft\Translator\TranslatorInterface;
use Yiisoft\Validator\Error;

/**
* @internal
*/
final class MessageProcessor
{
/**
* @param TranslatorInterface $translator A translator instance used for translations of error messages.
* @param string $translationCategory A translation category.
* @param MessageFormatterInterface $messageFormatter A message formatter instance used for formats of error
* messages.
* @param string $messageFormatterLocale Locale to use when error message requires format only.
*/
public function __construct(
private TranslatorInterface $translator,
private string $translationCategory,
private MessageFormatterInterface $messageFormatter,
private string $messageFormatterLocale,
) {
}

public function process(Error $error): string
{
return match ($error->getMessageProcessing()) {
Error::MESSAGE_TRANSLATE => $this->translator->translate(
$error->getMessage(),
$error->getParameters(),
$this->translationCategory
),
Error::MESSAGE_FORMAT => $this->messageFormatter->format(
$error->getMessage(),
$error->getParameters(),
$this->messageFormatterLocale,
),
default => $error->getMessage(),
};
}
}
32 changes: 32 additions & 0 deletions src/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,36 @@ public function addError(string $message, array $parameters = [], array $valuePa

return $this;
}

/**
* Add an error, the message of which does not require translation, but should be formatted.
*
* @see addError()
*
* @psalm-param array<string,scalar|null> $parameters
* @psalm-param list<int|string> $valuePath
*
* @return $this Same instance of result.
*/
public function addErrorWithFormatOnly(string $message, array $parameters = [], array $valuePath = []): self
{
$this->errors[] = new Error($message, $parameters, $valuePath, Error::MESSAGE_FORMAT);
return $this;
}

/**
* Add an error, the message of which does not require any post-processing.
*
* @see addError()
*
* @psalm-param array<string,scalar|null> $parameters
* @psalm-param list<int|string> $valuePath
*
* @return $this Same instance of result.
*/
public function addErrorWithoutPostProcessing(string $message, array $parameters = [], array $valuePath = []): self
{
$this->errors[] = new Error($message, $parameters, $valuePath, Error::MESSAGE_NONE);
return $this;
}
}
2 changes: 1 addition & 1 deletion src/Rule/EachHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function validate(mixed $value, object $rule, ValidationContext $context)
}

foreach ($itemResult->getErrors() as $error) {
$result->addError(
$result->addErrorWithoutPostProcessing(
$error->getMessage(),
$error->getParameters(),
$error->getValuePath() === [] ? [$index] : [$index, ...$error->getValuePath()],
Expand Down
6 changes: 5 additions & 1 deletion src/Rule/NestedHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ public function validate(mixed $value, object $rule, ValidationContext $context)
array_push($valuePathList, ...$error->getValuePath());
}

$compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePathList);
$compoundResult->addErrorWithoutPostProcessing(
$error->getMessage(),
$error->getParameters(),
$valuePathList
);
}
}

Expand Down
54 changes: 36 additions & 18 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
use Yiisoft\Translator\CategorySource;
use Yiisoft\Translator\IdMessageReader;
use Yiisoft\Translator\IntlMessageFormatter;
use Yiisoft\Translator\MessageFormatterInterface;
use Yiisoft\Translator\NullMessageFormatter;
use Yiisoft\Translator\SimpleMessageFormatter;
use Yiisoft\Translator\Translator;
use Yiisoft\Translator\TranslatorInterface;
use Yiisoft\Validator\AttributeTranslator\TranslatorAttributeTranslator;
use Yiisoft\Validator\Helper\DataSetNormalizer;
use Yiisoft\Validator\Helper\MessageProcessor;
use Yiisoft\Validator\Helper\RulesNormalizer;
use Yiisoft\Validator\Helper\SkipOnEmptyNormalizer;
use Yiisoft\Validator\RuleHandlerResolver\SimpleRuleHandlerContainer;
Expand All @@ -38,11 +41,6 @@ final class Validator implements ValidatorInterface
* @var RuleHandlerResolverInterface A container to resolve rule handler names to corresponding instances.
*/
private RuleHandlerResolverInterface $ruleHandlerResolver;
/**
* @var TranslatorInterface A translator instance used for translations of error messages. If it was not set
* explicitly in the constructor, a default one created automatically in {@see createDefaultTranslator()}.
*/
private TranslatorInterface $translator;

/**
* @var callable A default "skip on empty" condition ({@see SkipOnEmptyInterface}), already normalized. Used to
Expand All @@ -58,6 +56,8 @@ final class Validator implements ValidatorInterface
*/
private AttributeTranslatorInterface $defaultAttributeTranslator;

private MessageProcessor $messageProcessor;

/**
* @param RuleHandlerResolverInterface|null $ruleHandlerResolver Optional container to resolve rule handler names to
* corresponding instances. If not provided, {@see SimpleRuleContainer} used as a default one.
Expand All @@ -70,21 +70,32 @@ final class Validator implements ValidatorInterface
* argument was not specified explicitly. If not provided, a {@see DEFAULT_TRANSLATION_CATEGORY} will be used.
* @param AttributeTranslatorInterface|null $defaultAttributeTranslator A default translator used for translation of
* rule ({@see RuleInterface}) attributes. If not provided, a {@see TranslatorAttributeTranslator} will be used.
* @param MessageFormatterInterface|null $messageFormatter A message formatter instance used for formats of error
* messages that requires format only. If not provided, message is returned as is.
* @param string $messageFormatterLocale Locale to use when error message requires format only.
*
* @psalm-param SkipOnEmptyValue $defaultSkipOnEmpty
*/
public function __construct(
?RuleHandlerResolverInterface $ruleHandlerResolver = null,
?TranslatorInterface $translator = null,
bool|callable|null $defaultSkipOnEmpty = null,
private string $translationCategory = self::DEFAULT_TRANSLATION_CATEGORY,
string $translationCategory = self::DEFAULT_TRANSLATION_CATEGORY,
?AttributeTranslatorInterface $defaultAttributeTranslator = null,
?MessageFormatterInterface $messageFormatter = null,
string $messageFormatterLocale = 'en-US',
) {
$translator ??= $this->createDefaultTranslator($translationCategory);
$this->ruleHandlerResolver = $ruleHandlerResolver ?? new SimpleRuleHandlerContainer();
$this->translator = $translator ?? $this->createDefaultTranslator();
$this->defaultSkipOnEmptyCondition = SkipOnEmptyNormalizer::normalize($defaultSkipOnEmpty);
$this->defaultAttributeTranslator = $defaultAttributeTranslator
?? new TranslatorAttributeTranslator($this->translator);
?? new TranslatorAttributeTranslator($translator);
$this->messageProcessor = new MessageProcessor(
$translator,
$translationCategory,
$messageFormatter ?? new NullMessageFormatter(),
$messageFormatterLocale,
);
}

/**
Expand Down Expand Up @@ -144,12 +155,8 @@ public function validate(
$tempResult = $this->validateInternal($validatedData, $attributeRules, $context);

foreach ($tempResult->getErrors() as $error) {
$result->addError(
$this->translator->translate(
$error->getMessage(),
$error->getParameters(),
$this->translationCategory
),
$result->addErrorWithoutPostProcessing(
$this->messageProcessor->process($error),
$error->getParameters(),
$error->getValuePath()
);
Expand Down Expand Up @@ -201,7 +208,19 @@ private function validateInternal(mixed $value, iterable $rules, ValidationConte
if ($context->getAttribute() !== null) {
$valuePath = [$context->getAttribute(), ...$valuePath];
}
$compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath);
match ($error->getMessageProcessing()) {
Error::MESSAGE_TRANSLATE => $compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath),
Error::MESSAGE_FORMAT => $compoundResult->addErrorWithFormatOnly(
$error->getMessage(),
$error->getParameters(),
$valuePath
),
default => $compoundResult->addErrorWithoutPostProcessing(
$error->getMessage(),
$error->getParameters(),
$valuePath
),
};
}
}
return $compoundResult;
Expand Down Expand Up @@ -254,16 +273,15 @@ private function shouldSkipRule(RuleInterface $rule, mixed $value, ValidationCon
*
* @return Translator Translator instance used for translations of error messages.
*/
private function createDefaultTranslator(): Translator
private function createDefaultTranslator(string $category): Translator
{
$categorySource = new CategorySource(
$this->translationCategory,
$category,
new IdMessageReader(),
extension_loaded('intl') ? new IntlMessageFormatter() : new SimpleMessageFormatter(),
);
$translator = new Translator();
$translator->addCategorySources($categorySource);

return $translator;
}
}
60 changes: 60 additions & 0 deletions tests/MessagesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
use PHPUnit\Framework\TestCase;
use Yiisoft\Translator\CategorySource;
use Yiisoft\Translator\Message\Php\MessageSource;
use Yiisoft\Translator\MessageFormatterInterface;
use Yiisoft\Translator\SimpleMessageFormatter;
use Yiisoft\Translator\Translator;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Regex;
use Yiisoft\Validator\Validator;

Expand Down Expand Up @@ -68,6 +70,64 @@ public function testNonEmpty(string $locale): void
}
}

public function testErrorWithoutPostProcessing(): void
{
$translator = (new Translator('ru', 'en'))->addCategorySources(
new CategorySource(
Validator::DEFAULT_TRANSLATION_CATEGORY,
new MessageSource($this->getMessagesPath()),
new SimpleMessageFormatter(),
)
);
$validator = new Validator(translator: $translator);

$result = $validator->validate(
'hello',
[static fn() => (new Result())->addErrorWithoutPostProcessing('Value is invalid.')],
);

$this->assertSame(
['' => ['Value is invalid.']],
$result->getErrorMessagesIndexedByAttribute(),
);
}

public function testErrorWithFormatOnly(): void
{
$translator = (new Translator('ru', 'en'))->addCategorySources(
new CategorySource(
Validator::DEFAULT_TRANSLATION_CATEGORY,
new MessageSource($this->getMessagesPath()),
new SimpleMessageFormatter(),
)
);
$messageFormatter = new class () implements MessageFormatterInterface {
public function format(string $message, array $parameters, string $locale): string
{
$result = $message . '!';
foreach ($parameters as $key => $value) {
$result .= $key . '-' . $value;
}
return $result . '!' . $locale;
}
};
$validator = new Validator(
translator: $translator,
messageFormatter: $messageFormatter,
messageFormatterLocale: 'ru',
);

$result = $validator->validate(
'hello',
[static fn() => (new Result())->addErrorWithFormatOnly('Value is invalid.', ['a' => 3])],
);

$this->assertSame(
['' => ['Value is invalid.!a-3!ru']],
$result->getErrorMessagesIndexedByAttribute(),
);
}

private function getMessagesPath(): string
{
return dirname(__DIR__) . '/messages';
Expand Down
Loading

0 comments on commit 150eb8a

Please sign in to comment.