Skip to content

Commit ed9d8b6

Browse files
committed
feat(http): add typed FormRequest accessors
- Add typed helpers for reading validated integer, boolean, date, and enum values - Keep accessors scoped to validated FormRequest data only - Document expected validation/accessor responsibilities - Cover defaults, null values, invalid values, dot syntax, and enum variants Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
1 parent 1efb85b commit ed9d8b6

5 files changed

Lines changed: 489 additions & 1 deletion

File tree

system/HTTP/FormRequest.php

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@
1313

1414
namespace CodeIgniter\HTTP;
1515

16+
use BackedEnum;
17+
use CodeIgniter\Exceptions\InvalidArgumentException;
1618
use CodeIgniter\Exceptions\RuntimeException;
19+
use CodeIgniter\I18n\Time;
20+
use DateTimeZone;
21+
use Exception;
22+
use ReflectionEnum;
1723
use ReflectionNamedType;
1824
use 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

Comments
 (0)