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

codeigniter4 / CodeIgniter4 / 24041512552

06 Apr 2026 05:02PM UTC coverage: 86.562% (+0.006%) from 86.556%
24041512552

Pull #10087

github

web-flow
Merge 0b455a909 into d6e3f7b3f
Pull Request #10087: feat: add `FormRequest` for encapsulating validation and authorization

68 of 77 new or added lines in 6 files covered. (88.31%)

21 existing lines in 2 files now uncovered.

22771 of 26306 relevant lines covered (86.56%)

221.5 hits per line

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

94.29
/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');
31✔
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 [];
24✔
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;
24✔
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;
22✔
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;
11✔
174
    }
175

176
    /**
177
     * Returns a single validated field value by name, or null if the field
178
     * is not present in the validated data (either not declared in rules() or
179
     * validation has not yet run).
180
     *
181
     * Allows accessing individual fields as object properties:
182
     *
183
     *  $title = $request->title;
184
     *
185
     * @return mixed
186
     */
187
    public function __get(string $name)
188
    {
189
        return $this->validatedData[$name] ?? null;
3✔
190
    }
191

192
    /**
193
     * Returns true when the named field exists in the validated data and its
194
     * value is not null. Mirrors standard PHP isset() semantics on properties:
195
     *
196
     *  if (isset($request->title)) { ... }
197
     */
198
    public function __isset(string $name): bool
199
    {
200
        return isset($this->validatedData[$name]);
2✔
201
    }
202

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

221
        if (str_contains($contentType, 'application/json')) {
22✔
222
            return $this->request->getJSON(true) ?? [];
3✔
223
        }
224

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

232
        if (in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true)) {
19✔
NEW
233
            return $this->request->getGet() ?? [];
×
234
        }
235

236
        return $this->request->getPost() ?? [];
19✔
237
    }
238

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

257
        $data = $this->prepareForValidation($this->validationData());
23✔
258

259
        $validation = service('validation')
23✔
260
            ->setRules($this->rules(), $this->messages());
23✔
261

262
        if (! $validation->run($data)) {
23✔
263
            return $this->failedValidation($validation->getErrors());
9✔
264
        }
265

266
        $this->validatedData = $validation->getValidated();
14✔
267

268
        return null;
14✔
269
    }
270
}
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