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

codeigniter4 / CodeIgniter4 / 26721354383

31 May 2026 06:50PM UTC coverage: 88.554% (+0.001%) from 88.553%
26721354383

Pull #10259

github

web-flow
Merge e92187e27 into 2ef1571f5
Pull Request #10259: feat: expose prepared `FormRequest` data during validation failure

1 of 1 new or added line in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

24263 of 27399 relevant lines covered (88.55%)

223.66 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 CodeIgniter\Validation\ValidatedInput;
18
use ReflectionNamedType;
19
use ReflectionParameter;
20

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

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

38
    /**
39
     * Data after prepareForValidation() and before validation rules run.
40
     *
41
     * @var array<string, mixed>
42
     */
43
    private array $preparedValidationData = [];
44

45
    /**
46
     * When called by the framework, the current IncomingRequest is injected
47
     * explicitly. When instantiated manually (e.g. in tests), the constructor
48
     * falls back to service('request').
49
     */
50
    final public function __construct(?Request $request = null)
51
    {
52
        $request ??= service('request');
35✔
53

54
        if (! $request instanceof IncomingRequest) {
35✔
55
            throw new RuntimeException(
1✔
56
                sprintf('%s requires an IncomingRequest instance, got %s.', static::class, $request::class),
1✔
57
            );
1✔
58
        }
59

60
        $this->request = $request;
34✔
61
    }
62

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

78
    /**
79
     * Custom error messages keyed by field.rule.
80
     *
81
     *  return [
82
     *      'title' => ['required' => 'Post title cannot be empty.'],
83
     *  ];
84
     *
85
     * @return array<string, array<string, string>>
86
     */
87
    public function messages(): array
88
    {
89
        return [];
28✔
90
    }
91

92
    /**
93
     * Determine if the current user is authorized to make this request.
94
     *
95
     * Override in subclasses to add authorization logic:
96
     *
97
     *  public function isAuthorized(): bool
98
     *  {
99
     *      return auth()->user()->can('create-posts');
100
     *  }
101
     */
102
    public function isAuthorized(): bool
103
    {
104
        return true;
28✔
105
    }
106

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

120
        if (
121
            $type instanceof ReflectionNamedType
50✔
122
            && ! $type->isBuiltin()
50✔
123
            && is_subclass_of($type->getName(), self::class)
50✔
124
        ) {
125
            return $type->getName();
14✔
126
        }
127

128
        return null;
42✔
129
    }
130

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

153
    /**
154
     * Returns the data after prepareForValidation() has run.
155
     *
156
     * This is useful in failedValidation() when a custom failure response needs
157
     * the same prepared data that was passed to validation. This data has not
158
     * passed validation; use getValidated() or getValidatedInput() after
159
     * successful validation for trusted values.
160
     *
161
     * @return array<string, mixed>
162
     */
163
    protected function getPreparedValidationData(): array
164
    {
165
        return $this->preparedValidationData;
1✔
166
    }
167

168
    /**
169
     * Called when validation fails. Override to customize the failure response.
170
     *
171
     * The default implementation redirects back with input and flashes validation
172
     * errors via the standard ``_ci_validation_errors`` channel (the same channel
173
     * used by controller-level validation and readable by ``validation_errors()``
174
     * helpers). For JSON requests or requests that prefer JSON responses, it
175
     * returns a 422 JSON response instead.
176
     *
177
     * @param array<string, string> $errors
178
     */
179
    protected function failedValidation(array $errors): ResponseInterface
180
    {
181
        if ($this->shouldReturnJsonResponse()) {
12✔
182
            return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
6✔
183
        }
184

185
        return redirect()->back()->withInput();
6✔
186
    }
187

188
    /**
189
     * Determine whether the default validation failure response should be JSON.
190
     */
191
    private function shouldReturnJsonResponse(): bool
192
    {
193
        return $this->request->is('json')
12✔
194
            || $this->request->negotiate('media', ['text/html', 'application/json'], true) === 'application/json';
12✔
195
    }
196

197
    /**
198
     * Called when the isAuthorized() check returns false. Override to customize.
199
     */
200
    protected function failedAuthorization(): ResponseInterface
201
    {
202
        return service('response')->setStatusCode(403);
3✔
203
    }
204

205
    /**
206
     * Returns only the fields that passed validation (those covered by rules()).
207
     *
208
     * Prefer this over $this->request->getPost() in controllers to avoid
209
     * processing fields that were not declared in the rules.
210
     *
211
     * @return array<string, mixed>
212
     */
213
    public function getValidated(): array
214
    {
215
        return $this->validatedData;
13✔
216
    }
217

218
    /**
219
     * Returns the validated data as a typed input object.
220
     */
221
    public function getValidatedInput(): ValidatedInput
222
    {
223
        return service('inputdatafactory')->createValidated($this->validatedData);
1✔
224
    }
225

226
    /**
227
     * Returns the data to be validated.
228
     *
229
     * Override this method to provide custom data or to merge data from
230
     * multiple sources. By default, data is sourced from the appropriate
231
     * part of the request based on HTTP method and Content-Type:
232
     *
233
     * - JSON (any method)      - decoded JSON body
234
     * - PUT / PATCH / DELETE   - raw body (unless multipart/form-data)
235
     * - GET / HEAD             - query-string parameters
236
     * - Everything else (POST) - POST body
237
     *
238
     * @return array<string, mixed>
239
     */
240
    protected function validationData(): array
241
    {
242
        $contentType = $this->request->getHeaderLine('Content-Type');
26✔
243

244
        if (str_contains($contentType, 'application/json')) {
26✔
245
            return $this->request->getJSON(true) ?? [];
4✔
246
        }
247

248
        if (
249
            in_array($this->request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true)
22✔
250
            && ! str_contains($contentType, 'multipart/form-data')
22✔
251
        ) {
UNCOV
252
            return $this->request->getRawInput() ?? [];
×
253
        }
254

255
        if (in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true)) {
22✔
UNCOV
256
            return $this->request->getGet() ?? [];
×
257
        }
258

259
        return $this->request->getPost() ?? [];
22✔
260
    }
261

262
    /**
263
     * Runs authorization and validation. Called by the framework before
264
     * injecting the FormRequest into the controller method.
265
     *
266
     * Returns null on success, or a ResponseInterface to short-circuit the
267
     * request when authorization or validation fails.
268
     *
269
     * Do not call this method directly unless you are inside a ``_remap()``
270
     * method, where automatic injection is not available.
271
     */
272
    final public function resolveRequest(): ?ResponseInterface
273
    {
274
        $this->validatedData          = [];
31✔
275
        $this->preparedValidationData = [];
31✔
276

277
        if (! $this->isAuthorized()) {
31✔
278
            return $this->failedAuthorization();
4✔
279
        }
280

281
        $this->preparedValidationData = $this->prepareForValidation($this->validationData());
27✔
282

283
        $validation = service('validation')
27✔
284
            ->setRules($this->rules(), $this->messages());
27✔
285

286
        if (! $validation->run($this->preparedValidationData)) {
27✔
287
            return $this->failedValidation($validation->getErrors());
14✔
288
        }
289

290
        $this->validatedData = $validation->getValidated();
13✔
291

292
        return null;
13✔
293
    }
294
}
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