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

codeigniter4 / CodeIgniter4 / 26717872894

31 May 2026 04:19PM UTC coverage: 88.556% (+0.08%) from 88.475%
26717872894

Pull #10242

github

web-flow
Merge 47dc3835f into 2ef1571f5
Pull Request #10242: feat: add immutable URI query variable helpers

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

155 existing lines in 9 files now uncovered.

24267 of 27403 relevant lines covered (88.56%)

223.65 hits per line

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

95.12
/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');
34✔
46

47
        if (! $request instanceof IncomingRequest) {
34✔
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;
33✔
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 [];
27✔
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;
27✔
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
     */
157
    protected function failedValidation(array $errors): ResponseInterface
158
    {
159
        if ($this->shouldReturnJsonResponse()) {
12✔
160
            return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
6✔
161
        }
162

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

166
    /**
167
     * Determine whether the default validation failure response should be JSON.
168
     */
169
    private function shouldReturnJsonResponse(): bool
170
    {
171
        return $this->request->is('json')
12✔
172
            || $this->request->negotiate('media', ['text/html', 'application/json'], true) === 'application/json';
12✔
173
    }
174

175
    /**
176
     * Called when the isAuthorized() check returns false. Override to customize.
177
     */
178
    protected function failedAuthorization(): ResponseInterface
179
    {
180
        return service('response')->setStatusCode(403);
3✔
181
    }
182

183
    /**
184
     * Returns only the fields that passed validation (those covered by rules()).
185
     *
186
     * Prefer this over $this->request->getPost() in controllers to avoid
187
     * processing fields that were not declared in the rules.
188
     *
189
     * @return array<string, mixed>
190
     */
191
    public function getValidated(): array
192
    {
193
        return $this->validatedData;
13✔
194
    }
195

196
    /**
197
     * Returns the validated data as a typed input object.
198
     */
199
    public function getValidatedInput(): ValidatedInput
200
    {
201
        return service('inputdatafactory')->createValidated($this->validatedData);
1✔
202
    }
203

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

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

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

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

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

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

256
        $data = $this->prepareForValidation($this->validationData());
26✔
257

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

261
        if (! $validation->run($data)) {
26✔
262
            return $this->failedValidation($validation->getErrors());
13✔
263
        }
264

265
        $this->validatedData = $validation->getValidated();
13✔
266

267
        return null;
13✔
268
    }
269
}
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