Skip to content

Commit 4861e53

Browse files
committed
Create "Masked" validator
The Masked validator decorates other validators to mask sensitive input values in error messages while still validating the original unmasked data. This validator is essential for applications handling sensitive information such as passwords, credit cards, or email addresses. Without it, users would need to implement a custom layer between Validation and the end user to prevent PII from appearing in error messages or logs. With Masked, sensitive data protection is built directly into the validation workflow with no additional abstraction required. Assisted-by: Claude Code (Opus 4.5)
1 parent 640b03c commit 4861e53

26 files changed

Lines changed: 280 additions & 18 deletions

docs/validators.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ In this page you will find a list of validators by their category.
2323

2424
**Core**: [Named][] - [Not][] - [Templated][]
2525

26+
**Cosmetic**: [Masked][] - [Named][] - [Templated][]
27+
2628
**Date and Time**: [Date][] - [DateTime][] - [DateTimeDiff][] - [LeapDate][] - [LeapYear][] - [Time][]
2729

2830
**File system**: [Directory][] - [Executable][] - [Exists][] - [Extension][] - [File][] - [Image][] - [Mimetype][] - [Readable][] - [Size][] - [SymbolicLink][] - [Writable][]
@@ -37,7 +39,7 @@ In this page you will find a list of validators by their category.
3739

3840
**Math**: [Factor][] - [Finite][] - [Infinite][] - [Multiple][] - [Negative][] - [Positive][]
3941

40-
**Miscellaneous**: [Blank][] - [Falsy][] - [Named][] - [Templated][] - [Undef][]
42+
**Miscellaneous**: [Blank][] - [Falsy][] - [Masked][] - [Named][] - [Templated][] - [Undef][]
4143

