1313
1414namespace CodeIgniter \HTTP ;
1515
16+ use BackedEnum ;
17+ use CodeIgniter \Exceptions \InvalidArgumentException ;
1618use CodeIgniter \Exceptions \RuntimeException ;
19+ use CodeIgniter \I18n \Time ;
20+ use DateTimeZone ;
21+ use Exception ;
22+ use ReflectionEnum ;
1723use ReflectionNamedType ;
1824use ReflectionParameter ;
25+ use UnitEnum ;
1926
2027/**
2128 * @see \CodeIgniter\HTTP\FormRequestTest
@@ -199,6 +206,134 @@ public function getValidated(string $key, mixed $default = null): mixed
199206 return dot_array_search ($ key , $ this ->validatedData );
200207 }
201208
209+ /**
210+ * Returns a validated field as an integer.
211+ *
212+ * Supports dot-array syntax for nested validated data.
213+ */
214+ public function integer (string $ key , ?int $ default = null ): ?int
215+ {
216+ $ value = $ this ->getValidated ($ key , $ default );
217+
218+ if ($ value === null || is_int ($ value )) {
219+ return $ value ;
220+ }
221+
222+ if (is_string ($ value )) {
223+ $ integer = filter_var ($ value , FILTER_VALIDATE_INT , FILTER_NULL_ON_FAILURE );
224+
225+ if ($ integer !== null ) {
226+ return $ integer ;
227+ }
228+ }
229+
230+ throw $ this ->invalidValidatedType ($ key , 'integer ' );
231+ }
232+
233+ /**
234+ * Returns a validated field as a boolean.
235+ *
236+ * Supports dot-array syntax for nested validated data.
237+ */
238+ public function boolean (string $ key , ?bool $ default = null ): ?bool
239+ {
240+ $ value = $ this ->getValidated ($ key , $ default );
241+
242+ if ($ value === null || is_bool ($ value )) {
243+ return $ value ;
244+ }
245+
246+ if (is_int ($ value ) || is_string ($ value )) {
247+ $ boolean = filter_var ($ value , FILTER_VALIDATE_BOOLEAN , FILTER_NULL_ON_FAILURE );
248+
249+ if ($ boolean !== null ) {
250+ return $ boolean ;
251+ }
252+ }
253+
254+ throw $ this ->invalidValidatedType ($ key , 'boolean ' );
255+ }
256+
257+ /**
258+ * Returns a validated field as a Time instance.
259+ *
260+ * Supports dot-array syntax for nested validated data.
261+ */
262+ public function date (
263+ string $ key ,
264+ ?string $ format = null ,
265+ DateTimeZone |string |null $ timezone = null ,
266+ ): ?Time {
267+ $ value = $ this ->getValidated ($ key );
268+
269+ if ($ value === null ) {
270+ return null ;
271+ }
272+
273+ if (! is_string ($ value ) || $ value === '' ) {
274+ throw $ this ->invalidValidatedType ($ key , 'date ' );
275+ }
276+
277+ try {
278+ if ($ format === null ) {
279+ return Time::parse ($ value , $ timezone );
280+ }
281+
282+ return Time::createFromFormat ($ format , $ value , $ timezone );
283+ } catch (Exception ) {
284+ throw $ this ->invalidValidatedType ($ key , 'date ' );
285+ }
286+ }
287+
288+ /**
289+ * Returns a validated field as an enum instance.
290+ *
291+ * Supports dot-array syntax for nested validated data.
292+ *
293+ * @template TEnum of UnitEnum
294+ *
295+ * @param class-string<TEnum> $enumClass
296+ * @param TEnum|null $default
297+ *
298+ * @return TEnum|null
299+ */
300+ public function enum (string $ key , string $ enumClass , ?UnitEnum $ default = null ): ?UnitEnum
301+ {
302+ if (! enum_exists ($ enumClass )) {
303+ throw new InvalidArgumentException ('The " ' . $ enumClass . '" class is not a valid enum. ' );
304+ }
305+
306+ $ value = $ this ->getValidated ($ key , $ default );
307+
308+ if ($ value === null ) {
309+ return null ;
310+ }
311+
312+ if ($ value instanceof UnitEnum) {
313+ if ($ value instanceof $ enumClass ) {
314+ return $ value ;
315+ }
316+
317+ throw $ this ->invalidValidatedType ($ key , $ enumClass );
318+ }
319+
320+ $ reflection = new ReflectionEnum ($ enumClass );
321+
322+ if ($ reflection ->isBacked ()) {
323+ return $ this ->backedEnum ($ key , $ enumClass , $ reflection , $ value );
324+ }
325+
326+ if (is_string ($ value )) {
327+ foreach ($ enumClass ::cases () as $ case ) {
328+ if ($ case ->name === $ value ) {
329+ return $ case ;
330+ }
331+ }
332+ }
333+
334+ throw $ this ->invalidValidatedType ($ key , $ enumClass );
335+ }
336+
202337 /**
203338 * Returns true when the named field exists in the validated data, even if
204339 * its value is null.
@@ -212,6 +347,46 @@ public function hasValidated(string $key): bool
212347 return dot_array_has ($ key , $ this ->validatedData );
213348 }
214349
350+ private function backedEnum (string $ key , string $ enumClass , ReflectionEnum $ reflection , mixed $ value ): UnitEnum
351+ {
352+ $ backingType = $ reflection ->getBackingType ()?->getName();
353+
354+ if ($ backingType === 'int ' ) {
355+ if (is_string ($ value )) {
356+ $ value = filter_var ($ value , FILTER_VALIDATE_INT , FILTER_NULL_ON_FAILURE );
357+ }
358+
359+ if (! is_int ($ value )) {
360+ throw $ this ->invalidValidatedType ($ key , $ enumClass );
361+ }
362+ } elseif (! is_int ($ value ) && ! is_string ($ value )) {
363+ throw $ this ->invalidValidatedType ($ key , $ enumClass );
364+ }
365+
366+ if (! is_subclass_of ($ enumClass , BackedEnum::class)) {
367+ throw $ this ->invalidValidatedType ($ key , $ enumClass );
368+ }
369+
370+ if ($ backingType === 'string ' ) {
371+ $ value = (string ) $ value ;
372+ }
373+
374+ $ enum = $ enumClass ::tryFrom ($ value );
375+
376+ if ($ enum === null ) {
377+ throw $ this ->invalidValidatedType ($ key , $ enumClass );
378+ }
379+
380+ return $ enum ;
381+ }
382+
383+ private function invalidValidatedType (string $ key , string $ type ): InvalidArgumentException
384+ {
385+ return new InvalidArgumentException (
386+ sprintf ('The validated "%s" value cannot be read as %s. ' , $ key , $ type ),
387+ );
388+ }
389+
215390 /**
216391 * Returns the data to be validated.
217392 *
0 commit comments