1+ <?php
2+
3+ namespace ProgrammatorDev \Validator \Rule ;
4+
5+ use ProgrammatorDev \Validator \Exception \LengthException ;
6+ use ProgrammatorDev \Validator \Exception \UnexpectedOptionException ;
7+ use ProgrammatorDev \Validator \Exception \UnexpectedTypeException ;
8+ use ProgrammatorDev \Validator \Exception \UnexpectedValueException ;
9+ use ProgrammatorDev \Validator \Validator ;
10+
11+ class Length extends AbstractRule implements RuleInterface
12+ {
13+ public const COUNT_UNIT_BYTES = 'bytes ' ;
14+ public const COUNT_UNIT_CODEPOINTS = 'codepoints ' ;
15+ public const COUNT_UNIT_GRAPHEMES = 'graphemes ' ;
16+
17+ private const COUNT_UNITS = [
18+ self ::COUNT_UNIT_BYTES ,
19+ self ::COUNT_UNIT_CODEPOINTS ,
20+ self ::COUNT_UNIT_GRAPHEMES
21+ ];
22+
23+ /** @var ?callable */
24+ private $ normalizer ;
25+ private string $ minMessage = 'The {{ name }} value should have {{ min }} characters or more, {{ numChars }} characters given. ' ;
26+ private string $ maxMessage = 'The {{ name }} value should have {{ max }} characters or less, {{ numChars }} characters given. ' ;
27+ private string $ exactMessage = 'The {{ name }} value should have exactly {{ min }} characters, {{ numChars }} characters given. ' ;
28+ private string $ charsetMessage = 'The {{ name }} value does not match the expected {{ charset }} charset. ' ;
29+
30+ public function __construct (
31+ private readonly ?int $ min = null ,
32+ private readonly ?int $ max = null ,
33+ private readonly string $ charset = 'UTF-8 ' ,
34+ private readonly string $ countUnit = self ::COUNT_UNIT_CODEPOINTS ,
35+ ?callable $ normalizer = null ,
36+ ?string $ minMessage = null ,
37+ ?string $ maxMessage = null ,
38+ ?string $ exactMessage = null ,
39+ ?string $ charsetMessage = null
40+ )
41+ {
42+ $ this ->normalizer = $ normalizer ;
43+ $ this ->minMessage = $ minMessage ?? $ this ->minMessage ;
44+ $ this ->maxMessage = $ maxMessage ?? $ this ->maxMessage ;
45+ $ this ->exactMessage = $ exactMessage ?? $ this ->exactMessage ;
46+ $ this ->charsetMessage = $ charsetMessage ?? $ this ->charsetMessage ;
47+ }
48+
49+ public function assert (mixed $ value , ?string $ name = null ): void
50+ {
51+ if ($ this ->min === null && $ this ->max === null ) {
52+ throw new UnexpectedValueException ('At least one of the options "min" or "max" must be given. ' );
53+ }
54+
55+ if (
56+ $ this ->min !== null
57+ && $ this ->max !== null
58+ && !Validator::greaterThanOrEqual ($ this ->min )->validate ($ this ->max )
59+ ) {
60+ throw new UnexpectedValueException ('Maximum value must be greater than or equal to minimum value. ' );
61+ }
62+
63+ $ encodings = mb_list_encodings ();
64+
65+ if (!\in_array ($ this ->charset , $ encodings )) {
66+ throw new UnexpectedOptionException ('charset ' , $ encodings , $ this ->charset );
67+ }
68+
69+ if (!\in_array ($ this ->countUnit , self ::COUNT_UNITS )) {
70+ throw new UnexpectedOptionException ('countUnit ' , self ::COUNT_UNITS , $ this ->countUnit );
71+ }
72+
73+ if (!\is_scalar ($ value ) && !$ value instanceof \Stringable) {
74+ throw new UnexpectedTypeException ('string|\Stringable ' , get_debug_type ($ value ));
75+ }
76+
77+ $ value = (string ) $ value ;
78+
79+ if ($ this ->normalizer !== null ) {
80+ $ value = ($ this ->normalizer )($ value );
81+ }
82+
83+ if (!mb_detect_encoding ($ value , $ this ->charset )) {
84+ throw new LengthException (
85+ message: $ this ->charsetMessage ,
86+ parameters: [
87+ 'name ' => $ name ,
88+ 'value ' => $ value ,
89+ 'charset ' => $ this ->charset
90+ ]
91+ );
92+ }
93+
94+ $ numChars = match ($ this ->countUnit ) {
95+ self ::COUNT_UNIT_BYTES => \strlen ($ value ),
96+ self ::COUNT_UNIT_CODEPOINTS => \mb_strlen ($ value , $ this ->charset ),
97+ self ::COUNT_UNIT_GRAPHEMES => \grapheme_strlen ($ value ),
98+ };
99+
100+ if ($ this ->min !== null && $ numChars < $ this ->min ) {
101+ $ message = ($ this ->min === $ this ->max ) ? $ this ->exactMessage : $ this ->minMessage ;
102+
103+ throw new LengthException (
104+ message: $ message ,
105+ parameters: [
106+ 'value ' => $ value ,
107+ 'name ' => $ name ,
108+ 'min ' => $ this ->min ,
109+ 'max ' => $ this ->max ,
110+ 'numChars ' => $ numChars ,
111+ 'charset ' => $ this ->charset ,
112+ 'countUnit ' => $ this ->countUnit
113+ ]
114+ );
115+ }
116+
117+ if ($ this ->max !== null && $ numChars > $ this ->max ) {
118+ $ message = ($ this ->min === $ this ->max ) ? $ this ->exactMessage : $ this ->maxMessage ;
119+
120+ throw new LengthException (
121+ message: $ message ,
122+ parameters: [
123+ 'value ' => $ value ,
124+ 'name ' => $ name ,
125+ 'min ' => $ this ->min ,
126+ 'max ' => $ this ->max ,
127+ 'numChars ' => $ numChars ,
128+ 'charset ' => $ this ->charset ,
129+ 'countUnit ' => $ this ->countUnit
130+ ]
131+ );
132+ }
133+ }
134+ }
0 commit comments