4244
**Nesting**: [AllOf][] - [AnyOf][] - [Call][] - [Circuit][] - [Each][] - [Key][] - [KeySet][] - [Lazy][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [UndefOr][] - [When][]
4345

@@ -47,7 +49,7 @@ In this page you will find a list of validators by their category.
4749

4850
**Strings**: [Alnum][] - [Alpha][] - [Base64][] - [Charset][] - [Consonant][] - [Contains][] - [ContainsAny][] - [ContainsCount][] - [Control][] - [Digit][] - [Emoji][] - [EndsWith][] - [Graph][] - [HexRgbColor][] - [In][] - [Json][] - [Lowercase][] - [Phone][] - [PostalCode][] - [Printable][] - [Punct][] - [Regex][] - [Slug][] - [Sorted][] - [Space][] - [Spaced][] - [StartsWith][] - [StringType][] - [StringVal][] - [Uppercase][] - [Uuid][] - [Version][] - [Vowel][] - [Xdigit][]
4951

50-
**Structures**: [Attributes][] - [Key][] - [KeyExists][] - [KeyOptional][] - [KeySet][] - [Named][] - [Property][] - [PropertyExists][] - [PropertyOptional][] - [Templated][]
52+
**Structures**: [Attributes][] - [Key][] - [KeyExists][] - [KeyOptional][] - [KeySet][] - [Property][] - [PropertyExists][] - [PropertyOptional][]
5153

5254
**Transformations**: [All][] - [Call][] - [Each][] - [Length][] - [Max][] - [Min][] - [Size][]
5355

@@ -147,6 +149,7 @@ In this page you will find a list of validators by their category.
147149
- [Lowercase][] - `v::stringType()->lowercase()->assert('xkcd');`
148150
- [Luhn][] - `v::luhn()->assert('2222400041240011');`
149151
- [MacAddress][] - `v::macAddress()->assert('00:11:22:33:44:55');`
152+
- [Masked][] - `v::masked(v::email())->assert('foo@example.com');`
150153
- [Max][] - `v::max(v::equals(30))->assert([10, 20, 30]);`
151154
- [Mimetype][] - `v::mimetype('image/png')->assert('/path/to/image.png');`
152155
- [Min][] - `v::min(v::equals(10))->assert([10, 20, 30]);`
@@ -302,6 +305,7 @@ In this page you will find a list of validators by their category.
302305
[Lowercase]: validators/Lowercase.md "Validates whether the characters in the input are lowercase."
303306
[Luhn]: validators/Luhn.md "Validate whether a given input is a Luhn number."
304307
[MacAddress]: validators/MacAddress.md "Validates whether the input is a valid MAC address."
308+
[Masked]: validators/Masked.md "Decorates a validator to mask input values in error messages while still validating the original unmasked input."
305309
[Max]: validators/Max.md "Validates the maximum value of the input against a given validator."
306310
[Mimetype]: validators/Mimetype.md "Validates if the input is a file and if its MIME type matches the expected one."
307311
[Min]: validators/Min.md "Validates the minimum value of the input against a given validator."

docs/validators/Masked.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!--
2+
SPDX-FileCopyrightText: (c) Respect Project Contributors
3+
SPDX-License-Identifier: MIT
4+
-->
5+
6+
# Masked
7+
8+
- `Masked(Validator $validator)`
9+
- `Masked(Validator $validator, string $range)`
10+
- `Masked(Validator $validator, string $range, string $replacement)`
11+
12+
Decorates a validator to mask input values in error messages while still validating the original unmasked input.
13+
14+
```php
15+
v::masked(v::email())->assert('foo@example.com');
16+
// Validation passes successfully
17+
18+
v::masked(v::lengthGreaterThan(10))->assert('password');
19+
// → The length of "********" must be greater than 10
20+
21+
v::masked(v::email(), '1-@')->assert('invalid username@domain.com');
22+
// → "****************@domain.com" must be a valid email address
23+
24+
v::masked(v::creditCard(), '6-12', 'X')->assert('4111111111111211');
25+
// → "41111XXXXXXX1211" must be a valid credit card number
26+
```
27+
28+
This validator is useful for security-sensitive applications where error messages should not expose sensitive data like credit card numbers, passwords, or email addresses.
29+
30+
It uses [request/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.
31+
32+
## Categorization
33+
34+
- Cosmetic
35+
- Miscellaneous
36+
37+
## Behavior
38+
39+
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.
40+
41+
## Important Notes
42+
43+
- **Positions are 1-based**: Position `1` refers to the first character, not position `0`
44+
- **Empty ranges mask everything**: `range = ''` defaults to masking the entire string
45+
- **Range end is exclusive**: `1-@` masks up to (but not including) the `@` character
46+
- **Duplicate positions**: Repeated ranges in the specification are automatically handled
47+
48+
## Changelog
49+
50+
| Version | Description |
51+
| ------: | :---------- |
52+
| 3.0.0 | Created |

docs/validators/Named.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ This validator does not have any templates, as it will use the template of the g
3939
## Categorization
4040

4141
- Core
42-
- Structures
42+
- Cosmetic
4343
- Miscellaneous
4444

4545
## Changelog

docs/validators/Templated.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ This validator does not have any templates, as you must define the templates you
4040
## Categorization
4141

4242
- Core
43-
- Structures
43+
- Cosmetic
4444
- Miscellaneous
4545

4646
## Changelog

src-dev/Commands/LintMixinCommand.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
use function preg_replace;
6464
use function sprintf;
6565
use function str_contains;
66+
use function str_ends_with;
6667
use function str_starts_with;
6768
use function trim;
6869
use function ucfirst;
@@ -348,6 +349,7 @@ private function addParameterToMethod(
348349
$types[] = $type->getName();
349350
if (
350351
str_starts_with($type->getName(), 'Sokil')
352+
|| str_ends_with($type->getName(), 'TextMasker')
351353
|| str_starts_with($type->getName(), 'Egulias')
352354
|| $type->getName() === 'finfo'
353355
) {

src-dev/Markdown/Linters/ValidatorHeaderLinter.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ private function getParameter(ReflectionParameter $reflection): array|null
172172
} elseif ($type instanceof ReflectionNamedType) {
173173
if (
174174
str_starts_with($type->getName(), 'Sokil')
175+
|| str_ends_with($type->getName(), 'TextMasker')
175176
|| str_starts_with($type->getName(), 'Egulias')
176177
|| $type->getName() === 'finfo'
177178
) {

src/Mixins/AllBuilder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ public static function allLuhn(): Chain;
193193

194194
public static function allMacAddress(): Chain;
195195

196+
public static function allMasked(Validator $validator, string $range = '1-', string $replacement = '*'): Chain;
197+
196198
public static function allMax(Validator $validator): Chain;
197199

198200
public static function allMimetype(string $mimetype): Chain;

src/Mixins/AllChain.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ public function allLuhn(): Chain;
193193

194194
public function allMacAddress(): Chain;
195195

196+
public function allMasked(Validator $validator, string $range = '1-', string $replacement = '*'): Chain;
197+
196198
public function allMax(Validator $validator): Chain;
197199

198200
public function allMimetype(string $mimetype): Chain;

src/Mixins/Builder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ public static function luhn(): Chain;
208208

209209
public static function macAddress(): Chain;
210210

211+
public static function masked(Validator $validator, string $range = '1-', string $replacement = '*'): Chain;
212+
211213
public static function max(Validator $validator): Chain;
212214

213215
public static function mimetype(string $mimetype): Chain;

src/Mixins/Chain.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ public function luhn(): Chain;
210210

211211
public function macAddress(): Chain;
212212

213+
public function masked(Validator $validator, string $range = '1-', string $replacement = '*'): Chain;
214+
213215
public function max(Validator $validator): Chain;
214216

215217
public function mimetype(string $mimetype): Chain;

0 commit comments

Comments
 (0)