Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit ff5ef71

Browse files
authored
Merge pull request #55 from programmatordev/YAPV-40-create-passwordstrength-rule
Create PasswordStrength rule
2 parents 0498dc3 + 990e81e commit ff5ef71

11 files changed

+243
-13
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ Simple usage looks like:
3939
use ProgrammatorDev\Validator\Rule;
4040
use ProgrammatorDev\Validator\Validator;
4141

42-
// do this...
43-
$validator = Validator::notBlank()->greaterThanOrEqual(18);
42+
// do this:
43+
$validator = Validator::type('int')->greaterThanOrEqual(18);
4444

45-
// ...and validate with these:
45+
// and validate with these:
4646
$validator->validate(16); // returns bool: false
4747
$validator->assert(16, 'age'); // throws exception: The age value should be greater than or equal to 18, 16 given.
4848
```

docs/01-get-started.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ Simple usage looks like:
3030
use ProgrammatorDev\Validator\Rule;
3131
use ProgrammatorDev\Validator\Validator;
3232

33-
// do this...
34-
$validator = Validator::notBlank()->greaterThanOrEqual(18);
33+
// do this:
34+
$validator = Validator::type('int')->greaterThanOrEqual(18);
3535

36-
// ...and validate with these:
36+
// and validate with these:
3737
$validator->validate(16); // returns bool: false
3838
$validator->assert(16, 'age'); // throws exception: The age value should be greater than or equal to 18, 16 given.
3939
```

