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

codeigniter4 / CodeIgniter4 / 27154083781

08 Jun 2026 05:06PM UTC coverage: 88.757% (+0.04%) from 88.72%
27154083781

Pull #10285

github

web-flow
Merge e9c7cfc39 into 874a8206b
Pull Request #10285: feat: add per-type API transformer sparse fieldsets

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

9 existing lines in 3 files now uncovered.

24630 of 27750 relevant lines covered (88.76%)

223.91 hits per line

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

98.08
/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
     * When called by the framework, the current IncomingRequest is injected
40
     * explicitly. When instantiated manually (e.g. in tests), the constructor
41
     * falls back to service('request').
42
     */
43
    final public function __construct(?Request $request = null)
44
    {
45
        $request ??= service('request');
38✔
46

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

53
        $this->request = $request;
37✔
54
    }
55

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

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

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

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

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

121
        return null;
42✔
122
    }
123

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

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

164
        $redirect = redirect()->back()->withInput();
9✔
165

166
        $key = in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true)
9✔
167
            ? 'get'
1✔
168
            : 'post';
8✔
169

170
        $oldInput = [
9✔
171
            'get'  => service('superglobals')->getGetArray(),
9✔
172
            'post' => service('superglobals')->getPostArray(),
9✔
173
        ];
9✔
174
        $oldInput[$key] = $preparedData;
9✔
175

176
        service('session')->setFlashdata('_ci_old_input', $oldInput);
9✔
177

178
        return $redirect;
9✔
179
    }
180

181
    /**
182
     * Determine whether the default validation failure response should be JSON.
183
     */
184
    private function shouldReturnJsonResponse(): bool
185
    {
186
        return $this->request->is('json')
15✔
187
            || $this->request->negotiate('media', ['text/html', 'application/json'], true) === 'application/json';
15✔
188
    }
189

190
    /**
191
     * Called when the isAuthorized() check returns false. Override to customize.
192
     */
193
    protected function failedAuthorization(): ResponseInterface
194
    {
195
        return service('response')->setStatusCode(403);
3✔
196
    }
197

198
    /**
199
     * Returns only the fields that passed validation (those covered by rules()).
200
     *
201
     * Prefer this over $this->request->getPost() in controllers to avoid
202
     * processing fields that were not declared in the rules.
203
     *
204
     * @return array<string, mixed>
205
     */
206
    public function getValidated(): array
207
    {
208
        return $this->validatedData;
13✔
209
    }
210

211
    /**
212
     * Returns the validated data as a typed input object.
213
     */
214
    public function getValidatedInput(): ValidatedInput
215
    {
216
        return service('inputdatafactory')->createValidated($this->validatedData);
1✔
217
    }
218

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

237
        if (str_contains($contentType, 'application/json')) {
29✔
238
            return $this->request->getJSON(true) ?? [];
4✔
239
        }
240

241
        if (
242
            in_array($this->request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true)
25✔
243
            && ! str_contains($contentType, 'multipart/form-data')
25✔
244
        ) {
UNCOV
245
            return $this->request->getRawInput() ?? [];
×
246
        }
247

248
        if (in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true)) {
25✔
249
            return $this->request->getGet() ?? [];
1✔
250
        }
251

252
        return $this->request->getPost() ?? [];
24✔
253
    }
254

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

269
        if (! $this->isAuthorized()) {
34✔
270
            return $this->failedAuthorization();
4✔
271
        }
272

273
        $data = $this->prepareForValidation($this->validationData());
30✔
274

275
        $validation = service('validation')
30✔
276
            ->setRules($this->rules(), $this->messages());
30✔
277

278
        if (! $validation->run($data)) {
30✔
279
            return $this->failedValidation($validation->getErrors(), $data);
17✔
280
        }
281

282
        $this->validatedData = $validation->getValidated();
13✔
283

284
        return null;
13✔
285
    }
286
}
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