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

codeigniter4 / CodeIgniter4 / 24029774564

06 Apr 2026 11:17AM UTC coverage: 86.553% (-0.003%) from 86.556%
24029774564

Pull #10087

github

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

70 of 82 new or added lines in 6 files covered. (85.37%)

21 existing lines in 2 files now uncovered.

22773 of 26311 relevant lines covered (86.55%)

221.46 hits per line

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

87.5
/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 CodeIgniter\Exceptions\RuntimeException;
17
use ReflectionNamedType;
18
use ReflectionParameter;
19

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

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

37
    /**
38
     * When called by the framework, the current IncomingRequest is injected
39
     * explicitly. When instantiated manually (e.g. in tests or commands),
40
     * the constructor falls back to service('request').
41
     *
42
     * @throws RuntimeException if used outside of an HTTP request context.
43
     */
44
    final public function __construct(?IncomingRequest $request = null)
45
    {
46
        $resolved = $request ?? service('request');
31✔
47

48
        if (! $resolved instanceof IncomingRequest) {
31✔
NEW
49
            throw new RuntimeException(
×
NEW
50
                static::class . ' requires an IncomingRequest instance, got ' . $resolved::class . '.',
×
NEW
51
            );
×
52
        }
53

54
        $this->request = $resolved;
31✔
55
    }
56

57
    /**
58
     * Validation rules for this request.
59
     *
60
     * Return an array of field => rules pairs, identical to what you would
61
     * pass to $this->validate() in a controller:
62
     *
63
     *  return [
64
     *      'title' => 'required|min_length[5]',
65
     *      'body'  => ['required', 'max_length[10000]'],
66
     *  ];
67
     *
68
     * @return array<string, list<string>|string>
69
     */
70
    abstract public function rules(): array;
71

72
    /**
73
     * Custom error messages keyed by field.rule.
74
     *
75
     *  return [
76
     *      'title' => ['required' => 'Post title cannot be empty.'],
77
     *  ];
78
     *
79
     * @return array<string, array<string, string>|string>
80
     */
81
    public function messages(): array
82
    {
83
        return [];
24✔
84
    }
85

86
    /**
87
     * Determine if the current user is authorized to make this request.
88
     *
89
     * Override in subclasses to add authorization logic:
90
     *
91
     *  public function authorize(): bool
92
     *  {
93
     *      return auth()->user()->can('create-posts');
94
     *  }
95
     */
96
    public function authorize(): bool
97
    {
98
        return true;
24✔
99
    }
100

101
    /**
102
     * Returns the class name when the given reflection parameter is typed as a
103
     * FormRequest subclass, or null otherwise. Used by the dispatcher and
104
     * auto-router to distinguish injectable parameters from URI-segment parameters.
105
     *
106
     * @internal
107
     *
108
     * @return class-string<self>|null
109
     */
110
    public static function getFormRequestClass(ReflectionParameter $param): ?string
111
    {
112
        $type = $param->getType();
47✔
113

114
        if (
115
            $type instanceof ReflectionNamedType
47✔
116
            && ! $type->isBuiltin()
47✔
117
            && is_subclass_of($type->getName(), self::class)
47✔
118
        ) {
119
            return $type->getName();
11✔
120
        }
121

122
        return null;
40✔
123
    }
124

125
    /**
126
     * Modify the request data before validation rules are applied.
127
     * Override to normalize or cast input values:
128
     *
129
     *  protected function prepareForValidation(array $data): array
130
     *  {
131
     *      $data['slug'] = url_title($data['title'] ?? '', '-', true);
132
     *      return $data;
133
     *  }
134
     *
135
     * The $data array is the same payload that will be passed to the
136
     * validator. Return the (possibly modified) array.
137
     *
138
     * @param array<string, mixed> $data
139
     *
140
     * @return array<string, mixed>
141
     */
142
    protected function prepareForValidation(array $data): array
143
    {
144
        return $data;
22✔
145
    }
146

147
    /**
148
     * Called when validation fails. Override to customize the failure response.
149
     *
150
     * The default implementation redirects back with input and flashes validation
151
     * errors via the standard ``_ci_validation_errors`` channel (the same channel
152
     * used by controller-level validation and readable by ``validation_errors()``
153
     * helpers). For JSON/AJAX requests it returns a 422 JSON response instead.
154
     *
155
     * @param array<string, string> $errors
156
     */
157
    protected function failedValidation(array $errors): ResponseInterface
158
    {
159
        if ($this->request->is('json') || $this->request->isAJAX()) {
8✔
160
            return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
5✔
161
        }
162

163
        return redirect()->back()->withInput();
3✔
164
    }
165

166
    /**
167
     * Called when the authorize() check returns false. Override to customize.
168
     */
169
    protected function failedAuthorization(): ResponseInterface
170
    {
171
        return service('response')->setStatusCode(403);
3✔
172
    }
173

174
    /**
175
     * Returns only the fields that passed validation (those covered by rules()).
176
     *
177
     * Prefer this over $this->request->getPost() in controllers to avoid
178
     * processing fields that were not declared in the rules.
179
     *
180
     * @return array<string, mixed>
181
     */
182
    public function validated(): array
183
    {
184
        return $this->validatedData;
11✔
185
    }
186

187
    /**
188
     * Returns a single validated field value by name, or null if the field
189
     * is not present in the validated data (either not declared in rules() or
190
     * validation has not yet run).
191
     *
192
     * Allows accessing individual fields as object properties:
193
     *
194
     *  $title = $request->title;
195
     *
196
     * @return mixed
197
     */
198
    public function __get(string $name)
199
    {
200
        return $this->validatedData[$name] ?? null;
3✔
201
    }
202

203
    /**
204
     * Returns true when the named field exists in the validated data and its
205
     * value is not null. Mirrors standard PHP isset() semantics on properties:
206
     *
207
     *  if (isset($request->title)) { ... }
208
     */
209
    public function __isset(string $name): bool
210
    {
211
        return isset($this->validatedData[$name]);
2✔
212
    }
213

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

232
        if (str_contains($contentType, 'application/json')) {
22✔
233
            return $this->request->getJSON(true) ?? [];
3✔
234
        }
235

236
        if (
237
            in_array($this->request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true)
19✔
238
            && ! str_contains($contentType, 'multipart/form-data')
19✔
239
        ) {
NEW
240
            return $this->request->getRawInput() ?? [];
×
241
        }
242

243
        if (in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true)) {
19✔
NEW
244
            return $this->request->getGet() ?? [];
×
245
        }
246

247
        return $this->request->getPost() ?? [];
19✔
248
    }
249

250
    /**
251
     * Runs authorization and validation. Called by the framework before
252
     * injecting the FormRequest into the controller method.
253
     *
254
     * Returns null on success, or a ResponseInterface to short-circuit the
255
     * request when authorization or validation fails.
256
     *
257
     * Do not call this method directly unless you are inside a ``_remap()``
258
     * method, where automatic injection is not available.
259
     *
260
     * @internal
261
     */
262
    final public function resolveRequest(): ?ResponseInterface
263
    {
264
        if (! $this->authorize()) {
27✔
265
            return $this->failedAuthorization();
4✔
266
        }
267

268
        $data = $this->prepareForValidation($this->validationData());
23✔
269

270
        $validation = service('validation')
23✔
271
            ->setRules($this->rules(), $this->messages());
23✔
272

273
        if (! $validation->run($data)) {
23✔
274
            return $this->failedValidation($validation->getErrors());
9✔
275
        }
276

277
        $this->validatedData = $validation->getValidated();
14✔
278

279
        return null;
14✔
280
    }
281
}
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