• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 24348521081

13 Apr 2026 02:20PM UTC coverage: 88.22%. Remained the same
24348521081

Pull #10087

github

web-flow
Merge ac35c4abb into 427fac066
Pull Request #10087: feat: add `FormRequest` for encapsulating validation and authorization

72 of 82 new or added lines in 6 files covered. (87.8%)

21 existing lines in 2 files now uncovered.

22737 of 25773 relevant lines covered (88.22%)

220.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.87
/system/HTTP/FormRequest.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use ReflectionNamedType;
17
use ReflectionParameter;
18

19
/**
20
 * @see \CodeIgniter\HTTP\FormRequestTest
21
 */
22
abstract class FormRequest
23
{
24
    /**
25
     * The underlying HTTP request instance.
26
     */
27
    protected IncomingRequest $request;
28

29
    /**
30
     * Data that passed validation (only the fields covered by rules()).
31
     *
32
     * @var array<string, mixed>
33
     */
34
    private array $validatedData = [];
35

36
    /**
37
     * When called by the framework, the current IncomingRequest is injected
38
     * explicitly. When instantiated manually (e.g. in tests), the constructor
39
     * falls back to service('request').
40
     */
41
    final public function __construct(?IncomingRequest $request = null)
42
    {
43
        $this->request = $request ?? service('request');
34✔
44
    }
45

46
    /**
47
     * Validation rules for this request.
48
     *
49
     * Return an array of field => rules pairs, identical to what you would
50
     * pass to $this->validate() in a controller:
51
     *
52
     *  return [
53
     *      'title' => 'required|min_length[5]',
54
     *      'body'  => ['required', 'max_length[10000]'],
55
     *  ];
56
     *
57
     * @return array<string, list<string>|string>
58
     */
59
    abstract public function rules(): array;
60

61
    /**
62
     * Custom error messages keyed by field.rule.
63
     *
64
     *  return [
65
     *      'title' => ['required' => 'Post title cannot be empty.'],
66
     *  ];
67
     *
68
     * @return array<string, array<string, string>|string>
69
     */
70
    public function messages(): array
71
    {
72
        return [];
27✔
73
    }
74

75
    /**
76
     * Determine if the current user is authorized to make this request.
77
     *
78
     * Override in subclasses to add authorization logic:
79
     *
80
     *  public function isAuthorized(): bool
81
     *  {
82
     *      return auth()->user()->can('create-posts');
83
     *  }
84
     */
85
    public function isAuthorized(): bool
86
    {
87
        return true;
27✔
88
    }
89

90
    /**
91
     * Returns the class name when the given reflection parameter is typed as a
92
     * FormRequest subclass, or null otherwise. Used by the dispatcher and
93
     * auto-router to distinguish injectable parameters from URI-segment parameters.
94
     *
95
     * @internal
96
     *
97
     * @return class-string<self>|null
98
     */
99
    public static function getFormRequestClass(ReflectionParameter $param): ?string
100
    {
101
        $type = $param->getType();
47✔
102

103
        if (
104
            $type instanceof ReflectionNamedType
47✔
105
            && ! $type->isBuiltin()
47✔
106
            && is_subclass_of($type->getName(), self::class)
47✔
107
        ) {
108
            return $type->getName();
11✔
109
        }
110

111
        return null;
40✔
112
    }
113

114
    /**
115
     * Modify the request data before validation rules are applied.
116
     * Override to normalize or cast input values:
117
     *
118
     *  protected function prepareForValidation(array $data): array
119
     *  {
120
     *      $data['slug'] = url_title($data['title'] ?? '', '-', true);
121
     *      return $data;
122
     *  }
123
     *
124
     * The $data array is the same payload that will be passed to the
125
     * validator. Return the (possibly modified) array.
126
     *
127
     * @param array<string, mixed> $data
128
     *
129
     * @return array<string, mixed>
130
     */
131
    protected function prepareForValidation(array $data): array
132
    {
133
        return $data;
25✔
134
    }
135

136
    /**
137
     * Called when validation fails. Override to customize the failure response.
138
     *
139
     * The default implementation redirects back with input and flashes validation
140
     * errors via the standard ``_ci_validation_errors`` channel (the same channel
141
     * used by controller-level validation and readable by ``validation_errors()``
142
     * helpers). For JSON/AJAX requests it returns a 422 JSON response instead.
143
     *
144
     * @param array<string, string> $errors
145
     */
146
    protected function failedValidation(array $errors): ResponseInterface
147
    {
148
        if ($this->request->is('json') || $this->request->isAJAX()) {
8✔
149
            return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
5✔
150
        }
151

152
        return redirect()->back()->withInput();
3✔
153
    }
154

155
    /**
156
     * Called when the isAuthorized() check returns false. Override to customize.
157
     */
158
    protected function failedAuthorization(): ResponseInterface
159
    {
160
        return service('response')->setStatusCode(403);
3✔
161
    }
162

163
    /**
164
     * Returns only the fields that passed validation (those covered by rules()).
165
     *
166
     * Prefer this over $this->request->getPost() in controllers to avoid
167
     * processing fields that were not declared in the rules.
168
     *
169
     * @return array<string, mixed>
170
     */
171
    public function validated(): array
172
    {
173
        return $this->validatedData;
12✔
174
    }
175

176
    /**
177
     * Returns a single validated field value by name, or the default value
178
     * if the field is not present in the validated data.
179
     *
180
     * Supports dot-array syntax for nested validated data.
181
     */
182
    public function getValidated(string $key, mixed $default = null): mixed
183
    {
184
        helper('array');
6✔
185

186
        if (! dot_array_has($key, $this->validatedData)) {
6✔
187
            return $default;
3✔
188
        }
189

190
        return dot_array_search($key, $this->validatedData);
3✔
191
    }
192

193
    /**
194
     * Returns true when the named field exists in the validated data, even if
195
     * its value is null.
196
     *
197
     * Supports dot-array syntax for nested validated data.
198
     */
199
    public function hasValidated(string $key): bool
200
    {
201
        helper('array');
4✔
202

203
        return dot_array_has($key, $this->validatedData);
4✔
204
    }
205

206
    /**
207
     * Returns the data to be validated.
208
     *
209
     * Override this method to provide custom data or to merge data from
210
     * multiple sources. By default, data is sourced from the appropriate
211
     * part of the request based on HTTP method and Content-Type:
212
     *
213
     * - JSON (any method)      - decoded JSON body
214
     * - PUT / PATCH / DELETE   - raw body (unless multipart/form-data)
215
     * - GET / HEAD             - query-string parameters
216
     * - Everything else (POST) - POST body
217
     *
218
     * @return array<string, mixed>
219
     */
220
    protected function validationData(): array
221
    {
222
        $contentType = $this->request->getHeaderLine('Content-Type');
25✔
223

224
        if (str_contains($contentType, 'application/json')) {
25✔
225
            return $this->request->getJSON(true) ?? [];
4✔
226
        }
227

228
        if (
229
            in_array($this->request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true)
21✔
230
            && ! str_contains($contentType, 'multipart/form-data')
21✔
231
        ) {
NEW
232
            return $this->request->getRawInput() ?? [];
×
233
        }
234

235
        if (in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true)) {
21✔
NEW
236
            return $this->request->getGet() ?? [];
×
237
        }
238

239
        return $this->request->getPost() ?? [];
21✔
240
    }
241

242
    /**
243
     * Runs authorization and validation. Called by the framework before
244
     * injecting the FormRequest into the controller method.
245
     *
246
     * Returns null on success, or a ResponseInterface to short-circuit the
247
     * request when authorization or validation fails.
248
     *
249
     * Do not call this method directly unless you are inside a ``_remap()``
250
     * method, where automatic injection is not available.
251
     *
252
     * @internal
253
     */
254
    final public function resolveRequest(): ?ResponseInterface
255
    {
256
        if (! $this->isAuthorized()) {
30✔
257
            return $this->failedAuthorization();
4✔
258
        }
259

260
        $data = $this->prepareForValidation($this->validationData());
26✔
261

262
        $validation = service('validation')
26✔
263
            ->setRules($this->rules(), $this->messages());
26✔
264

265
        if (! $validation->run($data)) {
26✔
266
            return $this->failedValidation($validation->getErrors());
9✔
267
        }
268

269
        $this->validatedData = $validation->getValidated();
17✔
270

271
        return null;
17✔
272
    }
273
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc