1414namespace CodeIgniter \DataCaster ;
1515
1616use CodeIgniter \Entity \Exceptions \CastException ;
17+ use CodeIgniter \Exceptions \InvalidArgumentException ;
1718use CodeIgniter \Test \CIUnitTestCase ;
1819use PHPUnit \Framework \Attributes \Group ;
20+ use stdClass ;
1921
2022/**
2123 * @internal
@@ -31,4 +33,141 @@ public function testCastInvalidMethodException(): void
3133 $ dataCaster = new DataCaster ();
3234 $ dataCaster ->castAs ([], 'name ' , 'add ' );
3335 }
36+
37+ public function testCastAsWithUndefinedFieldReturnsOriginalValue (): void
38+ {
39+ $ dataCaster = new DataCaster ();
40+ $ value = 'unregistered_field_value ' ;
41+
42+ $ this ->assertSame ($ value , $ dataCaster ->castAs ($ value , 'undefined_field ' ));
43+ }
44+
45+ public function testCastAsCachesCompiledTypesAcrossRepeatedCalls (): void
46+ {
47+ $ types = [
48+ 'id ' => 'int ' ,
49+ 'is_admin ' => 'bool ' ,
50+ 'tags ' => 'csv ' ,
51+ 'options ' => 'json[array] ' ,
52+ 'bio ' => '?string ' ,
53+ ];
54+
55+ $ dataCaster = new DataCaster (null , $ types );
56+
57+ // First call compiles rules
58+ $ this ->assertSame (42 , $ dataCaster ->castAs ('42 ' , 'id ' ));
59+ $ this ->assertTrue ($ dataCaster ->castAs ('1 ' , 'is_admin ' ));
60+ $ this ->assertSame (['news ' , 'tech ' ], $ dataCaster ->castAs ('news,tech ' , 'tags ' ));
61+ $ this ->assertSame (['theme ' => 'dark ' ], $ dataCaster ->castAs ('{"theme":"dark"} ' , 'options ' ));
62+
63+ // Subsequent calls use compiled cache
64+ $ this ->assertSame (100 , $ dataCaster ->castAs ('100 ' , 'id ' ));
65+ $ this ->assertFalse ($ dataCaster ->castAs ('0 ' , 'is_admin ' ));
66+ $ this ->assertSame (['php ' ], $ dataCaster ->castAs ('php ' , 'tags ' ));
67+ $ this ->assertSame (['lang ' => 'en ' ], $ dataCaster ->castAs ('{"lang":"en"} ' , 'options ' ));
68+ }
69+
70+ public function testCompiledTypeCacheIsSharedByType (): void
71+ {
72+ $ dataCaster = new DataCaster (null , [
73+ 'first ' => 'int ' ,
74+ 'second ' => 'int ' ,
75+ 'third ' => 'bool ' ,
76+ ]);
77+
78+ $ dataCaster ->castAs ('1 ' , 'first ' );
79+ $ dataCaster ->castAs ('2 ' , 'second ' );
80+ $ dataCaster ->castAs ('1 ' , 'third ' );
81+
82+ $ compiledTypes = $ this ->getPrivateProperty ($ dataCaster , 'compiledTypes ' );
83+
84+ $ this ->assertIsArray ($ compiledTypes );
85+ $ this ->assertCount (2 , $ compiledTypes );
86+ }
87+
88+ public function testCastAsNullableHandling (): void
89+ {
90+ $ types = [
91+ 'nullable_field ' => '?int ' ,
92+ 'strict_field ' => 'int ' ,
93+ ];
94+
95+ $ dataCaster = new DataCaster (null , $ types , null , true );
96+
97+ $ this ->assertNull ($ dataCaster ->castAs (null , 'nullable_field ' ));
98+ $ this ->assertSame (123 , $ dataCaster ->castAs ('123 ' , 'nullable_field ' ));
99+
100+ $ this ->expectException (InvalidArgumentException::class);
101+ $ this ->expectExceptionMessage ('Field "strict_field" is not nullable, but null was passed. ' );
102+ $ dataCaster ->castAs (null , 'strict_field ' );
103+ }
104+
105+ public function testCastAsNullableWithUnknownTypeReturnsNull (): void
106+ {
107+ $ dataCaster = new DataCaster (null , ['ghost ' => '?unknown_handler ' ]);
108+
109+ // Null value for nullable field returns null even if handler is not defined
110+ $ this ->assertNull ($ dataCaster ->castAs (null , 'ghost ' ));
111+
112+ // Non-null value triggers missing handler exception
113+ $ this ->expectException (InvalidArgumentException::class);
114+ $ this ->expectExceptionMessage ('No such handler for "ghost". Invalid type: unknown_handler ' );
115+ $ dataCaster ->castAs ('foo ' , 'ghost ' );
116+ }
117+
118+ public function testCastAsNotNullableStrictThrowsBeforeCheckingHandler (): void
119+ {
120+ $ dataCaster = new DataCaster (null , ['ghost ' => 'unknown_handler ' ], null , true );
121+
122+ // Strict mode throws not nullable exception first
123+ $ this ->expectException (InvalidArgumentException::class);
124+ $ this ->expectExceptionMessage ('Field "ghost" is not nullable, but null was passed. ' );
125+ $ dataCaster ->castAs (null , 'ghost ' );
126+ }
127+
128+ public function testSetTypesInvalidatesCompiledCache (): void
129+ {
130+ $ dataCaster = new DataCaster (null , ['data ' => 'int ' ]);
131+
132+ $ this ->assertSame (5 , $ dataCaster ->castAs ('5 ' , 'data ' ));
133+ $ this ->assertArrayHasKey ('int ' , $ this ->getPrivateProperty ($ dataCaster , 'compiledTypes ' ));
134+
135+ // Change type to csv
136+ $ dataCaster ->setTypes (['data ' => 'csv ' ]);
137+ $ this ->assertSame ([], $ this ->getPrivateProperty ($ dataCaster , 'compiledTypes ' ));
138+ $ this ->assertSame (['5 ' ], $ dataCaster ->castAs ('5 ' , 'data ' ));
139+
140+ $ compiledTypes = $ this ->getPrivateProperty ($ dataCaster , 'compiledTypes ' );
141+ $ this ->assertArrayHasKey ('csv ' , $ compiledTypes );
142+
143+ // Calling setTypes with identical types preserves cached compilation
144+ $ dataCaster ->setTypes (['data ' => 'csv ' ]);
145+ $ this ->assertSame ($ compiledTypes , $ this ->getPrivateProperty ($ dataCaster , 'compiledTypes ' ));
146+ $ this ->assertSame (['a ' , 'b ' ], $ dataCaster ->castAs ('a,b ' , 'data ' ));
147+ }
148+
149+ public function testCastAsNoSuchHandlerException (): void
150+ {
151+ $ dataCaster = new DataCaster (null , ['name ' => 'non_existent_type ' ]);
152+
153+ $ this ->expectException (InvalidArgumentException::class);
154+ $ this ->expectExceptionMessage ('No such handler for "name". Invalid type: non_existent_type ' );
155+
156+ $ dataCaster ->castAs ('value ' , 'name ' );
157+ }
158+
159+ public function testCastAsInvalidInterfaceExceptionWhenNotStrict (): void
160+ {
161+ /** @phpstan-var mixed $castHandlers */
162+ $ castHandlers = [
163+ 'invalid ' => stdClass::class,
164+ ];
165+
166+ $ dataCaster = new DataCaster ($ castHandlers , ['field ' => 'invalid ' ], null , false );
167+
168+ $ this ->expectException (CastException::class);
169+ $ this ->expectExceptionMessage (CastException ::forInvalidInterface (stdClass::class)->getMessage ());
170+
171+ $ dataCaster ->castAs ('value ' , 'field ' );
172+ }
34173}
0 commit comments