88use PHPStan \PhpDocParser \Ast \Type \ArrayTypeNode ;
99use PHPStan \PhpDocParser \Ast \Type \TypeNode ;
1010use Traversable ;
11+ use TypePHP \Internal \Config ;
1112use TypePHP \Internal \ErrorFactory ;
1213use TypePHP \Internal \ErrorMessage ;
1314use TypePHP \Internal \TypeFormatter ;
1415
1516/**
1617 * Validates array and Traversable collection instances against ArrayTypeNode ASTs (Type[]).
18+ * Supports both exhaustive O(n) verification and Beartype-style hybrid O(1) sampling.
1719 *
1820 * @internal
1921 */
2022final class ArrayValidator implements TypeValidatorInterface
2123{
22- /**
23- * Validates an array or Traversable collection against an ArrayTypeNode (Type[]).
24- * Accepts native arrays and Traversable objects (e.g. ArrayIterator, Symfony RewindableGenerator).
25- * Bypasses eager iteration on Generator instances to prevent premature generator closure.
26- */
24+ private const HYBRID_SAMPLE_THRESHOLD = 64 ;
25+
2726 public function validate (mixed $ value , TypeNode $ node , string $ context , TypeValidatorRegistry $ registry ): ?ErrorMessage
2827 {
2928 if (! \is_array ($ value ) && ! ($ value instanceof Traversable)) {
@@ -37,6 +36,28 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali
3736 return null ;
3837 }
3938
39+ if (\is_array ($ value )) {
40+ $ count = \count ($ value );
41+ if ($ count === 0 ) {
42+ return null ;
43+ }
44+
45+ if ($ count > self ::HYBRID_SAMPLE_THRESHOLD && Config::isArrayValidationHybrid ()) {
46+ return $ this ->validateArrayHybrid ($ value , $ arrayNode , $ context , $ registry , $ count );
47+ }
48+
49+ foreach ($ value as $ k => $ v ) {
50+ $ err = $ registry ->validate ($ v , $ arrayNode ->type , '' );
51+ if ($ err !== null ) {
52+ $ keyStr = (string ) $ k ;
53+
54+ return ErrorFactory::createError ($ context . '[ ' . $ keyStr . '] ' . $ err ->getMessage ());
55+ }
56+ }
57+
58+ return null ;
59+ }
60+
4061 foreach ($ value as $ k => $ v ) {
4162 $ err = $ registry ->validate ($ v , $ arrayNode ->type , '' );
4263 if ($ err !== null ) {
@@ -48,4 +69,50 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali
4869
4970 return null ;
5071 }
51- }
72+
73+ /**
74+ * @param array<mixed> $value
75+ */
76+ private function validateArrayHybrid (
77+ array $ value ,
78+ ArrayTypeNode $ arrayNode ,
79+ string $ context ,
80+ TypeValidatorRegistry $ registry ,
81+ int $ count
82+ ): ?ErrorMessage {
83+ if (array_is_list ($ value )) {
84+ $ sampleIndices = [0 , $ count - 1 ];
85+ $ samplesToTake = min (3 , $ count - 2 );
86+ for ($ i = 0 ; $ i < $ samplesToTake ; $ i ++) {
87+ $ sampleIndices [] = mt_rand (1 , $ count - 2 );
88+ }
89+
90+ foreach ($ sampleIndices as $ idx ) {
91+ $ err = $ registry ->validate ($ value [$ idx ], $ arrayNode ->type , '' );
92+ if ($ err !== null ) {
93+ return ErrorFactory::createError ($ context . '[ ' . $ idx . '] ' . $ err ->getMessage ());
94+ }
95+ }
96+
97+ return null ;
98+ }
99+
100+ $ keys = array_keys ($ value );
101+ $ sampleKeys = [$ keys [0 ], $ keys [$ count - 1 ]];
102+ $ samplesToTake = min (3 , $ count - 2 );
103+ for ($ i = 0 ; $ i < $ samplesToTake ; $ i ++) {
104+ $ sampleKeys [] = $ keys [mt_rand (1 , $ count - 2 )];
105+ }
106+
107+ foreach ($ sampleKeys as $ k ) {
108+ $ err = $ registry ->validate ($ value [$ k ], $ arrayNode ->type , '' );
109+ if ($ err !== null ) {
110+ $ keyStr = (string ) $ k ;
111+
112+ return ErrorFactory::createError ($ context . '[ ' . $ keyStr . '] ' . $ err ->getMessage ());
113+ }
114+ }
115+
116+ return null ;
117+ }
118+ }
0 commit comments