docs/02-usage.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function getWeather(float $latitude, float $longitude, string $unitSystem
2222
{
2323
Validator::range(-90, 90)->assert($latitude, 'latitude');
2424
Validator::range(-180, 180)->assert($longitude, 'longitude');
25-
Validator::notBlank()->choice(['metric', 'imperial'])->assert($unitSystem, 'unit system');
25+
Validator::choice(['metric', 'imperial'])->assert($unitSystem, 'unit system');
2626

2727
// ...
2828
}
@@ -51,7 +51,7 @@ function getWeather(float $latitude, float $longitude, string $unitSystem): floa
5151
{
5252
Validator::range(-90, 90)->assert($latitude, 'latitude');
5353
Validator::range(-180, 180)->assert($longitude, 'longitude');
54-
Validator::notBlank()->choice(['metric', 'imperial'])->assert($unitSystem, 'unit system');
54+
Validator::choice(['metric', 'imperial'])->assert($unitSystem, 'unit system');
5555

5656
// ...
5757
}
@@ -98,7 +98,7 @@ use ProgrammatorDev\Validator\Validator;
9898
try {
9999
Validator::range(-90, 90)->assert($latitude, 'latitude');
100100
Validator::range(-180, 180)->assert($longitude, 'longitude');
101-
Validator::notBlank()->choice(['metric', 'imperial'])->assert($unitSystem, 'unit system');
101+
Validator::choice(['metric', 'imperial'])->assert($unitSystem, 'unit system');
102102
}
103103
catch (Exception\RangeException $exception) {
104104
// do something when Range fails
@@ -120,7 +120,7 @@ use ProgrammatorDev\Validator\Validator;
120120
try {
121121
Validator::range(-90, 90)->assert($latitude, 'latitude');
122122
Validator::range(-180, 180)->assert($longitude, 'longitude');
123-
Validator::notBlank()->choice(['metric', 'imperial'])->assert($unitSystem, 'unit system');
123+
Validator::choice(['metric', 'imperial'])->assert($unitSystem, 'unit system');
124124
}
125125
catch (ValidationException $exception) {
126126
// do something when a rule fails

docs/03-rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
- [Email](03-rules_email.md)
1919
- [Length](03-rules_length.md)
20+
- [PasswordStrength](03-rules_password-strength.md)
2021
- [URL](03-rules_url.md)
2122

2223
## Comparison Rules

docs/03-rules_length.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ Allows to define a `callable` that will be applied to the value before checking
9999
For example, use `trim`, or pass your own function, to not measure whitespaces at the end of a string:
100100

101101
```php
102-
// existing php callables
102+
Validator::length(max: 3)->validate('abc '); // false
103+
103104
Validator::length(max: 3, normalizer: 'trim')->validate('abc '); // true
104-
// function
105-
Validator::length(max: 3, normalizer: fn($value) => trim($value))->validate('abc '); // false
105+
Validator::length(max: 3, normalizer: fn($value) => trim($value))->validate('abc '); // true
106106
```
107107

108108
### `minMessage`

docs/03-rules_password-strength.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# PasswordStrength
2+
3+
Validates that the given password has reached the minimum strength required by the constraint.
4+
The strength is calculated by measuring the entropy of the password (in bits) based on its length and the number of unique characters.
5+
6+
```php
7+
PasswordStrength(
8+
string $minStrength = 'medium',
9+
?string $minMessage = null
10+
);
11+
```
12+
13+
## Basic Usage
14+
15+
```php
16+
Validator::passwordStrength()->validate('password'); // false
17+
Validator::passwordStrength()->validate('i8Kq*MBob~2W"=p'); // true
18+
Validator::passwordStrength(minStrength: 'very-strong')->validate('i8Kq*MBob~2W"=p'); // false
19+
```
20+
21+
> [!NOTE]
22+
> An `UnexpectedValueException` will be thrown when a `minStrength` option is invalid.
23+
24+
> [!NOTE]
25+
> An `UnexpectedValueException` will be thrown when the input value is not a `string`.
26+
27+
## Options
28+
29+
### `minStrength`
30+
31+
type: `string` default: `medium`
32+
33+
Sets the minimum strength of the password in entropy bits.
34+
The entropy is calculated using the formula [here](https://www.pleacher.com/mp/mlessons/algebra/entropy.html).
35+
36+
Available options are:
37+
38+
- `weak` entropy between `64` and `79` bits.
39+
- `medium` entropy between `80` and `95` bits.
40+
- `strong` entropy between `96` and `127` bits.
41+
- `very-strong` entropy greater than `128` bits.
42+
43+
All measurements less than `64` bits will fail.
44+
45+
### `message`
46+
47+
type: `?string` default: `The password strength is not strong enough.`
48+
49+
Message that will be shown when the password is not strong enough.
50+
51+
The following parameters are available:
52+
53+
| Parameter | Description |
54+
|---------------------|---------------------------|
55+
| `{{ name }}` | Name of the invalid value |
56+
| `{{ minStrength }}` | Selected minimum strength |
57+
58+
## Changelog
59+
60+
- `0.8.0` Created

src/ChainedValidatorInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ public function notBlank(
8484
?string $message = null
8585
): ChainedValidatorInterface&Validator;
8686

87+
public function passwordStrength(
88+
string $minStrength = 'medium',
89+
?string $message = null
90+
): ChainedValidatorInterface&Validator;
91+
8792
public function range(
8893
mixed $min,
8994
mixed $max,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\Validator\Exception;
4+
5+
class PasswordStrengthException extends ValidationException {}

src/Rule/PasswordStrength.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\Validator\Rule;
4+
5+
use ProgrammatorDev\Validator\Exception\PasswordStrengthException;
6+
use ProgrammatorDev\Validator\Exception\UnexpectedOptionException;
7+
use ProgrammatorDev\Validator\Exception\UnexpectedTypeException;
8+
9+
class PasswordStrength extends AbstractRule implements RuleInterface
10+
{
11+
private const STRENGTH_VERY_WEAK = 'very-weak';
12+
public const STRENGTH_WEAK = 'weak';
13+
public const STRENGTH_MEDIUM = 'medium';
14+
public const STRENGTH_STRONG = 'strong';
15+
public const STRENGTH_VERY_STRONG = 'very-strong';
16+
17+
private const STRENGTH_OPTIONS = [
18+
self::STRENGTH_WEAK,
19+
self::STRENGTH_MEDIUM,
20+
self::STRENGTH_STRONG,
21+
self::STRENGTH_VERY_STRONG
22+
];
23+
24+
private const STRENGTH_SCORE = [
25+
self::STRENGTH_VERY_WEAK => 0,
26+
self::STRENGTH_WEAK => 1,
27+
self::STRENGTH_MEDIUM => 2,
28+
self::STRENGTH_STRONG => 3,
29+
self::STRENGTH_VERY_STRONG => 4
30+
];
31+
32+
private string $message = 'The password strength is not strong enough.';
33+
34+
public function __construct(
35+
private readonly string $minStrength = self::STRENGTH_MEDIUM,
36+
?string $message = null
37+
)
38+
{
39+
$this->message = $message ?? $this->message;
40+
}
41+
42+
public function assert(#[\SensitiveParameter] mixed $value, ?string $name = null): void
43+
{
44+
if (!\in_array($this->minStrength, self::STRENGTH_OPTIONS)) {
45+
throw new UnexpectedOptionException('minStrength', self::STRENGTH_OPTIONS, $this->minStrength);
46+
}
47+
48+
if (!\is_string($value)) {
49+
throw new UnexpectedTypeException('string', get_debug_type($value));
50+
}
51+
52+
$minScore = self::STRENGTH_SCORE[$this->minStrength];
53+
$score = self::STRENGTH_SCORE[$this->calcStrength($value)];
54+
55+
if ($minScore > $score) {
56+
throw new PasswordStrengthException(
57+
message: $this->message,
58+
parameters: [
59+
'name' => $name,
60+
'minStrength' => $this->minStrength
61+
]
62+
);
63+
}
64+
}
65+
66+
private function calcStrength(#[\SensitiveParameter] string $password): string
67+
{
68+
$length = \strlen($password);
69+
$chars = \count_chars($password, 1);
70+
71+
$control = $digit = $upper = $lower = $symbol = $other = 0;
72+
foreach ($chars as $char => $count) {
73+
match (true) {
74+
($char < 32 || $char === 127) => $control = 33,
75+
($char >= 48 && $char <= 57) => $digit = 10,
76+
($char >= 65 && $char <= 90) => $upper = 26,
77+
($char >= 97 && $char <= 122) => $lower = 26,
78+
($char >= 128) => $other = 128,
79+
default => $symbol = 33,
80+
};
81+
}
82+
83+
$pool = $control + $digit + $upper + $lower + $symbol + $other;
84+
$entropy = \log(\pow($pool, $length), 2);
85+
86+
return match (true) {
87+
$entropy >= 128 => self::STRENGTH_VERY_STRONG,
88+
$entropy >= 96 => self::STRENGTH_STRONG,
89+
$entropy >= 80 => self::STRENGTH_MEDIUM,
90+
$entropy >= 64 => self::STRENGTH_WEAK,
91+
default => self::STRENGTH_VERY_WEAK
92+
};
93+
}
94+
}

src/StaticValidatorInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ public static function notBlank(
8383
?string $message = null
8484
): ChainedValidatorInterface&Validator;
8585

86+
public static function passwordStrength(
87+
string $minStrength = 'medium',
88+
?string $message = null
89+
): ChainedValidatorInterface&Validator;
90+
8691
public static function range(
8792
mixed $min,
8893
mixed $max,

0 commit comments

Comments
 (0)