From dc0c0345c9fcb614c172a105f9fda0db576835e1 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Sat, 31 Jan 2026 02:27:08 +0100 Subject: [PATCH 1/2] Create "Formatted" validator The Formatted validator decorates another validator to transform how input values appear in error messages, while still validating the original unmodified input. This is useful for improving the readability of error messages by displaying values in a user-friendly formatd. The validator accepts any Respect\StringFormatter\Formatter implementation, allowing direct use of StringFormatter's fluent builder. As StringFormatter expands with more formatters in future releases, users will automatically benefit from the full range of formatting options. Assisted-by: Claude Code (Opus 4.5) --- docs/migrating-from-v2-to-v3.md | 15 +++++ docs/validators.md | 6 +- docs/validators/Formatted.md | 52 +++++++++++++++++ src-dev/Commands/LintMixinCommand.php | 1 + src/Mixins/Builder.php | 2 + src/Mixins/Chain.php | 2 + src/Mixins/NotBuilder.php | 2 + src/Mixins/NotChain.php | 2 + src/Mixins/NullOrBuilder.php | 2 + src/Mixins/NullOrChain.php | 2 + src/Mixins/UndefOrBuilder.php | 2 + src/Mixins/UndefOrChain.php | 2 + src/Validators/Formatted.php | 37 ++++++++++++ tests/feature/Validators/FormattedTest.php | 35 ++++++++++++ tests/src/Formatters/FormatterStub.php | 26 +++++++++ tests/src/SmokeTestProvider.php | 1 + tests/unit/Validators/FormattedTest.php | 66 ++++++++++++++++++++++ 17 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 docs/validators/Formatted.md create mode 100644 src/Validators/Formatted.php create mode 100644 tests/feature/Validators/FormattedTest.php create mode 100644 tests/src/Formatters/FormatterStub.php create mode 100644 tests/unit/Validators/FormattedTest.php diff --git a/docs/migrating-from-v2-to-v3.md b/docs/migrating-from-v2-to-v3.md index d7f3f12f5..8159ba68a 100644 --- a/docs/migrating-from-v2-to-v3.md +++ b/docs/migrating-from-v2-to-v3.md @@ -585,6 +585,7 @@ Version 3.0 introduces several new validators: | `BetweenExclusive` | Validates that a value is between two bounds (exclusive) | | `ContainsCount` | Validates the count of occurrences in a value | | `DateTimeDiff` | Validates date/time differences (replaces Age validators) | +| `Formatted` | Formats input values in error messages | | `ShortCircuit` | Stops at first failure instead of collecting all errors | | `Hetu` | Validates Finnish personal identity codes (henkilötunnus) | | `KeyExists` | Checks if an array key exists | @@ -659,6 +660,20 @@ v::dateTimeDiff('years', v::greaterThanOrEqual(18))->assert('2000-01-01'); // pa v::dateTimeDiff('days', v::lessThan(30))->assert('2024-01-15'); // passes if less than 30 days ago ``` +#### Formatted + +Decorates a validator to format input values in error messages while still validating the original input: + +```php +use Respect\StringFormatter\FormatterBuilder as f; + +v::formatted(f::mask('1-4'), v::email())->assert('not an email'); +// → "****an email" must be an email address + +v::formatted(f::pattern('#### #### #### ####'), v::creditCard())->assert('1234123412341234'); +// → "1234 1234 1234 1234" must be a credit card number +``` + #### ShortCircuit Validates input against a series of validators, stopping at the first failure. Useful for dependent validations: diff --git a/docs/validators.md b/docs/validators.md index d91a443ce..6c67cad71 100644 --- a/docs/validators.md +++ b/docs/validators.md @@ -27,7 +27,7 @@ In this page you will find a list of validators by their category. **Date and Time**: [Date][] - [DateTime][] - [DateTimeDiff][] - [LeapDate][] - [LeapYear][] - [Time][] -**Display**: [Format][] - [Masked][] - [Named][] - [Templated][] +**Display**: [Format][] - [Formatted][] - [Masked][] - [Named][] - [Templated][] **File system**: [Directory][] - [Executable][] - [Exists][] - [Extension][] - [File][] - [Image][] - [Mimetype][] - [Readable][] - [Size][] - [SymbolicLink][] - [Writable][] @@ -53,7 +53,7 @@ In this page you will find a list of validators by their category. **Structures**: [Attributes][] - [Key][] - [KeyExists][] - [KeyOptional][] - [KeySet][] - [Property][] - [PropertyExists][] - [PropertyOptional][] -**Transformations**: [After][] - [All][] - [Each][] - [Length][] - [Max][] - [Min][] - [Size][] +**Transformations**: [After][] - [All][] - [Each][] - [Formatted][] - [Length][] - [Max][] - [Min][] - [Size][] **Types**: [ArrayType][] - [ArrayVal][] - [BoolType][] - [BoolVal][] - [CallableType][] - [Countable][] - [FloatType][] - [FloatVal][] - [IntType][] - [IntVal][] - [IterableType][] - [IterableVal][] - [NullType][] - [NumericVal][] - [ObjectType][] - [ResourceType][] - [ScalarVal][] - [StringType][] - [StringVal][] @@ -118,6 +118,7 @@ In this page you will find a list of validators by their category. - [FloatType][] - `v::floatType()->assert(1.5);` - [FloatVal][] - `v::floatVal()->assert(1.5);` - [Format][] - `v::format(f::pattern('00-00'))->assert('42-33');` +- [Formatted][] - `v::formatted(f::mask('1-4'), v::email())->assert('foo@example.com');` - [Graph][] - `v::graph()->assert('LKM@#$%4;');` - [GreaterThan][] - `v::greaterThan(10)->assert(11);` - [GreaterThanOrEqual][] - `v::intVal()->greaterThanOrEqual(10)->assert(10);` @@ -275,6 +276,7 @@ In this page you will find a list of validators by their category. [FloatType]: validators/FloatType.md "Validates whether the type of the input is float." [FloatVal]: validators/FloatVal.md "Validate whether the input value is float." [Format]: validators/Format.md "Validates whether an input is already formatted as the result of applying a provided" +[Formatted]: validators/Formatted.md "Decorates a validator to format input values in error messages while still validating the original input." [Graph]: validators/Graph.md "Validates if all characters in the input are printable and actually creates" [GreaterThan]: validators/GreaterThan.md "Validates whether the input is greater than a value." [GreaterThanOrEqual]: validators/GreaterThanOrEqual.md "Validates whether the input is greater than or equal to a value." diff --git a/docs/validators/Formatted.md b/docs/validators/Formatted.md new file mode 100644 index 000000000..7e37832f5 --- /dev/null +++ b/docs/validators/Formatted.md @@ -0,0 +1,52 @@ + + +# Formatted + +- `Formatted(Formatter $formatter, Validator $validator)` + +Decorates a validator to format input values in error messages while still validating the original input. + +```php +use Respect\StringFormatter\FormatterBuilder as f; + +v::formatted(f::mask('1-4'), v::email())->assert('foo@example.com'); +// Validation passes successfully + +v::formatted(f::mask('1-4'), v::email())->assert('not an email'); +// → "****an email" must be an email address + +v::formatted(f::pattern('#### #### #### ####'), v::creditCard())->assert('4111111111111111'); +// Validation passes successfully + +v::formatted(f::pattern('#### #### #### ####'), v::creditCard())->assert('1234123412341234'); +// → "1234 1234 1234 1234" must be a credit card number +``` + +This validator is useful for displaying formatted values in error messages, making them more readable for end users. For example, showing credit card numbers with spaces or phone numbers with proper formatting. + +It uses [respect/string-formatter](https://github.com/Respect/StringFormatter) as the underlying formatting engine. See the [StringFormatter documentation](https://github.com/Respect/StringFormatter) for available formatters. + +## Behavior + +The validator first ensures the input is a valid string using `StringVal`. If the input passes string validation, it validates the original input using the inner validator. The formatted version is only used for display in error messages. + +## Categorization + +- Display +- Transformations + +## Changelog + +| Version | Description | +| ------: | :---------- | +| 3.0.0 | Created | + +## See Also + +- [Masked](Masked.md) +- [Named](Named.md) +- [Templated](Templated.md) diff --git a/src-dev/Commands/LintMixinCommand.php b/src-dev/Commands/LintMixinCommand.php index bf06d91c0..974440045 100644 --- a/src-dev/Commands/LintMixinCommand.php +++ b/src-dev/Commands/LintMixinCommand.php @@ -111,6 +111,7 @@ final class LintMixinCommand extends Command 'PropertyExists', 'PropertyOptional', 'Attributes', + 'Formatted', 'Templated', 'Named', ]; diff --git a/src/Mixins/Builder.php b/src/Mixins/Builder.php index f27b4bc61..506cf0b8d 100644 --- a/src/Mixins/Builder.php +++ b/src/Mixins/Builder.php @@ -141,6 +141,8 @@ public static function floatVal(): Chain; public static function format(Formatter $formatter): Chain; + public static function formatted(Formatter $formatter, Validator $validator): Chain; + public static function graph(string ...$additionalChars): Chain; public static function greaterThan(mixed $compareTo): Chain; diff --git a/src/Mixins/Chain.php b/src/Mixins/Chain.php index 7f1a16e4e..057f2b086 100644 --- a/src/Mixins/Chain.php +++ b/src/Mixins/Chain.php @@ -143,6 +143,8 @@ public function floatVal(): Chain; public function format(Formatter $formatter): Chain; + public function formatted(Formatter $formatter, Validator $validator): Chain; + public function graph(string ...$additionalChars): Chain; public function greaterThan(mixed $compareTo): Chain; diff --git a/src/Mixins/NotBuilder.php b/src/Mixins/NotBuilder.php index 57ceb5124..ca02462fe 100644 --- a/src/Mixins/NotBuilder.php +++ b/src/Mixins/NotBuilder.php @@ -138,6 +138,8 @@ public static function notFloatVal(): Chain; public static function notFormat(Formatter $formatter): Chain; + public static function notFormatted(Formatter $formatter, Validator $validator): Chain; + public static function notGraph(string ...$additionalChars): Chain; public static function notGreaterThan(mixed $compareTo): Chain; diff --git a/src/Mixins/NotChain.php b/src/Mixins/NotChain.php index 686bca591..653bd9b0d 100644 --- a/src/Mixins/NotChain.php +++ b/src/Mixins/NotChain.php @@ -138,6 +138,8 @@ public function notFloatVal(): Chain; public function notFormat(Formatter $formatter): Chain; + public function notFormatted(Formatter $formatter, Validator $validator): Chain; + public function notGraph(string ...$additionalChars): Chain; public function notGreaterThan(mixed $compareTo): Chain; diff --git a/src/Mixins/NullOrBuilder.php b/src/Mixins/NullOrBuilder.php index 7a959b272..5de485680 100644 --- a/src/Mixins/NullOrBuilder.php +++ b/src/Mixins/NullOrBuilder.php @@ -138,6 +138,8 @@ public static function nullOrFloatVal(): Chain; public static function nullOrFormat(Formatter $formatter): Chain; + public static function nullOrFormatted(Formatter $formatter, Validator $validator): Chain; + public static function nullOrGraph(string ...$additionalChars): Chain; public static function nullOrGreaterThan(mixed $compareTo): Chain; diff --git a/src/Mixins/NullOrChain.php b/src/Mixins/NullOrChain.php index 0cb7c16a6..9665f6603 100644 --- a/src/Mixins/NullOrChain.php +++ b/src/Mixins/NullOrChain.php @@ -138,6 +138,8 @@ public function nullOrFloatVal(): Chain; public function nullOrFormat(Formatter $formatter): Chain; + public function nullOrFormatted(Formatter $formatter, Validator $validator): Chain; + public function nullOrGraph(string ...$additionalChars): Chain; public function nullOrGreaterThan(mixed $compareTo): Chain; diff --git a/src/Mixins/UndefOrBuilder.php b/src/Mixins/UndefOrBuilder.php index ac6353e14..b2aede324 100644 --- a/src/Mixins/UndefOrBuilder.php +++ b/src/Mixins/UndefOrBuilder.php @@ -136,6 +136,8 @@ public static function undefOrFloatVal(): Chain; public static function undefOrFormat(Formatter $formatter): Chain; + public static function undefOrFormatted(Formatter $formatter, Validator $validator): Chain; + public static function undefOrGraph(string ...$additionalChars): Chain; public static function undefOrGreaterThan(mixed $compareTo): Chain; diff --git a/src/Mixins/UndefOrChain.php b/src/Mixins/UndefOrChain.php index 952691fd4..1864ba053 100644 --- a/src/Mixins/UndefOrChain.php +++ b/src/Mixins/UndefOrChain.php @@ -136,6 +136,8 @@ public function undefOrFloatVal(): Chain; public function undefOrFormat(Formatter $formatter): Chain; + public function undefOrFormatted(Formatter $formatter, Validator $validator): Chain; + public function undefOrGraph(string ...$additionalChars): Chain; public function undefOrGreaterThan(mixed $compareTo): Chain; diff --git a/src/Validators/Formatted.php b/src/Validators/Formatted.php new file mode 100644 index 000000000..0425ca732 --- /dev/null +++ b/src/Validators/Formatted.php @@ -0,0 +1,37 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Validators; + +use Attribute; +use Respect\StringFormatter\Formatter; +use Respect\Validation\Result; +use Respect\Validation\Validator; + +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final readonly class Formatted implements Validator +{ + public function __construct( + private Formatter $formatter, + private Validator $validator, + ) { + } + + public function evaluate(mixed $input): Result + { + $stringVal = new StringVal(); + $stringValResult = $stringVal->evaluate($input); + if (!$stringValResult->hasPassed) { + return $stringValResult->withNameFrom($this->validator)->withIdFrom($this->validator); + } + + return $this->validator->evaluate($input)->withInput($this->formatter->format((string) $input)); + } +} diff --git a/tests/feature/Validators/FormattedTest.php b/tests/feature/Validators/FormattedTest.php new file mode 100644 index 000000000..b31fa11e4 --- /dev/null +++ b/tests/feature/Validators/FormattedTest.php @@ -0,0 +1,35 @@ + + */ + +declare(strict_types=1); + +use Respect\StringFormatter\FormatterBuilder as f; + +test('input is not a string', catchAll( + fn() => v::formatted(f::mask('1-'), v::alnum())->assert(new stdClass()), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`stdClass {}` must be a string') + ->and($fullMessage)->toBe('- `stdClass {}` must be a string') + ->and($messages)->toBe(['alnum' => '`stdClass {}` must be a string']), +)); + +test('failed validator with masked input', catchAll( + fn() => v::formatted(f::mask('1-4'), v::email())->assert('not an email'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"****an email" must be an email address') + ->and($fullMessage)->toBe('- "****an email" must be an email address') + ->and($messages)->toBe(['email' => '"****an email" must be an email address']), +)); + +test('failed validator with pattern formatted input', catchAll( + fn() => v::formatted(f::pattern('#### #### #### ####'), v::creditCard())->assert('1234123412341234'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"1234 1234 1234 1234" must be a credit card number') + ->and($fullMessage)->toBe('- "1234 1234 1234 1234" must be a credit card number') + ->and($messages)->toBe(['creditCard' => '"1234 1234 1234 1234" must be a credit card number']), +)); diff --git a/tests/src/Formatters/FormatterStub.php b/tests/src/Formatters/FormatterStub.php new file mode 100644 index 000000000..5f171e4b0 --- /dev/null +++ b/tests/src/Formatters/FormatterStub.php @@ -0,0 +1,26 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Test\Formatters; + +use Respect\StringFormatter\Formatter; + +final readonly class FormatterStub implements Formatter +{ + public function __construct( + private string $formattedValue, + ) { + } + + public function format(string $input): string + { + return $this->formattedValue; + } +} diff --git a/tests/src/SmokeTestProvider.php b/tests/src/SmokeTestProvider.php index 212aae12b..0c5fe48df 100644 --- a/tests/src/SmokeTestProvider.php +++ b/tests/src/SmokeTestProvider.php @@ -84,6 +84,7 @@ public static function provideValidatorInput(): Generator yield 'FloatType' => [new vs\FloatType(), 1.23]; yield 'FloatVal' => [new vs\FloatVal(), 1.23]; yield 'Format' => [new vs\Format(new fo\PatternFormatter('0-0')), '1-2']; + yield 'Formatted' => [new vs\Formatted(new fo\MaskFormatter('1-'), new vs\StringType()), 'unmasked']; yield 'Graph' => [new vs\Graph(), 'abc123!@#']; yield 'GreaterThan' => [new vs\GreaterThan(0), 1]; yield 'GreaterThanOrEqual' => [new vs\GreaterThanOrEqual(1), 1]; diff --git a/tests/unit/Validators/FormattedTest.php b/tests/unit/Validators/FormattedTest.php new file mode 100644 index 000000000..d725fff37 --- /dev/null +++ b/tests/unit/Validators/FormattedTest.php @@ -0,0 +1,66 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Validators; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use Respect\Validation\Test\Formatters\FormatterStub; +use Respect\Validation\Test\TestCase; +use Respect\Validation\Test\Validators\Stub; + +#[CoversClass(Formatted::class)] +final class FormattedTest extends TestCase +{ + #[Test] + #[DataProvider('providerForNonStringValues')] + public function shouldNotValidateWhenInputIsNotStringValue(mixed $input): void + { + $this->assertInvalidInput(new Formatted(new FormatterStub('any'), Stub::any(1)), $input); + } + + #[Test] + #[DataProvider('providerForStringValues')] + public function shouldFormatTheInputWhenInputIsStringValue(mixed $input): void + { + $formattedValue = 'formatted-value'; + $formatter = new FormatterStub($formattedValue); + + $stub = Stub::pass(2); + $comparableResult = $stub->evaluate($input); + + $validator = new Formatted($formatter, $stub); + + $result = $validator->evaluate($input); + + self::assertSame($formattedValue, $result->input); + self::assertSame($comparableResult->hasPassed, $result->hasPassed); + self::assertSame($comparableResult->validator, $result->validator); + } + + #[Test] + public function shouldPassValidationWhenInnerValidatorPasses(): void + { + $formatter = new FormatterStub('formatted'); + $validator = new Formatted($formatter, Stub::pass(1)); + + $this->assertValidInput($validator, 'any-string'); + } + + #[Test] + public function shouldFailValidationWhenInnerValidatorFails(): void + { + $formatter = new FormatterStub('formatted'); + $validator = new Formatted($formatter, Stub::fail(1)); + + $this->assertInvalidInput($validator, 'any-string'); + } +} From 6abf3a548cf065a057af116fae7d6ab2175b8fef Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Fri, 6 Feb 2026 20:25:42 +0100 Subject: [PATCH 2/2] Remove Masked validator in favor of Formatted The Masked validator was a proxy for what the new Formatted validator already does, so it is being removed to reduce redundancy. All tests and documentation have been updated accordingly. Assisted-by: OpenCode (ollama-cloud/glm-4.7) --- docs/migrating-from-v2-to-v3.md | 13 ------ docs/validators.md | 6 +-- docs/validators/Format.md | 1 - docs/validators/Formatted.md | 1 - docs/validators/Masked.md | 50 --------------------- docs/validators/Named.md | 1 - docs/validators/Templated.md | 1 - src/Mixins/AllBuilder.php | 2 - src/Mixins/AllChain.php | 2 - src/Mixins/Builder.php | 2 - src/Mixins/Chain.php | 2 - src/Mixins/KeyBuilder.php | 2 - src/Mixins/KeyChain.php | 2 - src/Mixins/NotBuilder.php | 2 - src/Mixins/NotChain.php | 2 - src/Mixins/NullOrBuilder.php | 2 - src/Mixins/NullOrChain.php | 2 - src/Mixins/PropertyBuilder.php | 2 - src/Mixins/PropertyChain.php | 2 - src/Mixins/UndefOrBuilder.php | 2 - src/Mixins/UndefOrChain.php | 2 - src/Validators/Masked.php | 47 -------------------- tests/feature/Validators/MaskedTest.php | 25 ----------- tests/src/SmokeTestProvider.php | 1 - tests/unit/Validators/MaskedTest.php | 58 ------------------------- 25 files changed, 2 insertions(+), 230 deletions(-) delete mode 100644 docs/validators/Masked.md delete mode 100644 src/Validators/Masked.php delete mode 100644 tests/feature/Validators/MaskedTest.php delete mode 100644 tests/unit/Validators/MaskedTest.php diff --git a/docs/migrating-from-v2-to-v3.md b/docs/migrating-from-v2-to-v3.md index 8159ba68a..3ac05e504 100644 --- a/docs/migrating-from-v2-to-v3.md +++ b/docs/migrating-from-v2-to-v3.md @@ -591,7 +591,6 @@ Version 3.0 introduces several new validators: | `KeyExists` | Checks if an array key exists | | `KeyOptional` | Validates an array key only if it exists | | `Factory` | Creates validators dynamically based on input | -| `Masked` | Masks sensitive input values in error messages | | `Named` | Customizes the subject name in error messages | | `PropertyExists` | Checks if an object property exists | | `PropertyOptional` | Validates an object property only if it exists | @@ -732,18 +731,6 @@ v::factory( )->assert(['password' => 'secret', 'confirmation' => 'secret']); // passes ``` -#### Masked - -Decorates a validator to mask sensitive input values in error messages while still validating the original unmasked data. This allows applications to protect sensitive information like passwords, credit cards, or emails without implementing a custom layer between Validation and end users: - -```php -v::masked('1-@', v::email(),v::email())->assert('invalid@example.com'); -// → "*******@example.com" must be a valid email address - -v::masked('6-12', v::creditCard(), 'X')->assert('4111111111111211'); -// → "41111XXXXXXX1211" must be a valid credit card number -``` - #### Named Customizes the subject name in error messages: diff --git a/docs/validators.md b/docs/validators.md index 6c67cad71..958731a6d 100644 --- a/docs/validators.md +++ b/docs/validators.md @@ -27,7 +27,7 @@ In this page you will find a list of validators by their category. **Date and Time**: [Date][] - [DateTime][] - [DateTimeDiff][] - [LeapDate][] - [LeapYear][] - [Time][] -**Display**: [Format][] - [Formatted][] - [Masked][] - [Named][] - [Templated][] +**Display**: [Format][] - [Formatted][] - [Named][] - [Templated][] **File system**: [Directory][] - [Executable][] - [Exists][] - [Extension][] - [File][] - [Image][] - [Mimetype][] - [Readable][] - [Size][] - [SymbolicLink][] - [Writable][] @@ -41,7 +41,7 @@ In this page you will find a list of validators by their category. **Math**: [Factor][] - [Finite][] - [Infinite][] - [Multiple][] - [Negative][] - [Positive][] -**Miscellaneous**: [Blank][] - [Falsy][] - [Masked][] - [Named][] - [Templated][] - [Undef][] +**Miscellaneous**: [Blank][] - [Falsy][] - [Named][] - [Templated][] - [Undef][] **Nesting**: [After][] - [AllOf][] - [AnyOf][] - [Each][] - [Factory][] - [Key][] - [KeySet][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [ShortCircuit][] - [UndefOr][] - [When][] @@ -151,7 +151,6 @@ In this page you will find a list of validators by their category. - [Lowercase][] - `v::stringType()->lowercase()->assert('xkcd');` - [Luhn][] - `v::luhn()->assert('2222400041240011');` - [MacAddress][] - `v::macAddress()->assert('00:11:22:33:44:55');` -- [Masked][] - `v::masked('1-@', v::email())->assert('foo@example.com');` - [Max][] - `v::max(v::equals(30))->assert([10, 20, 30]);` - [Mimetype][] - `v::mimetype('image/png')->assert('/path/to/image.png');` - [Min][] - `v::min(v::equals(10))->assert([10, 20, 30]);` @@ -309,7 +308,6 @@ In this page you will find a list of validators by their category. [Lowercase]: validators/Lowercase.md "Validates whether the characters in the input are lowercase." [Luhn]: validators/Luhn.md "Validate whether a given input is a Luhn number." [MacAddress]: validators/MacAddress.md "Validates whether the input is a valid MAC address." -[Masked]: validators/Masked.md "Decorates a validator to mask input values in error messages while still validating the original unmasked input." [Max]: validators/Max.md "Validates the maximum value of the input against a given validator." [Mimetype]: validators/Mimetype.md "Validates if the input is a file and if its MIME type matches the expected one." [Min]: validators/Min.md "Validates the minimum value of the input against a given validator." diff --git a/docs/validators/Format.md b/docs/validators/Format.md index e7c457adc..581d06753 100644 --- a/docs/validators/Format.md +++ b/docs/validators/Format.md @@ -62,7 +62,6 @@ stored data follows a required display format). ## See Also -- [Masked](Masked.md) - [Templated](Templated.md) [Respect\StringFormatter]: https://github.com/Respect/StringFormatter diff --git a/docs/validators/Formatted.md b/docs/validators/Formatted.md index 7e37832f5..e239f3a11 100644 --- a/docs/validators/Formatted.md +++ b/docs/validators/Formatted.md @@ -47,6 +47,5 @@ The validator first ensures the input is a valid string using `StringVal`. If th ## See Also -- [Masked](Masked.md) - [Named](Named.md) - [Templated](Templated.md) diff --git a/docs/validators/Masked.md b/docs/validators/Masked.md deleted file mode 100644 index 93a0cce60..000000000 --- a/docs/validators/Masked.md +++ /dev/null @@ -1,50 +0,0 @@ - - -# Masked - -- `Masked(string $range, Validator $validator)` -- `Masked(string $range, Validator $validator, string $replacement)` - -Decorates a validator to mask input values in error messages while still validating the original unmasked input. - -```php -v::masked('1-@', v::email())->assert('foo@example.com'); -// Validation passes successfully - -v::masked('1-@', v::email())->assert('invalid username@domain.com'); -// → "****************@domain.com" must be an email address - -v::masked('1-', v::lengthGreaterThan(10))->assert('password'); -// → The length of "********" must be greater than 10 - -v::masked('6-12', v::creditCard(), 'X')->assert('4111111111111211'); -// → "41111XXXXXXX1211" must be a credit card number -``` - -This validator is useful for security-sensitive applications where error messages should not expose sensitive data like credit card numbers, passwords, or email addresses. - -It uses [respect/string-formatter](https://github.com/Respect/StringFormatter) as the underlying masking engine. See the section the documentation of [MaskFormatter](https://github.com/Respect/StringFormatter/blob/main/docs/MaskFormatter.md) for more information. - -## Categorization - -- Display -- Miscellaneous - -## Behavior - -The validator first ensures the input is a valid string using `StringVal`. If the input passes string validation, it validates the original unmasked input using the inner validator. If validation fails, it applies masking to the input value shown in error messages. - -## Changelog - -| Version | Description | -| ------: | :---------- | -| 3.0.0 | Created | - -## See Also - -- [Named](Named.md) -- [Templated](Templated.md) diff --git a/docs/validators/Named.md b/docs/validators/Named.md index 134a454c4..3f3ad5271 100644 --- a/docs/validators/Named.md +++ b/docs/validators/Named.md @@ -53,6 +53,5 @@ This validator does not have any templates, as it will use the template of the g ## See Also - [Attributes](Attributes.md) -- [Masked](Masked.md) - [Not](Not.md) - [Templated](Templated.md) diff --git a/docs/validators/Templated.md b/docs/validators/Templated.md index 466ffe876..fbed973cf 100644 --- a/docs/validators/Templated.md +++ b/docs/validators/Templated.md @@ -54,6 +54,5 @@ This validator does not have any templates, as you must define the templates you ## See Also - [Attributes](Attributes.md) -- [Masked](Masked.md) - [Named](Named.md) - [Not](Not.md) diff --git a/src/Mixins/AllBuilder.php b/src/Mixins/AllBuilder.php index ae2e0adbc..a455969a3 100644 --- a/src/Mixins/AllBuilder.php +++ b/src/Mixins/AllBuilder.php @@ -192,8 +192,6 @@ public static function allLuhn(): Chain; public static function allMacAddress(): Chain; - public static function allMasked(string $range, Validator $validator, string $replacement = '*'): Chain; - public static function allMax(Validator $validator): Chain; public static function allMimetype(string $mimetype): Chain; diff --git a/src/Mixins/AllChain.php b/src/Mixins/AllChain.php index 515042265..aef201642 100644 --- a/src/Mixins/AllChain.php +++ b/src/Mixins/AllChain.php @@ -192,8 +192,6 @@ public function allLuhn(): Chain; public function allMacAddress(): Chain; - public function allMasked(string $range, Validator $validator, string $replacement = '*'): Chain; - public function allMax(Validator $validator): Chain; public function allMimetype(string $mimetype): Chain; diff --git a/src/Mixins/Builder.php b/src/Mixins/Builder.php index 506cf0b8d..354c339fb 100644 --- a/src/Mixins/Builder.php +++ b/src/Mixins/Builder.php @@ -209,8 +209,6 @@ public static function luhn(): Chain; public static function macAddress(): Chain; - public static function masked(string $range, Validator $validator, string $replacement = '*'): Chain; - public static function max(Validator $validator): Chain; public static function mimetype(string $mimetype): Chain; diff --git a/src/Mixins/Chain.php b/src/Mixins/Chain.php index 057f2b086..0a4868896 100644 --- a/src/Mixins/Chain.php +++ b/src/Mixins/Chain.php @@ -211,8 +211,6 @@ public function luhn(): Chain; public function macAddress(): Chain; - public function masked(string $range, Validator $validator, string $replacement = '*'): Chain; - public function max(Validator $validator): Chain; public function mimetype(string $mimetype): Chain; diff --git a/src/Mixins/KeyBuilder.php b/src/Mixins/KeyBuilder.php index 4c1c1d02c..bb6f7187e 100644 --- a/src/Mixins/KeyBuilder.php +++ b/src/Mixins/KeyBuilder.php @@ -194,8 +194,6 @@ public static function keyLuhn(int|string $key): Chain; public static function keyMacAddress(int|string $key): Chain; - public static function keyMasked(int|string $key, string $range, Validator $validator, string $replacement = '*'): Chain; - public static function keyMax(int|string $key, Validator $validator): Chain; public static function keyMimetype(int|string $key, string $mimetype): Chain; diff --git a/src/Mixins/KeyChain.php b/src/Mixins/KeyChain.php index 933cc7602..f29ea57f2 100644 --- a/src/Mixins/KeyChain.php +++ b/src/Mixins/KeyChain.php @@ -194,8 +194,6 @@ public function keyLuhn(int|string $key): Chain; public function keyMacAddress(int|string $key): Chain; - public function keyMasked(int|string $key, string $range, Validator $validator, string $replacement = '*'): Chain; - public function keyMax(int|string $key, Validator $validator): Chain; public function keyMimetype(int|string $key, string $mimetype): Chain; diff --git a/src/Mixins/NotBuilder.php b/src/Mixins/NotBuilder.php index ca02462fe..bcf83744d 100644 --- a/src/Mixins/NotBuilder.php +++ b/src/Mixins/NotBuilder.php @@ -206,8 +206,6 @@ public static function notLuhn(): Chain; public static function notMacAddress(): Chain; - public static function notMasked(string $range, Validator $validator, string $replacement = '*'): Chain; - public static function notMax(Validator $validator): Chain; public static function notMimetype(string $mimetype): Chain; diff --git a/src/Mixins/NotChain.php b/src/Mixins/NotChain.php index 653bd9b0d..9a5504134 100644 --- a/src/Mixins/NotChain.php +++ b/src/Mixins/NotChain.php @@ -206,8 +206,6 @@ public function notLuhn(): Chain; public function notMacAddress(): Chain; - public function notMasked(string $range, Validator $validator, string $replacement = '*'): Chain; - public function notMax(Validator $validator): Chain; public function notMimetype(string $mimetype): Chain; diff --git a/src/Mixins/NullOrBuilder.php b/src/Mixins/NullOrBuilder.php index 5de485680..3ac1b0b76 100644 --- a/src/Mixins/NullOrBuilder.php +++ b/src/Mixins/NullOrBuilder.php @@ -206,8 +206,6 @@ public static function nullOrLuhn(): Chain; public static function nullOrMacAddress(): Chain; - public static function nullOrMasked(string $range, Validator $validator, string $replacement = '*'): Chain; - public static function nullOrMax(Validator $validator): Chain; public static function nullOrMimetype(string $mimetype): Chain; diff --git a/src/Mixins/NullOrChain.php b/src/Mixins/NullOrChain.php index 9665f6603..1c5e4ac44 100644 --- a/src/Mixins/NullOrChain.php +++ b/src/Mixins/NullOrChain.php @@ -206,8 +206,6 @@ public function nullOrLuhn(): Chain; public function nullOrMacAddress(): Chain; - public function nullOrMasked(string $range, Validator $validator, string $replacement = '*'): Chain; - public function nullOrMax(Validator $validator): Chain; public function nullOrMimetype(string $mimetype): Chain; diff --git a/src/Mixins/PropertyBuilder.php b/src/Mixins/PropertyBuilder.php index a6ab30e7e..ff523c57b 100644 --- a/src/Mixins/PropertyBuilder.php +++ b/src/Mixins/PropertyBuilder.php @@ -194,8 +194,6 @@ public static function propertyLuhn(string $propertyName): Chain; public static function propertyMacAddress(string $propertyName): Chain; - public static function propertyMasked(string $propertyName, string $range, Validator $validator, string $replacement = '*'): Chain; - public static function propertyMax(string $propertyName, Validator $validator): Chain; public static function propertyMimetype(string $propertyName, string $mimetype): Chain; diff --git a/src/Mixins/PropertyChain.php b/src/Mixins/PropertyChain.php index 384b4e7d9..e06549f85 100644 --- a/src/Mixins/PropertyChain.php +++ b/src/Mixins/PropertyChain.php @@ -194,8 +194,6 @@ public function propertyLuhn(string $propertyName): Chain; public function propertyMacAddress(string $propertyName): Chain; - public function propertyMasked(string $propertyName, string $range, Validator $validator, string $replacement = '*'): Chain; - public function propertyMax(string $propertyName, Validator $validator): Chain; public function propertyMimetype(string $propertyName, string $mimetype): Chain; diff --git a/src/Mixins/UndefOrBuilder.php b/src/Mixins/UndefOrBuilder.php index b2aede324..769ae53f5 100644 --- a/src/Mixins/UndefOrBuilder.php +++ b/src/Mixins/UndefOrBuilder.php @@ -204,8 +204,6 @@ public static function undefOrLuhn(): Chain; public static function undefOrMacAddress(): Chain; - public static function undefOrMasked(string $range, Validator $validator, string $replacement = '*'): Chain; - public static function undefOrMax(Validator $validator): Chain; public static function undefOrMimetype(string $mimetype): Chain; diff --git a/src/Mixins/UndefOrChain.php b/src/Mixins/UndefOrChain.php index 1864ba053..fd9ceb966 100644 --- a/src/Mixins/UndefOrChain.php +++ b/src/Mixins/UndefOrChain.php @@ -204,8 +204,6 @@ public function undefOrLuhn(): Chain; public function undefOrMacAddress(): Chain; - public function undefOrMasked(string $range, Validator $validator, string $replacement = '*'): Chain; - public function undefOrMax(Validator $validator): Chain; public function undefOrMimetype(string $mimetype): Chain; diff --git a/src/Validators/Masked.php b/src/Validators/Masked.php deleted file mode 100644 index a3162c741..000000000 --- a/src/Validators/Masked.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Validation\Validators; - -use Attribute; -use Respect\StringFormatter\InvalidFormatterException; -use Respect\StringFormatter\MaskFormatter; -use Respect\Validation\Exceptions\InvalidValidatorException; -use Respect\Validation\Result; -use Respect\Validation\Validator; - -#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final readonly class Masked implements Validator -{ - private MaskFormatter $maskFormatter; - - public function __construct( - private string $range, - private Validator $validator, - private string $replacement = '*', - ) { - try { - $this->maskFormatter = new MaskFormatter($this->range, $this->replacement); - } catch (InvalidFormatterException $exception) { - throw new InvalidValidatorException($exception->getMessage()); - } - } - - public function evaluate(mixed $input): Result - { - $stringVal = new StringVal(); - $stringValResult = $stringVal->evaluate($input); - if (!$stringValResult->hasPassed) { - return $stringValResult->withNameFrom($this->validator)->withIdFrom($this->validator); - } - - return $this->validator->evaluate($input)->withInput($this->maskFormatter->format((string) $input)); - } -} diff --git a/tests/feature/Validators/MaskedTest.php b/tests/feature/Validators/MaskedTest.php deleted file mode 100644 index 40d1dfb22..000000000 --- a/tests/feature/Validators/MaskedTest.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - -declare(strict_types=1); - -test('input is not a string', catchAll( - fn() => v::masked('1-@', v::email())->assert(new stdClass()), - fn(string $message, string $fullMessage, array $messages) => expect() - ->and($message)->toBe('`stdClass {}` must be a string') - ->and($fullMessage)->toBe('- `stdClass {}` must be a string') - ->and($messages)->toBe(['email' => '`stdClass {}` must be a string']), -)); - -test('failed validator', catchAll( - fn() => v::masked('1-@', v::email())->assert('in valid@email.com'), - fn(string $message, string $fullMessage, array $messages) => expect() - ->and($message)->toBe('"********@email.com" must be an email address') - ->and($fullMessage)->toBe('- "********@email.com" must be an email address') - ->and($messages)->toBe(['email' => '"********@email.com" must be an email address']), -)); diff --git a/tests/src/SmokeTestProvider.php b/tests/src/SmokeTestProvider.php index 0c5fe48df..dbf438f07 100644 --- a/tests/src/SmokeTestProvider.php +++ b/tests/src/SmokeTestProvider.php @@ -118,7 +118,6 @@ public static function provideValidatorInput(): Generator yield 'Lowercase' => [new vs\Lowercase(), 'abc']; yield 'Luhn' => [new vs\Luhn(), '2222400041240011']; yield 'MacAddress' => [new vs\MacAddress(), '00:11:22:33:44:55']; - yield 'Masked' => [new vs\Masked('1-', new vs\IntVal()), 123]; yield 'Max' => [new vs\Max(new vs\Equals(30)), [10, 20, 30]]; yield 'Min' => [new vs\Min(new vs\Equals(10)), [10, 20, 30]]; yield 'Mimetype' => [new vs\Mimetype('image/png'), 'tests/fixtures/valid-image.png']; diff --git a/tests/unit/Validators/MaskedTest.php b/tests/unit/Validators/MaskedTest.php deleted file mode 100644 index 40bd01371..000000000 --- a/tests/unit/Validators/MaskedTest.php +++ /dev/null @@ -1,58 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Validation\Validators; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Test; -use Respect\StringFormatter\MaskFormatter; -use Respect\Validation\Exceptions\InvalidValidatorException; -use Respect\Validation\Test\TestCase; -use Respect\Validation\Test\Validators\Stub; - -#[CoversClass(Masked::class)] -final class MaskedTest extends TestCase -{ - #[Test] - public function shouldNotAllowCreatingValidatorWithAnInvalidRange(): void - { - $range = '0-3'; - - $this->expectException(InvalidValidatorException::class); - - new Masked($range, Stub::daze()); - } - - #[Test] - #[DataProvider('providerForNonStringValues')] - public function shouldNotValidateWhenInputIsNotStringValue(mixed $input): void - { - $this->assertInvalidInput(new Masked('1-', Stub::any(1)), $input); - } - - #[Test] - #[DataProvider('providerForStringValues')] - public function shouldMaskTheInputWhenInputIsStringValue(mixed $input): void - { - $maskFormatter = new MaskFormatter('1-', '*'); - - $stub = Stub::pass(2); - $comparableResult = $stub->evaluate($input); - - $validator = new Masked('1-', $stub); - - $result = $validator->evaluate($input); - - self::assertSame($maskFormatter->format((string) $input), $result->input); - self::assertSame($comparableResult->hasPassed, $result->hasPassed); - self::assertSame($comparableResult->validator, $result->validator); - } -}