|
19 | 19 | use CodeIgniter\Exceptions\PageNotFoundException; |
20 | 20 | use CodeIgniter\Filters\Filters; |
21 | 21 | use CodeIgniter\HTTP\CLIRequest; |
| 22 | +use CodeIgniter\HTTP\Exceptions\FormRequestException; |
22 | 23 | use CodeIgniter\HTTP\Exceptions\RedirectException; |
| 24 | +use CodeIgniter\HTTP\FormRequest; |
23 | 25 | use CodeIgniter\HTTP\IncomingRequest; |
24 | 26 | use CodeIgniter\HTTP\Method; |
25 | 27 | use CodeIgniter\HTTP\NonBufferedResponseInterface; |
|
36 | 38 | use Config\Feature; |
37 | 39 | use Config\Services; |
38 | 40 | use Locale; |
| 41 | +use ReflectionFunction; |
| 42 | +use ReflectionFunctionAbstract; |
| 43 | +use ReflectionMethod; |
39 | 44 | use Throwable; |
40 | 45 |
|
41 | 46 | /** |
@@ -104,7 +109,7 @@ class CodeIgniter |
104 | 109 | /** |
105 | 110 | * Controller to use. |
106 | 111 | * |
107 | | - * @var (Closure(mixed...): ResponseInterface|string)|string|null |
| 112 | + * @var Closure|string|null |
108 | 113 | */ |
109 | 114 | protected $controller; |
110 | 115 |
|
@@ -584,8 +589,9 @@ protected function startController() |
584 | 589 | // Is it routed to a Closure? |
585 | 590 | if (is_object($this->controller) && ($this->controller::class === 'Closure')) { |
586 | 591 | $controller = $this->controller; |
| 592 | + $resolved = $this->resolveCallableParams(new ReflectionFunction($controller), $this->router->params()); |
587 | 593 |
|
588 | | - return $controller(...$this->router->params()); |
| 594 | + return $controller(...$resolved); |
589 | 595 | } |
590 | 596 |
|
591 | 597 | // No controller specified - we don't know what to do now. |
@@ -662,15 +668,117 @@ protected function runController($class) |
662 | 668 |
|
663 | 669 | // The controller method param types may not be string. |
664 | 670 | // So cannot set `declare(strict_types=1)` in this file. |
665 | | - $output = method_exists($class, '_remap') |
666 | | - ? $class->_remap($this->method, ...$params) |
667 | | - : $class->{$this->method}(...$params); |
| 671 | + if (method_exists($class, '_remap')) { |
| 672 | + // FormRequest injection is not supported for _remap() because its |
| 673 | + // signature is fixed to ($method, ...$params). Instantiate the |
| 674 | + // FormRequest manually inside _remap() if needed. |
| 675 | + $output = $class->_remap($this->method, ...$params); |
| 676 | + } else { |
| 677 | + $resolved = $this->resolveMethodParams($class, $this->method, $params); |
| 678 | + $output = $class->{$this->method}(...$resolved); |
| 679 | + } |
668 | 680 |
|
669 | 681 | $this->benchmark->stop('controller'); |
670 | 682 |
|
671 | 683 | return $output; |
672 | 684 | } |
673 | 685 |
|
| 686 | + /** |
| 687 | + * Resolves the final parameter list for a controller method call. |
| 688 | + * |
| 689 | + * @param list<string> $routeParams URI segments from the router. |
| 690 | + * |
| 691 | + * @return list<mixed> |
| 692 | + */ |
| 693 | + private function resolveMethodParams(object $class, string $method, array $routeParams): array |
| 694 | + { |
| 695 | + return $this->resolveCallableParams(new ReflectionMethod($class, $method), $routeParams); |
| 696 | + } |
| 697 | + |
| 698 | + /** |
| 699 | + * Shared FormRequest resolver for both controller methods and closures. |
| 700 | + * |
| 701 | + * Builds a sequential positional argument list for the call site. |
| 702 | + * The supported signature shape is: required scalar route params first, |
| 703 | + * then the FormRequest, then optional scalar params. |
| 704 | + * |
| 705 | + * - FormRequest subclasses are instantiated, authorized, and validated |
| 706 | + * before being injected. |
| 707 | + * - Variadic non-FormRequest parameters consume all remaining URI segments. |
| 708 | + * - Scalar non-FormRequest parameters consume one URI segment each. |
| 709 | + * - When route segments run out, a required non-FormRequest parameter stops |
| 710 | + * iteration so PHP throws an ArgumentCountError on the call site. |
| 711 | + * - Optional non-FormRequest parameters with no remaining segment are omitted |
| 712 | + * from the list; PHP then applies their declared default values. |
| 713 | + * |
| 714 | + * @param list<string> $routeParams URI segments from the router. |
| 715 | + * |
| 716 | + * @return list<mixed> |
| 717 | + */ |
| 718 | + private function resolveCallableParams(ReflectionFunctionAbstract $reflection, array $routeParams): array |
| 719 | + { |
| 720 | + $resolved = []; |
| 721 | + $routeIndex = 0; |
| 722 | + |
| 723 | + foreach ($reflection->getParameters() as $param) { |
| 724 | + // Inject FormRequest subclasses regardless of position. |
| 725 | + $formRequestClass = FormRequest::getFormRequestClass($param); |
| 726 | + |
| 727 | + if ($formRequestClass !== null) { |
| 728 | + $resolved[] = $this->resolveFormRequest($formRequestClass); |
| 729 | + |
| 730 | + continue; |
| 731 | + } |
| 732 | + |
| 733 | + // Variadic parameter - consume all remaining route segments. |
| 734 | + if ($param->isVariadic()) { |
| 735 | + while (array_key_exists($routeIndex, $routeParams)) { |
| 736 | + $resolved[] = $routeParams[$routeIndex++]; |
| 737 | + } |
| 738 | + |
| 739 | + break; |
| 740 | + } |
| 741 | + |
| 742 | + // Consume the next route segment if one is available. |
| 743 | + if (array_key_exists($routeIndex, $routeParams)) { |
| 744 | + $resolved[] = $routeParams[$routeIndex++]; |
| 745 | + |
| 746 | + continue; |
| 747 | + } |
| 748 | + |
| 749 | + // No more route segments. Required params stop iteration so that |
| 750 | + // PHP throws an ArgumentCountError on the call site. Optional |
| 751 | + // params are omitted - PHP then applies their declared default value. |
| 752 | + if (! $param->isOptional()) { |
| 753 | + break; |
| 754 | + } |
| 755 | + } |
| 756 | + |
| 757 | + return $resolved; |
| 758 | + } |
| 759 | + |
| 760 | + /** |
| 761 | + * Instantiates, authorizes, and validates a FormRequest class. |
| 762 | + * |
| 763 | + * If authorization or validation fails, the FormRequest returns a |
| 764 | + * ResponseInterface. The framework wraps it in a FormRequestException |
| 765 | + * (which implements ResponsableInterface) so the response is sent |
| 766 | + * without reaching the controller method. |
| 767 | + * |
| 768 | + * @param class-string<FormRequest> $className |
| 769 | + */ |
| 770 | + private function resolveFormRequest(string $className): FormRequest |
| 771 | + { |
| 772 | + $formRequest = new $className($this->request); |
| 773 | + $response = $formRequest->resolveRequest(); |
| 774 | + |
| 775 | + if ($response !== null) { |
| 776 | + throw new FormRequestException($response); |
| 777 | + } |
| 778 | + |
| 779 | + return $formRequest; |
| 780 | + } |
| 781 | + |
674 | 782 | /** |
675 | 783 | * Displays a 404 Page Not Found error. If set, will try to |
676 | 784 | * call the 404Override controller/method that was set in routing config. |
|
0 commit comments