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

codeigniter4 / CodeIgniter4 / 24565812078

17 Apr 2026 12:47PM UTC coverage: 88.256% (+0.03%) from 88.23%
24565812078

Pull #10087

github

web-flow
Merge d676085c9 into 5a3bc4282
Pull Request #10087: feat: add `FormRequest` for encapsulating validation and authorization

83 of 87 new or added lines in 6 files covered. (95.4%)

21 existing lines in 2 files now uncovered.

22770 of 25800 relevant lines covered (88.26%)

220.82 hits per line

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

95.45
/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), the constructor
40
     * falls back to service('request').
41
     */
42
    final public function __construct(?IncomingRequest $request = null)
43
    {
44
        $request ??= service('request');
35✔
45

46
        if (! $request instanceof IncomingRequest) {
35✔
47
            throw new RuntimeException(
1✔
48
                sprintf('%s requires an IncomingRequest instance, got %s.', static::class, $request::class),
1✔
49
            );
1✔
50
        }
51

52
        $this->request = $request;
34✔
53
    }
54

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

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

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

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

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

120
        return null;
40✔
121
    }
122

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

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

161
        return redirect()->back()->withInput();
3✔
162
    }
163

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

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

185
    /**
186
     * Returns a single validated field value by name, or the default value
187
     * if the field is not present in the validated data.
188
     *
189
     * Supports dot-array syntax for nested validated data.
190
     */
191
    public function getValidated(string $key, mixed $default = null): mixed
192
    {
193
        helper('array');
6✔
194

195
        if (! dot_array_has($key, $this->validatedData)) {
6✔
196
            return $default;
3✔
197
        }
198

199
        return dot_array_search($key, $this->validatedData);
3✔
200
    }
201

202
    /**
203
     * Returns true when the named field exists in the validated data, even if
204
     * its value is null.
205
     *
206
     * Supports dot-array syntax for nested validated data.
207
     */
208
    public function hasValidated(string $key): bool
209
    {
210
        helper('array');
4✔
211

212
        return dot_array_has($key, $this->validatedData);
4✔
213
    }
214

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

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

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

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

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

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

267
        $data = $this->prepareForValidation($this->validationData());
26✔
268

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

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

276
        $this->validatedData = $validation->getValidated();
17✔
277

278
        return null;
17✔
279
    }
280
}
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