@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. package/dist/index.d-BJyJx6U1.d.ts +0 -319
@@ -0,0 +1,1792 @@
1
+ # Validation
2
+
3
+ Oriel checks the data your app receives with Laravel's validation rules, and types the validated data from the rules
4
+ you write.
5
+
6
+ ## Introduction
7
+
8
+ Oriel validates in three ways: `request().validate()` in a route or a controller, form request classes, and
9
+ validators you make yourself with `Validator.make()`. Each takes the same rules, with Laravel's names, such as
10
+ `required|email|max:255`, and fails with Laravel's messages. When validation fails, the user goes back to the form
11
+ with the errors, or an API client receives them as JSON.
12
+
13
+ ## Validation quickstart
14
+
15
+ Here is a complete example: a form that creates a blog post, validated, with its errors shown to the user.
16
+
17
+ ### Defining the routes
18
+
19
+ Two routes in `routes/web.ts`: a `GET` route shows the form, and a `POST` route stores the post:
20
+
21
+ ```ts title="routes/web.ts"
22
+ // @filename: app/Http/Controllers/PostController.ts
23
+ export declare class PostController {
24
+ create(): unknown
25
+ store(): Promise<unknown>
26
+ }
27
+ // ---cut---
28
+ import { Route } from '@orieljs/framework'
29
+ import { PostController } from '../app/Http/Controllers/PostController'
30
+
31
+ Route.get('/post/create', [PostController, 'create'])
32
+ Route.post('/post', [PostController, 'store'])
33
+ ```
34
+
35
+ ### Creating the controller
36
+
37
+ The controller shows the form and stores the post. `store()` is empty for now:
38
+
39
+ ```ts title="app/Http/Controllers/PostController.ts"
40
+ import { redirect, view } from '@orieljs/framework'
41
+ import { Controller } from './Controller'
42
+
43
+ export class PostController extends Controller {
44
+ /** Show the form to create a new blog post. */
45
+ create() {
46
+ return view('post.create')
47
+ }
48
+
49
+ /** Store a new blog post. */
50
+ async store() {
51
+ // Validate and store the blog post...
52
+
53
+ return redirect('/posts')
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### Writing the validation logic
59
+
60
+ `request().validate()` takes the rules for each field. When the input passes, it returns the validated data and your
61
+ code goes on. When it fails, it throws, and Oriel sends the response:
62
+
63
+ - to a browser's form, a redirect back to the previous page, with the errors and the input flashed to the session;
64
+ - to a request that expects JSON, a 422 response with the errors (see the
65
+ [error response format](#validation-error-response-format)).
66
+
67
+ ```ts title="app/Http/Controllers/PostController.ts"
68
+ import { redirect, request } from '@orieljs/framework'
69
+ import { Controller } from './Controller'
70
+
71
+ export class PostController extends Controller {
72
+ /** Store a new blog post. */
73
+ async store() {
74
+ const validated = await request().validate({
75
+ title: 'required|unique:posts|max:255',
76
+ body: 'required',
77
+ })
78
+
79
+ // The blog post is valid...
80
+
81
+ return redirect('/posts')
82
+ }
83
+ }
84
+ ```
85
+
86
+ The rules are a string with `|` between them, or a list. A list can also hold [rule objects](#rule-objects):
87
+
88
+ ```ts
89
+ const validated = await request().validate({
90
+ title: ['required', 'unique:posts', 'max:255'],
91
+ body: ['required'],
92
+ })
93
+ ```
94
+
95
+ The validated data is typed from the rules. Here `title` and `body` are always there but `unknown`, since no rule
96
+ says what they are; with `string` among their rules, both are typed `string`. [Working with validated
97
+ input](#working-with-validated-input) covers the types.
98
+
99
+ A rule name Oriel doesn't know, such as a misspelt one, is an error when it runs on a field the request sends, and the
100
+ request ends with a 500:
101
+ `Validation rule "requird" is not registered. Register it with ValidationFactory.extend() first.` On a field the request
102
+ leaves out or sends blank, the rule doesn't run: a misspelt `required` lets a missing field through.
103
+
104
+ #### Stopping on first validation failure
105
+
106
+ `bail` stops a field's rules at its first failure. Without it, every rule of the field runs and each failure adds a
107
+ message:
108
+
109
+ ```ts
110
+ const validated = await request().validate({
111
+ title: 'bail|required|unique:posts|max:255',
112
+ body: 'required',
113
+ })
114
+ ```
115
+
116
+ Here a failing `unique` skips `max`. Rules run in the order you write them. A failing `required`, or another rule
117
+ that runs on a missing field, always stops its field.
118
+
119
+ #### A note on nested attributes
120
+
121
+ Fields the request sends nested, as JSON or as `author[name]` in a form, are named with dots:
122
+
123
+ ```ts
124
+ const validated = await request().validate({
125
+ title: 'required|unique:posts|max:255',
126
+ 'author.name': 'required',
127
+ 'author.description': 'required',
128
+ })
129
+ ```
130
+
131
+ Laravel escapes a dot that belongs to a field's name with a backslash, as `v1\.0`; Oriel has no such escape.
132
+
133
+ ### Displaying the validation errors
134
+
135
+ When validation fails, the user is sent back to the form, with the errors and the input flashed to the session.
136
+ `errors()` reads the errors in your views:
137
+
138
+ ```tsx title="resources/views/post/create.tsx"
139
+ import { errors } from '@orieljs/framework'
140
+
141
+ export default function Create() {
142
+ const messages = errors()
143
+
144
+ return (
145
+ <main>
146
+ <h1>Create Post</h1>
147
+
148
+ {!messages.isEmpty() && (
149
+ <div class="alert alert-danger">
150
+ <ul>
151
+ {messages.all().map((message) => (
152
+ <li>{message}</li>
153
+ ))}
154
+ </ul>
155
+ </div>
156
+ )}
157
+
158
+ {/* Create Post Form */}
159
+ </main>
160
+ )
161
+ }
162
+ ```
163
+
164
+ `errors()` returns an empty bag when there are no errors, so a view can call it at any time.
165
+
166
+ #### Customizing the error messages
167
+
168
+ Each rule has Laravel's English message, such as "The title field is required.". Give your own to a
169
+ [form request](#customizing-the-error-messages-1) or to a [validator](#customizing-the-error-messages-2).
170
+
171
+ #### XHR requests and validation
172
+
173
+ A request that expects JSON, as a script's `fetch()` with `Accept: application/json` does, gets no redirect: a
174
+ failed validation answers with a 422 and the errors as JSON. [Error handling](errors.md) covers how Oriel tells the
175
+ two kinds of request apart.
176
+
177
+ #### Showing a field's errors
178
+
179
+ `errors().first('title')` returns a field's first message, or `undefined`, so a field can show its own error:
180
+
181
+ ```tsx title="resources/views/post/create.tsx"
182
+ import { errors } from '@orieljs/framework'
183
+
184
+ export default function Create() {
185
+ const error = errors().first('title')
186
+
187
+ return (
188
+ <form method="post" action="/post">
189
+ <label for="title">Post Title</label>
190
+ <input id="title" type="text" name="title" class={{ 'is-invalid': error !== undefined }} />
191
+
192
+ {error && <div class="alert alert-danger">{error}</div>}
193
+ </form>
194
+ )
195
+ }
196
+ ```
197
+
198
+ With [named error bags](#named-error-bags), `errors('post').first('title')` reads the `post` bag.
199
+
200
+ ### Repopulating forms
201
+
202
+ The input of a failed validation is flashed with the errors, except the `password`, `password_confirmation` and
203
+ `current_password` fields. `old()` reads it on the next request, so a form shows what the user typed:
204
+
205
+ ```tsx
206
+ const input = <input type="text" name="title" value={old('title', '')} />
207
+ ```
208
+
209
+ Without old input for the field, `old()` returns the default you give it. [Requests](requests.md#old-input) has
210
+ more on old input.
211
+
212
+ ### A note on optional fields
213
+
214
+ The `TrimStrings` and `ConvertEmptyStringsToNull` middleware tidy every request's input, so a field left empty
215
+ arrives as `null`. Mark an optional field `nullable`, so `null` passes its other rules:
216
+
217
+ ```ts
218
+ const validated = await request().validate({
219
+ title: 'required|unique:posts|max:255',
220
+ body: 'required',
221
+ publish_at: 'nullable|date',
222
+ })
223
+ ```
224
+
225
+ Without `nullable`, `null` fails the `date` rule.
226
+
227
+ ### Validation error response format
228
+
229
+ A failed validation answers a request that expects JSON with a 422, a `message` (the first error, and how many more
230
+ there are) and `errors`, by field. Nested fields are named with dots:
231
+
232
+ ```json
233
+ {
234
+ "message": "The team name field must be a string. (and 4 more errors)",
235
+ "errors": {
236
+ "team_name": ["The team name field must be a string.", "The team name field must be at least 1 characters."],
237
+ "authorization.role": ["The selected authorization.role is invalid."],
238
+ "users.0.email": ["The users.0.email field is required."],
239
+ "users.2.email": ["The users.2.email field must be a valid email address."]
240
+ }
241
+ }
242
+ ```
243
+
244
+ ### Displaying errors in Inertia pages
245
+
246
+ In the Inertia starter kits, a failed validation sends the user back to the page, and the errors reach it as the
247
+ `errors` prop: one message per field. Each adapter's `useForm()` holds them in `errors`:
248
+
249
+ ```tsx tab="React" title="resources/js/pages/Auth/ForgotPassword.tsx"
250
+ import { route, useForm } from '@orieljs/inertia-react'
251
+ import type { FormEvent } from 'react'
252
+
253
+ export default function ForgotPassword() {
254
+ const { data, setData, post, processing, errors } = useForm<{ email: string }>({
255
+ email: '',
256
+ })
257
+
258
+ const submit = (event: FormEvent) => {
259
+ event.preventDefault()
260
+ post(route('password.email'))
261
+ }
262
+
263
+ return (
264
+ <form onSubmit={submit}>
265
+ <input type="email" value={data.email} onChange={(event) => setData('email', event.target.value)} />
266
+ {errors.email && <p className="error">{errors.email}</p>}
267
+
268
+ <button type="submit" disabled={processing}>
269
+ Email password reset link
270
+ </button>
271
+ </form>
272
+ )
273
+ }
274
+ ```
275
+ ```vue tab="Vue" title="resources/js/pages/Auth/ForgotPassword.vue"
276
+ <script setup lang="ts">
277
+ import { route, useForm } from '@orieljs/inertia-vue'
278
+
279
+ const form = useForm({
280
+ email: '',
281
+ })
282
+
283
+ const submit = () => {
284
+ form.post(route('password.email'))
285
+ }
286
+ </script>
287
+
288
+ <template>
289
+ <form @submit.prevent="submit">
290
+ <input v-model="form.email" type="email" />
291
+ <p v-if="form.errors.email" class="error">{{ form.errors.email }}</p>
292
+
293
+ <button type="submit" :disabled="form.processing">Email password reset link</button>
294
+ </form>
295
+ </template>
296
+ ```
297
+ ```svelte tab="Svelte" title="resources/js/pages/Auth/ForgotPassword.svelte"
298
+ <script lang="ts">
299
+ import { route, useForm } from '@orieljs/inertia-svelte'
300
+
301
+ const form = useForm({
302
+ email: '',
303
+ })
304
+
305
+ function submit(event: SubmitEvent) {
306
+ event.preventDefault()
307
+ form.post(route('password.email'))
308
+ }
309
+ </script>
310
+
311
+ <form onsubmit={submit}>
312
+ <input type="email" bind:value={form.email} />
313
+ {#if form.errors.email}
314
+ <p class="error">{form.errors.email}</p>
315
+ {/if}
316
+
317
+ <button type="submit" disabled={form.processing}>Email password reset link</button>
318
+ </form>
319
+ ```
320
+ ```tsx tab="Solid" title="resources/js/pages/Auth/ForgotPassword.tsx"
321
+ import { route, useForm } from '@orieljs/inertia-solid'
322
+ import { Show } from 'solid-js'
323
+
324
+ export default function ForgotPassword() {
325
+ const form = useForm<{ email: string }>({
326
+ email: '',
327
+ })
328
+
329
+ const submit = (event: SubmitEvent) => {
330
+ event.preventDefault()
331
+ form.post(route('password.email'))
332
+ }
333
+
334
+ return (
335
+ <form onSubmit={submit}>
336
+ <input type="email" value={form.data.email} onInput={(event) => form.setData('email', event.currentTarget.value)} />
337
+ <Show when={form.errors.email}>
338
+ <p class="error">{form.errors.email}</p>
339
+ </Show>
340
+
341
+ <button type="submit" disabled={form.processing}>
342
+ Email password reset link
343
+ </button>
344
+ </form>
345
+ )
346
+ }
347
+ ```
348
+ ```ts tab="Angular" title="resources/js/pages/Auth/ForgotPassword.component.ts"
349
+ import { Component } from '@angular/core'
350
+ import { route, useForm } from '@orieljs/inertia-angular'
351
+
352
+ @Component({
353
+ selector: 'forgot-password-page',
354
+ template: `
355
+ <form (submit)="submit($event)">
356
+ <input #emailInput type="email" [value]="form.data.email" (input)="form.setData('email', emailInput.value)" />
357
+ @if (form.errors.email) {
358
+ <p class="error">{{ form.errors.email }}</p>
359
+ }
360
+
361
+ <button type="submit" [disabled]="form.processing">Email password reset link</button>
362
+ </form>
363
+ `,
364
+ })
365
+ export default class ForgotPassword {
366
+ protected readonly form = useForm<{ email: string }>({ email: '' })
367
+
368
+ protected submit(event: SubmitEvent): void {
369
+ event.preventDefault()
370
+ this.form.post(route('password.email'))
371
+ }
372
+ }
373
+ ```
374
+
375
+ [Inertia](inertia.md#forms) covers forms in more depth.
376
+
377
+ #### Precognition
378
+
379
+ A route with the `precognitive` middleware can validate a form while the user fills it in: the React, Vue and
380
+ Svelte forms send a precognitive request, which runs the action up to its first validation and answers with the
381
+ errors, or a 204 when the data passes. [Precognitive requests](middleware.md#precognitive-requests) describes the
382
+ requests. The Solid and Angular forms don't offer Precognition yet.
383
+
384
+ > [!WARNING]
385
+ > Validate first. Laravel validates a form request before it calls the action; Oriel validates when the action calls
386
+ > `validate()`, so on a precognitive request, what the action does before its validation runs too. On a route with
387
+ > the `precognitive` middleware, put the validation at the top of the action.
388
+
389
+ ### Displaying errors in Live components
390
+
391
+ A Live component validates its own properties with `this.validate()`. A failed validation stops the action, and the
392
+ component renders again with its errors in `this.errors`:
393
+
394
+ ```tsx title="app/Live/CreatePost.tsx"
395
+ import { Component, request, Str } from '@orieljs/framework'
396
+ import type { User } from '../Models/User'
397
+
398
+ export default class CreatePost extends Component {
399
+ title = ''
400
+
401
+ async save(): Promise<void> {
402
+ const data = await this.validate({ title: 'required|string|max:255' })
403
+
404
+ const user = (await request().user<User>())!
405
+
406
+ await user.posts().create({ title: data.title, slug: Str.slug(data.title), body: '' })
407
+ }
408
+
409
+ render() {
410
+ return (
411
+ <form o:submit="save">
412
+ <input type="text" o:model="title" />
413
+ {this.errors.has('title') && <p class="error">{this.errors.first('title')}</p>}
414
+
415
+ <button type="submit">Save</button>
416
+ </form>
417
+ )
418
+ }
419
+ }
420
+ ```
421
+
422
+ `await request().user()` returns the signed-in user, or `undefined` for a guest. The page's route has the `auth`
423
+ middleware, which each update runs again, so here there is always one. [Live](live.md#validation) covers validation in
424
+ components.
425
+
426
+ ## Form request validation
427
+
428
+ ### Creating form requests
429
+
430
+ A form request is a class that holds the rules of one form, and the check that the user may send it. `make:request`
431
+ writes one in `app/Http/Requests`:
432
+
433
+ ```sh
434
+ node oriel make:request StorePostRequest
435
+ ```
436
+
437
+ The class it writes has two methods: `authorize()`, which decides whether the signed-in user may make the request,
438
+ and `rules()`, which returns the rules:
439
+
440
+ ```ts title="app/Http/Requests/StorePostRequest.ts"
441
+ import { FormRequest } from '@orieljs/framework'
442
+
443
+ export class StorePostRequest extends FormRequest {
444
+ override authorize(): boolean {
445
+ return true
446
+ }
447
+
448
+ /** Get the validation rules that apply to the request. */
449
+ rules() {
450
+ return {
451
+ title: 'required|string|unique:posts|max:255',
452
+ body: 'required|string',
453
+ } as const
454
+ }
455
+ }
456
+ ```
457
+
458
+ The action validates the request by calling the class's `validate()`, which returns the validated data:
459
+
460
+ ```ts title="app/Http/Controllers/PostController.ts"
461
+ // @filename: app/Http/Requests/StorePostRequest.ts
462
+ import { FormRequest } from '@orieljs/framework'
463
+ export class StorePostRequest extends FormRequest {
464
+ rules() {
465
+ return { title: 'required|string|unique:posts|max:255', body: 'required|string' } as const
466
+ }
467
+ }
468
+ // ---cut---
469
+ import { redirect, request, Str } from '@orieljs/framework'
470
+ import type { User } from '../../Models/User'
471
+ import { StorePostRequest } from '../Requests/StorePostRequest'
472
+ import { Controller } from './Controller'
473
+
474
+ export class PostController extends Controller {
475
+ /** Store a new blog post. */
476
+ async store() {
477
+ const validated = await StorePostRequest.validate()
478
+
479
+ const user = (await request().user<User>())!
480
+ const post = await user.posts().create({ ...validated, slug: Str.slug(validated.title) })
481
+
482
+ return redirect().route('posts.show', post)
483
+ }
484
+ }
485
+ ```
486
+
487
+ The route to `store()` has the `auth` middleware, so `await request().user()` is the signed-in user. A failure is
488
+ answered as `request().validate()`'s is: a redirect back with the errors, or a 422 with the errors as JSON.
489
+
490
+ > [!NOTE]
491
+ > Laravel validates a form request before the action runs, when the action type-hints it. TypeScript types are gone
492
+ > when your app runs, so Oriel can't see the hint: the action calls `StorePostRequest.validate()` itself, which is
493
+ > also why a [precognitive](#precognition) action validates first.
494
+
495
+ > [!WARNING]
496
+ > Write `rules()` without a return type and with `as const`. `as const` keeps each rule string's text, which types the
497
+ > validated data: without it, every field is optional and `unknown`. A return type of `Rules` makes the data
498
+ > `Record<string, unknown>`.
499
+
500
+ ```ts
501
+ class WithoutAsConstRequest extends FormRequest {
502
+ rules() {
503
+ return { title: 'required|string' }
504
+ }
505
+ }
506
+
507
+ const data = await WithoutAsConstRequest.validate()
508
+
509
+ // @ts-expect-error: without as const, title is unknown and may be missing
510
+ const title: string = data.title
511
+ ```
512
+
513
+ `request().validate()` needs neither: it reads the rules' text from the call.
514
+
515
+ ### Authorizing form requests
516
+
517
+ `authorize()` decides whether the user may make the request. It may be async, as checking a policy is. When it returns
518
+ `false`, `validate()` throws before validating, and the request ends with a 403, "This action is unauthorized.":
519
+
520
+ ```ts title="app/Http/Requests/UpdatePostRequest.ts"
521
+ import { Auth, FormRequest, request } from '@orieljs/framework'
522
+ import type { Post } from '../../Models/Post'
523
+ import type { User } from '../../Models/User'
524
+
525
+ export class UpdatePostRequest extends FormRequest {
526
+ override async authorize(): Promise<boolean> {
527
+ const post = request().route<Post>('post')
528
+ const user = await Auth.user<User>()
529
+
530
+ return user !== undefined && (await user.can('update', post))
531
+ }
532
+
533
+ rules() {
534
+ return { title: 'required|string|max:255' } as const
535
+ }
536
+ }
537
+ ```
538
+
539
+ `request().route('post')` returns the route's `post` parameter, after [model binding](routing.md#route-model-binding),
540
+ and `Auth.user()` the signed-in user, or `undefined` for a guest. Without an `authorize()`, every request may proceed.
541
+ Gates and policies are covered on the [Authorization](authorization.md#via-form-requests) page.
542
+
543
+ ### Customizing the error messages
544
+
545
+ `messages()` returns messages for a field's rule, and `attributes()` the names that replace `:attribute` in them:
546
+
547
+ ```ts title="app/Http/Requests/StorePostRequest.ts"
548
+ import { FormRequest } from '@orieljs/framework'
549
+
550
+ export class StorePostRequest extends FormRequest {
551
+ rules() {
552
+ return {
553
+ title: 'required|string|max:255',
554
+ body: 'required|string',
555
+ } as const
556
+ }
557
+
558
+ override messages(): Record<string, string> {
559
+ return {
560
+ 'title.required': 'A title is required',
561
+ 'body.required': 'A message is required',
562
+ }
563
+ }
564
+
565
+ override attributes(): Record<string, string> {
566
+ return {
567
+ body: 'post body',
568
+ }
569
+ }
570
+ }
571
+ ```
572
+
573
+ Laravel's form requests also have `after()`, `stopOnFirstFailure`, `$redirect`, `prepareForValidation()` and
574
+ `passedValidation()`, which Oriel's don't. Change the input before you validate with `request().merge()`, and check
575
+ more with a [validator of your own](#performing-additional-validation).
576
+
577
+ ## Manually creating validators
578
+
579
+ The `Validator` [facade](facades.md) makes a validator from any data and rules, when you want to decide what a failure
580
+ does:
581
+
582
+ ```ts title="app/Http/Controllers/PostController.ts"
583
+ import { redirect, request, Validator } from '@orieljs/framework'
584
+ import { Controller } from './Controller'
585
+
586
+ export class PostController extends Controller {
587
+ /** Store a new blog post. */
588
+ async store() {
589
+ const validator = Validator.make(request().all(), {
590
+ title: 'required|unique:posts|max:255',
591
+ body: 'required',
592
+ })
593
+
594
+ if (await validator.fails()) {
595
+ return redirect('/post/create').withErrors(validator.errors()).withInput()
596
+ }
597
+
598
+ // Retrieve the validated input...
599
+ const validated = await validator.validated()
600
+
601
+ // Store the blog post...
602
+
603
+ return redirect('/posts')
604
+ }
605
+ }
606
+ ```
607
+
608
+ `make()` takes the data to validate, the rules, and then custom messages and attribute names. `passes()` and
609
+ `fails()` run the rules once, and `errors()` returns the messages afterwards. `validated()` returns the data that has
610
+ rules, and throws when it fails. Its data is typed from the rules, as `request().validate()`'s is.
611
+
612
+ > [!NOTE]
613
+ > Laravel's `passes()`, `fails()` and `validated()` return their answer. Oriel's return promises, to await: some rules
614
+ > wait for a database query (`unique`, `exists`), a file's bytes (`image`, `mimes`) or the network (`active_url`),
615
+ > and a Node process can't block on them.
616
+
617
+ ### Automatic redirection
618
+
619
+ A failing `validated()` throws the same error as `request().validate()`, so the redirect back, or the 422, is sent
620
+ for you:
621
+
622
+ ```ts
623
+ const validated = await Validator.make(request().all(), {
624
+ title: 'required|unique:posts|max:255',
625
+ body: 'required',
626
+ }).validated()
627
+ ```
628
+
629
+ ### Named error bags
630
+
631
+ A page with several forms keeps each form's errors apart in a named error bag. `withErrors()` takes the bag's name
632
+ as its second argument:
633
+
634
+ ```ts
635
+ declare const validator: import('@orieljs/framework/contracts').Validator
636
+ // ---cut---
637
+ const res = redirect('/register').withErrors(validator.errors(), 'login')
638
+ ```
639
+
640
+ The view reads it with `errors('login')`:
641
+
642
+ ```tsx
643
+ const message = <p>{errors('login').first('email')}</p>
644
+ ```
645
+
646
+ To send a failure to a named bag from anywhere, throw a `ValidationException` with the bag's name; a third argument
647
+ redirects somewhere other than back:
648
+
649
+ ```ts
650
+ throw new ValidationException({ email: ['These credentials do not match our records.'] }, 'login')
651
+ ```
652
+
653
+ `ValidationException.withMessages({ email: 'These credentials do not match our records.' })` fails the request
654
+ with your own messages, in the default bag, as the starter kits' login does. Laravel's `validateWithBag()` isn't in
655
+ Oriel.
656
+
657
+ ### Customizing the error messages
658
+
659
+ The third argument of `make()` replaces the messages of rules. `:attribute` is the field's name:
660
+
661
+ ```ts
662
+ declare const input: Record<string, unknown>
663
+ declare const rules: Rules
664
+ // ---cut---
665
+ const validator = Validator.make(input, rules, {
666
+ required: 'The :attribute field is required.',
667
+ })
668
+ ```
669
+
670
+ A message can use the placeholders its rule fills: `:other` for the field `same` compares with, `:size`, `:min`,
671
+ `:max`, `:values` and so on, as Laravel's messages do.
672
+
673
+ #### Specifying a custom message for a given attribute
674
+
675
+ Name the field, then the rule, to change one field's message. A wildcard names every item of a list:
676
+
677
+ ```ts
678
+ const messages = {
679
+ 'email.required': 'We need to know your email address!',
680
+ 'users.*.email.unique': 'Each user must have a unique email address',
681
+ }
682
+ ```
683
+
684
+ #### Specifying custom attribute values
685
+
686
+ The fourth argument names fields in the messages, in place of their key:
687
+
688
+ ```ts
689
+ declare const input: Record<string, unknown>
690
+ declare const rules: Rules
691
+ declare const messages: Record<string, string>
692
+ // ---cut---
693
+ const validator = Validator.make(input, rules, messages, {
694
+ email: 'email address',
695
+ })
696
+ ```
697
+
698
+ Without one, a key is written in words: `first_name` is "first name", `productName` "product name", and
699
+ `users.0.email` stays as it is.
700
+
701
+ ### Performing additional validation
702
+
703
+ `after()` runs a function once the rules have run, to check more. The function receives the validator, whose
704
+ `fail()` adds a message:
705
+
706
+ ```ts
707
+ declare const somethingElseIsInvalid: () => boolean
708
+ // ---cut---
709
+ const validator = Validator.make(request().all(), { title: 'required' })
710
+
711
+ validator.after((v) => {
712
+ if (somethingElseIsInvalid()) {
713
+ v.fail('field', 'Something is wrong with this field!')
714
+ }
715
+ })
716
+
717
+ if (await validator.fails()) {
718
+ // ...
719
+ }
720
+ ```
721
+
722
+ The function may be async, and runs whether the rules passed or not. Each call to `after()` adds a function and
723
+ returns the validator, so the calls chain.
724
+
725
+ ## Working with validated input
726
+
727
+ `validate()` and `validated()` return only the fields that have rules, nested as the request sent them: a key without
728
+ rules is left out, and so is a key the request didn't send.
729
+
730
+ ```ts
731
+ const validated = await request().validate({ name: 'required|string', email: 'required|email' })
732
+
733
+ const { name, email } = validated
734
+ ```
735
+
736
+ The data is a plain object. Laravel's `safe()`, with `only()`, `except()` and `merge()`, isn't in Oriel: pick the
737
+ keys you want from the object.
738
+
739
+ ### The validated data's type
740
+
741
+ Each field's type comes from its rules. The type rules decide what a value is, and the presence rules whether the key
742
+ is there:
743
+
744
+ ```ts
745
+ const data = await request().validate({
746
+ title: 'required|string|max:255',
747
+ votes: 'nullable|integer',
748
+ status: 'required|in:draft,published',
749
+ tags: 'array|in:php,laravel',
750
+ 'author.name': 'required|string',
751
+ })
752
+
753
+ const title: string = data.title
754
+ const votes: NumericValue | null | undefined = data.votes
755
+ const status: 'draft' | 'published' = data.status
756
+ const tags: Array<'php' | 'laravel'> | undefined = data.tags
757
+ const author: string = data.author.name
758
+ ```
759
+
760
+ - `string`, `email`, `url`, `date` and the other text rules make a `string`; `file` and `image` an `UploadedFile`.
761
+ - `required`, `accepted` and `declined` put the key in the data and rule out `null`; `present` puts the key in.
762
+ Without one of them, the key is optional.
763
+ - `nullable` adds `null`, unless one of those rules, or `filled`, rules it out.
764
+ - `in:a,b` lets the listed values through, and with `array` makes a list of them.
765
+ - `array` is a list, `unknown[]`, and `array:name,email` an object with those optional keys.
766
+ - Dotted keys nest: `author.name` is `{ author: { name } }`.
767
+ - Rules that only check a value, such as `max`, `confirmed` or `unique`, change nothing, and a field without a type
768
+ rule is `unknown`.
769
+
770
+ > [!NOTE]
771
+ > Laravel returns the input as the request sent it, and so does Oriel: nothing is cast. A form sends text, so a
772
+ > field that passes `integer` is `5` from JSON and `'5'` from a form, and is typed `` number | `${number}` ``, which
773
+ > `NumericValue` names. Convert it where you need a number, with `Number(data.votes)`. In the same way `boolean` is
774
+ > `boolean | 0 | 1 | '0' | '1'`. The types say what passed, so they never promise a number a form didn't send.
775
+
776
+ A few rules make a key optional, or keep it from being typed as a list:
777
+
778
+ - `sometimes` skips a missing field, and `exclude_if`, `exclude_unless`, `exclude_with` and `exclude_without` can take
779
+ a field out: with any of them the key is optional, even with `required`.
780
+ - `prohibited` passes a missing or empty field, so its value is typed `null | string | [] | Record<string, never>`.
781
+ - A `*` level whose parent has no `array` or `list` rule is typed `V[] | Record<string, V>`: the request may send an
782
+ object there, and a sparse match keeps its indexes as keys. Give the parent `array` to make it a list:
783
+
784
+ ```ts
785
+ const data = await request().validate({
786
+ items: 'required|array',
787
+ 'items.*.qty': 'required|integer',
788
+ })
789
+
790
+ const quantities: NumericValue[] = data.items.map((item) => item.qty)
791
+ ```
792
+
793
+ Rule strings TypeScript only knows as `string`, such as a form request's `rules()` without `as const`, give
794
+ optional `unknown` fields.
795
+
796
+ ## Working with error messages
797
+
798
+ A validator's `errors()` returns its messages, by field. `errors()` in a view returns the same kind of bag, read from
799
+ the session:
800
+
801
+ ```ts
802
+ declare const validator: import('@orieljs/framework/contracts').Validator
803
+ // ---cut---
804
+ const errors = validator.errors()
805
+
806
+ const first = errors.first('email')
807
+ ```
808
+
809
+ #### Retrieving all error messages for a field
810
+
811
+ `get()` returns a field's messages, or an empty list. Fields under a wildcard are named by their index, as
812
+ `attachments.0`:
813
+
814
+ ```ts
815
+ declare const errors: import('@orieljs/framework/contracts').MessageBag
816
+ // ---cut---
817
+ for (const message of errors.get('email')) {
818
+ // ...
819
+ }
820
+ ```
821
+
822
+ #### Retrieving all error messages for all fields
823
+
824
+ ```ts
825
+ declare const errors: import('@orieljs/framework/contracts').MessageBag
826
+ // ---cut---
827
+ for (const message of errors.all()) {
828
+ // ...
829
+ }
830
+ ```
831
+
832
+ #### Determining if messages exist for a field
833
+
834
+ ```ts
835
+ declare const errors: import('@orieljs/framework/contracts').MessageBag
836
+ // ---cut---
837
+ if (errors.has('email')) {
838
+ // ...
839
+ }
840
+ ```
841
+
842
+ `isEmpty()` and `count()` tell you whether there are messages at all and how many, and `toObject()` returns them
843
+ as `{ field: [messages] }`.
844
+
845
+ Laravel reads its messages, custom messages and attribute names from `lang/en/validation.php`. Oriel has Laravel's
846
+ English messages built in and no validation language file: give your own messages to the validator or the form
847
+ request.
848
+
849
+ ## Available validation rules
850
+
851
+ The rules by what they check, then each rule in alphabetical order.
852
+
853
+ #### Booleans
854
+
855
+ [`accepted`](#accepted), [`accepted_if`](#accepted_if), [`boolean`](#boolean), [`declined`](#declined),
856
+ [`declined_if`](#declined_if)
857
+
858
+ #### Strings
859
+
860
+ [`active_url`](#active_url), [`alpha`](#alpha), [`alpha_dash`](#alpha_dash), [`alpha_num`](#alpha_num),
861
+ [`ascii`](#ascii), [`confirmed`](#confirmed), [`current_password`](#current_password), [`different`](#different),
862
+ [`doesnt_start_with`](#doesnt_start_with), [`doesnt_end_with`](#doesnt_end_with), [`email`](#email),
863
+ [`ends_with`](#ends_with), [`enum`](#enum), [`hex_color`](#hex_color), [`in`](#in), [`ip`](#ip), [`ipv4`](#ipv4),
864
+ [`ipv6`](#ipv6), [`json`](#json), [`lowercase`](#lowercase), [`mac_address`](#mac_address), [`max`](#max),
865
+ [`min`](#min), [`not_in`](#not_in), [`regex`](#regex), [`not_regex`](#not_regex), [`same`](#same), [`size`](#size),
866
+ [`starts_with`](#starts_with), [`string`](#string), [`uppercase`](#uppercase), [`url`](#url), [`ulid`](#ulid),
867
+ [`uuid`](#uuid)
868
+
869
+ #### Numbers
870
+
871
+ [`between`](#between), [`decimal`](#decimal), [`different`](#different), [`digits`](#digits),
872
+ [`digits_between`](#digits_between), [`gt`](#gt), [`gte`](#gte), [`integer`](#integer), [`lt`](#lt),
873
+ [`lte`](#lte), [`max`](#max), [`max_digits`](#max_digits), [`min`](#min), [`min_digits`](#min_digits),
874
+ [`multiple_of`](#multiple_of), [`numeric`](#numeric), [`same`](#same), [`size`](#size)
875
+
876
+ #### Arrays
877
+
878
+ [`array`](#array), [`between`](#between), [`contains`](#contains), [`doesnt_contain`](#doesnt_contain),
879
+ [`distinct`](#distinct), [`in_array`](#in_array), [`in_array_keys`](#in_array_keys), [`list`](#list),
880
+ [`max`](#max), [`min`](#min), [`size`](#size)
881
+
882
+ #### Dates
883
+
884
+ [`after`](#after), [`after_or_equal`](#after_or_equal), [`before`](#before),
885
+ [`before_or_equal`](#before_or_equal), [`date`](#date), [`date_equals`](#date_equals),
886
+ [`date_format`](#date_format), [`different`](#different), [`timezone`](#timezone)
887
+
888
+ #### Files
889
+
890
+ [`between`](#between), [`dimensions`](#dimensions), [`extensions`](#extensions), [`file`](#file),
891
+ [`image`](#image), [`max`](#max), [`min`](#min), [`mimetypes`](#mimetypes), [`mimes`](#mimes), [`size`](#size)
892
+
893
+ #### Database
894
+
895
+ [`exists`](#exists), [`unique`](#unique)
896
+
897
+ #### Utilities
898
+
899
+ [`bail`](#bail), [`exclude`](#exclude), [`exclude_if`](#exclude_if), [`exclude_unless`](#exclude_unless),
900
+ [`exclude_with`](#exclude_with), [`exclude_without`](#exclude_without), [`filled`](#filled),
901
+ [`missing`](#missing), [`missing_if`](#missing_if), [`missing_unless`](#missing_unless),
902
+ [`missing_with`](#missing_with), [`missing_with_all`](#missing_with_all), [`nullable`](#nullable),
903
+ [`present`](#present), [`present_if`](#present_if), [`present_unless`](#present_unless),
904
+ [`present_with`](#present_with), [`present_with_all`](#present_with_all), [`prohibited`](#prohibited),
905
+ [`prohibited_if`](#prohibited_if), [`prohibited_if_accepted`](#prohibited_if_accepted),
906
+ [`prohibited_if_declined`](#prohibited_if_declined), [`prohibited_unless`](#prohibited_unless),
907
+ [`prohibits`](#prohibits), [`required`](#required), [`required_if`](#required_if),
908
+ [`required_if_accepted`](#required_if_accepted), [`required_if_declined`](#required_if_declined),
909
+ [`required_unless`](#required_unless), [`required_with`](#required_with),
910
+ [`required_with_all`](#required_with_all), [`required_without`](#required_without),
911
+ [`required_without_all`](#required_without_all), [`required_array_keys`](#required_array_keys),
912
+ [`sometimes`](#sometimes)
913
+
914
+ Most rules don't run on a field that is missing or blank text, so they pass it. Only `required`, `accepted`,
915
+ `declined`, `filled`, `present` and `missing`, their `_if`, `_unless`, `_with` and `_without` forms, and the exclude
916
+ rules run on one.
917
+
918
+ #### accepted
919
+
920
+ The field is `'yes'`, `'on'`, `1`, `'1'`, `true` or `'true'`, as a ticked "terms of service" checkbox sends. A
921
+ missing field fails.
922
+
923
+ #### accepted_if
924
+
925
+ `accepted_if:anotherfield,value,...`: the field is accepted when the other field holds one of the values.
926
+
927
+ #### active_url
928
+
929
+ The URL's host has an address. The value is read as a URL, and its host looked up.
930
+
931
+ > [!NOTE]
932
+ > Laravel asks DNS for the host's `A` or `AAAA` records. Oriel asks the system's resolver with `dns.lookup()`, the
933
+ > lookup Node makes to connect to a host, so the rule passes a host your app could reach by name: one only your
934
+ > machine knows, such as `localhost` or one in `/etc/hosts`, passes too. The lookup has no timeout of its own.
935
+
936
+ #### after
937
+
938
+ `after:date`: a date after the one given. The parameter is another field's name, `now`, `today`, `tomorrow`,
939
+ `yesterday`, an offset such as `+1 week` or `-3 days`, or a date. With `date_format` on the field, the value and the
940
+ date are read with that format. A parameter that is neither, and doesn't look like a field's name, is an error in
941
+ your rules (a 500). Laravel reads any phrase PHP's `strtotime()` knows; Oriel reads only these, and takes another
942
+ word, such as `monday`, for a field's name. A number passes as a timestamp, as in Laravel, so the value is typed
943
+ `string | number`, or `string` with `date` among the rules; the other date comparisons are typed the same way.
944
+
945
+ #### after_or_equal
946
+
947
+ `after_or_equal:date`: a date after or equal to the one given, read as `after` reads it.
948
+
949
+ #### alpha
950
+
951
+ Only the letters `A` to `Z` and `a` to `z`. Laravel's accepts letters of any script unless you write `alpha:ascii`;
952
+ Oriel's accepts these letters only.
953
+
954
+ #### alpha_dash
955
+
956
+ Only the letters `A` to `Z` and `a` to `z`, digits, `-` and `_`.
957
+
958
+ #### alpha_num
959
+
960
+ Only the letters `A` to `Z` and `a` to `z`, and digits.
961
+
962
+ #### array
963
+
964
+ A list: a JavaScript array. `array:name,email` is an object whose keys are all among those named, as is
965
+ `Rule.array(['name', 'email'])`:
966
+
967
+ ```ts
968
+ const data = await request().validate({ user: 'required|array:name,username' })
969
+ ```
970
+
971
+ > [!NOTE]
972
+ > Laravel's `array` also accepts an associative array, which is what a JSON object becomes in PHP. In JavaScript an
973
+ > object isn't a list, so Oriel's `array` refuses one and types the field `unknown[]`: typing an object as a list
974
+ > would lie. Check an object's keys with `array:a,b` or `Rule.array([...])`. Both refuse an empty list, `[]`, which
975
+ > some clients send for an empty object.
976
+
977
+ #### ascii
978
+
979
+ Only printable ASCII characters, tabs and line breaks: text or a number.
980
+
981
+ #### bail
982
+
983
+ Stops the field's rules at its first failure (see
984
+ [stopping on first validation failure](#stopping-on-first-validation-failure)).
985
+
986
+ #### before
987
+
988
+ `before:date`: a date before the one given, read as `after` reads it.
989
+
990
+ #### before_or_equal
991
+
992
+ `before_or_equal:date`: a date before or equal to the one given, read as `after` reads it.
993
+
994
+ #### between
995
+
996
+ `between:min,max`: a size between `min` and `max`, both included. Sizes are measured as [`size`](#size) measures them.
997
+
998
+ #### boolean
999
+
1000
+ `true`, `false`, `1`, `0`, `'1'` or `'0'`. `boolean:strict` accepts only `true` and `false`. The text `'true'` fails,
1001
+ as in Laravel.
1002
+
1003
+ #### confirmed
1004
+
1005
+ The field equals `<field>_confirmation`: `password` is compared with `password_confirmation`.
1006
+ `confirmed:repeat_password` names the other field.
1007
+
1008
+ #### contains
1009
+
1010
+ `contains:foo,bar`: an array that holds every value given. `1` and `'1'` count as the same value. `Rule.contains([...])`
1011
+ does the same.
1012
+
1013
+ #### current_password
1014
+
1015
+ The signed-in user's password. `current_password:api` checks the user of that guard. A guest fails.
1016
+
1017
+ #### date
1018
+
1019
+ Text that reads as a date: an ISO date, with or without a time, a date such as `10/19/2026`, month first, or one
1020
+ with a month name. A `YYYY-MM-DD` date without a time must be a real day.
1021
+
1022
+ #### date_equals
1023
+
1024
+ `date_equals:date`: the same date as the one given, read as `after` reads it.
1025
+
1026
+ #### date_format
1027
+
1028
+ `date_format:format,...`: a date in one of the formats, written with PHP's format characters, as
1029
+ `date_format:Y-m-d`. The value must read with the format and write back the same, so `2026-02-30` fails. Oriel
1030
+ reads the characters `d j D l S F M m n Y y a A g h G H i s v u O P p U`, and `\` to escape one; a format with one of
1031
+ PHP's other characters, such as `T` or `z`, is an error in your rules (a 500). Commas separate the formats, so a
1032
+ format can't hold one.
1033
+
1034
+ #### decimal
1035
+
1036
+ `decimal:min,max`: a number with `min` decimal places, or between `min` and `max`: `decimal:2` takes `1.25`.
1037
+
1038
+ #### declined
1039
+
1040
+ The field is `'no'`, `'off'`, `0`, `'0'`, `false` or `'false'`. A missing field fails.
1041
+
1042
+ #### declined_if
1043
+
1044
+ `declined_if:anotherfield,value,...`: the field is declined when the other field holds one of the values.
1045
+
1046
+ #### different
1047
+
1048
+ `different:field`: the field's value isn't the other field's. The values are compared exactly, so `1` and `'1'` are
1049
+ different.
1050
+
1051
+ #### digits
1052
+
1053
+ `digits:value`: only digits, exactly `value` of them.
1054
+
1055
+ #### digits_between
1056
+
1057
+ `digits_between:min,max`: only digits, between `min` and `max` of them.
1058
+
1059
+ #### dimensions
1060
+
1061
+ `dimensions:min_width=100,min_height=200`: an image whose size meets the constraints. The constraints are `width`,
1062
+ `height`, `min_width`, `max_width`, `min_height`, `max_height` and `ratio`, written `3/2` or `1.5`. The size is read
1063
+ from the image's header, for JPEG, PNG, GIF, WebP, AVIF and BMP; an SVG fails. `Rule.dimensions()` builds the rule:
1064
+
1065
+ ```ts
1066
+ const rule = Rule.dimensions().maxWidth(1000).maxHeight(500).ratio(3 / 2)
1067
+ ```
1068
+
1069
+ #### distinct
1070
+
1071
+ On a list's items, none holds the same value as another: `'tags.*': 'distinct'`. `1` and `'1'` count as the same;
1072
+ `distinct:strict` compares exactly, and `distinct:ignore_case` ignores case.
1073
+
1074
+ #### doesnt_contain
1075
+
1076
+ `doesnt_contain:foo,bar`: an array that holds none of the values. `Rule.doesntContain([...])` does the same.
1077
+
1078
+ #### doesnt_end_with
1079
+
1080
+ `doesnt_end_with:foo,bar`: text or a number that doesn't end with any of the values.
1081
+
1082
+ #### doesnt_start_with
1083
+
1084
+ `doesnt_start_with:foo,bar`: text or a number that doesn't start with any of the values.
1085
+
1086
+ #### email
1087
+
1088
+ An email address: one `@` with text before it, no spaces, and a dot inside the part after it.
1089
+
1090
+ > [!NOTE]
1091
+ > Laravel's `email` checks the address against RFC 5322 with PHP's `egulias/email-validator`, and its styles (`rfc`,
1092
+ > `strict`, `dns`, `spoof`, `filter`, `filter_unicode`) choose the checks. Those are PHP libraries' checks, so
1093
+ > Oriel's `email` is one pattern: it accepts the styles as parameters and doesn't act on them.
1094
+
1095
+ #### ends_with
1096
+
1097
+ `ends_with:foo,bar`: text or a number that ends with one of the values.
1098
+
1099
+ #### enum
1100
+
1101
+ One of an enum's values, with `Rule.enum()`. It takes a TypeScript enum, or an object of values `as const`, and
1102
+ types the field with the enum's values:
1103
+
1104
+ ```ts
1105
+ enum ServerStatus {
1106
+ Active = 'active',
1107
+ Inactive = 'inactive',
1108
+ }
1109
+
1110
+ const data = await request().validate({
1111
+ status: ['required', Rule.enum(ServerStatus)],
1112
+ })
1113
+
1114
+ const status: ServerStatus = data.status
1115
+ ```
1116
+
1117
+ `only()` and `except()` narrow the values:
1118
+
1119
+ ```ts
1120
+ enum ServerStatus {
1121
+ Active = 'active',
1122
+ Inactive = 'inactive',
1123
+ }
1124
+ // ---cut---
1125
+ Rule.enum(ServerStatus).only([ServerStatus.Active])
1126
+ ```
1127
+
1128
+ A numeric enum takes its numbers, or exactly their text, `'1'` but not `'01'`, and its field is typed as either.
1129
+
1130
+ #### exclude
1131
+
1132
+ The field is left out of the validated data, and the rules after it don't run.
1133
+
1134
+ #### exclude_if
1135
+
1136
+ `exclude_if:anotherfield,value`: the field is left out when the other field holds the value, and nothing happens
1137
+ while the other field is missing. Write the exclude rules first: the rules before one run before it decides.
1138
+
1139
+ ```ts
1140
+ declare const data: Record<string, unknown>
1141
+ // ---cut---
1142
+ const validator = Validator.make(data, {
1143
+ has_appointment: 'required|boolean',
1144
+ appointment_date: 'exclude_if:has_appointment,false|required|date',
1145
+ doctor_name: 'exclude_if:has_appointment,false|required|string',
1146
+ })
1147
+ ```
1148
+
1149
+ #### exclude_unless
1150
+
1151
+ `exclude_unless:anotherfield,value`: the field is left out unless the other field holds the value.
1152
+
1153
+ #### exclude_with
1154
+
1155
+ `exclude_with:anotherfield`: the field is left out when the other field's key is there.
1156
+
1157
+ #### exclude_without
1158
+
1159
+ `exclude_without:anotherfield,...`: the field is left out when one of the others is missing or empty.
1160
+
1161
+ #### exists
1162
+
1163
+ `exists:table,column`: a row of the table holds the value in the column. Without a column, the field's name is the
1164
+ column, or its last part for a list's items (`email` for `users.*.email`). The query runs on the default
1165
+ connection. `Rule.exists('users', 'email')` does the same. Laravel's rule also takes a model's class, a
1166
+ `connection.table` and extra `where` conditions, which Oriel's doesn't.
1167
+
1168
+ #### extensions
1169
+
1170
+ `extensions:jpg,png`: an uploaded file whose name ends with one of the extensions. It checks the name only: pair it
1171
+ with `mimes` or `image`.
1172
+
1173
+ #### file
1174
+
1175
+ An uploaded file.
1176
+
1177
+ #### filled
1178
+
1179
+ When the key is there, its value isn't empty. A missing key passes.
1180
+
1181
+ #### gt
1182
+
1183
+ `gt:field`: greater than the other field. Numbers, and the text of numbers, compare by value, and two values of one
1184
+ kind (text, lists, files) by size; values of different kinds fail. `gt:10` compares a numeric value with the number,
1185
+ and any other value fails.
1186
+
1187
+ #### gte
1188
+
1189
+ `gte:field`: greater than or equal to the other field, compared as `gt` compares.
1190
+
1191
+ #### hex_color
1192
+
1193
+ A hexadecimal colour: `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa`.
1194
+
1195
+ #### image
1196
+
1197
+ An image, by its bytes: JPEG, PNG, GIF, WebP, AVIF or BMP. `image:allow_svg` accepts SVG too, which can carry
1198
+ scripts.
1199
+
1200
+ #### in
1201
+
1202
+ `in:foo,bar`: the value's text is one of those listed, so `in:1,2` takes `1` and `'1'`. `Rule.in([...])` does the
1203
+ same, and types the field with its values. An object fails, and so does an array unless the field also has `array`;
1204
+ then every item must be listed:
1205
+
1206
+ ```ts
1207
+ const data = await request().validate({
1208
+ zones: 'required|array|in:first-zone,second-zone',
1209
+ })
1210
+ ```
1211
+
1212
+ A comma always separates the values: Laravel's quoted values (`in:"a,b",c`) aren't read.
1213
+
1214
+ #### in_array
1215
+
1216
+ `in_array:anotherfield.*`: the value is one of the other field's values.
1217
+
1218
+ #### in_array_keys
1219
+
1220
+ `in_array_keys:foo,bar`: an object with at least one of the keys.
1221
+
1222
+ #### integer
1223
+
1224
+ A whole number: `5`, or text of digits with an optional minus, `'5'` or `'-3'`. `integer:strict` refuses text. The
1225
+ field is typed `NumericValue` (`number` with `:strict`), and nothing is converted.
1226
+
1227
+ #### ip
1228
+
1229
+ An IPv4 or IPv6 address.
1230
+
1231
+ #### ipv4
1232
+
1233
+ An IPv4 address.
1234
+
1235
+ #### ipv6
1236
+
1237
+ An IPv6 address.
1238
+
1239
+ #### json
1240
+
1241
+ Text that is valid JSON: `'{"name":"Taylor"}'` or `'5'`.
1242
+
1243
+ > [!NOTE]
1244
+ > Laravel's `json` also passes a number, or `true`, as PHP turns them into JSON text first. A number, a boolean or an
1245
+ > object in a JSON body has already been read from JSON, so Oriel's `json` takes text only.
1246
+
1247
+ #### list
1248
+
1249
+ A list: a JavaScript array.
1250
+
1251
+ #### lowercase
1252
+
1253
+ Text or a number with no upper case letters.
1254
+
1255
+ #### lt
1256
+
1257
+ `lt:field`: less than the other field, compared as `gt` compares.
1258
+
1259
+ #### lte
1260
+
1261
+ `lte:field`: less than or equal to the other field, compared as `gt` compares.
1262
+
1263
+ #### mac_address
1264
+
1265
+ A MAC address: six pairs of hexadecimal digits joined all by `-` or all by `:`, or three groups of four joined by `.`.
1266
+
1267
+ #### max
1268
+
1269
+ `max:value`: a size of `value` or less, measured as [`size`](#size) measures it.
1270
+
1271
+ #### max_digits
1272
+
1273
+ `max_digits:value`: only digits, at most `value` of them.
1274
+
1275
+ #### mimes
1276
+
1277
+ `mimes:jpg,pdf`: an uploaded file of one of the types; `jpg` and `jpeg` are one. Oriel recognizes JPEG, PNG, GIF,
1278
+ WebP, AVIF, BMP, SVG, PDF, HTML and XML by their bytes, and a file of one of these kinds passes only by its own type.
1279
+ Plain text, also read from the bytes, passes `txt`, or its name's extension, such as `csv` or `yaml`, when the list
1280
+ names it and it isn't one of the kinds above. A file of a kind the bytes don't show, such as a `docx` or an `mp4`,
1281
+ passes by its name's extension, unless that extension names one of the kinds above or a text format such as `csv`.
1282
+ An empty file fails.
1283
+
1284
+ #### mimetypes
1285
+
1286
+ `mimetypes:text/plain,video/*`: an uploaded file whose type, read from its bytes, is listed, or matches a `type/*`. A
1287
+ file of a kind Oriel doesn't recognize is `application/octet-stream`.
1288
+
1289
+ #### min
1290
+
1291
+ `min:value`: a size of `value` or more, measured as [`size`](#size) measures it.
1292
+
1293
+ #### min_digits
1294
+
1295
+ `min_digits:value`: only digits, at least `value` of them.
1296
+
1297
+ #### missing
1298
+
1299
+ The key isn't there. A key holding `null` is there.
1300
+
1301
+ #### missing_if
1302
+
1303
+ `missing_if:anotherfield,value,...`: the key isn't there when the other field holds one of the values.
1304
+
1305
+ #### missing_unless
1306
+
1307
+ `missing_unless:anotherfield,value`: the key isn't there unless the other field holds the value.
1308
+
1309
+ #### missing_with
1310
+
1311
+ `missing_with:foo,bar`: the key isn't there when one of the other keys is.
1312
+
1313
+ #### missing_with_all
1314
+
1315
+ `missing_with_all:foo,bar`: the key isn't there when all of the other keys are.
1316
+
1317
+ #### multiple_of
1318
+
1319
+ `multiple_of:value`: a multiple of the number, in exact decimals: `0.3` is a multiple of `0.1`.
1320
+
1321
+ #### not_in
1322
+
1323
+ `not_in:foo,bar`: the value's text isn't one of those listed. `Rule.notIn([...])` does the same. An array or an object
1324
+ passes, unless the field has `array`; then it fails only when every item is listed. Pair `not_in` with `string` to
1325
+ refuse a list, or use `doesnt_contain` to refuse a list holding any of the values.
1326
+
1327
+ #### not_regex
1328
+
1329
+ `not_regex:pattern`: text or a number the pattern doesn't match.
1330
+
1331
+ #### nullable
1332
+
1333
+ `null` passes the field's other rules. See [a note on optional fields](#a-note-on-optional-fields).
1334
+
1335
+ #### numeric
1336
+
1337
+ A number, or text of one, as PHP's `is_numeric()` reads it: a sign, digits, a decimal point and an exponent, with
1338
+ spaces around. Hexadecimal text and `Infinity` fail. `numeric:strict` refuses text. The field is typed
1339
+ `NumericValue`, and nothing is converted.
1340
+
1341
+ #### present
1342
+
1343
+ The key is there, whatever its value, `null` and empty text included.
1344
+
1345
+ #### present_if
1346
+
1347
+ `present_if:anotherfield,value,...`: the key is there when the other field holds one of the values.
1348
+
1349
+ #### present_unless
1350
+
1351
+ `present_unless:anotherfield,value`: the key is there unless the other field holds the value.
1352
+
1353
+ #### present_with
1354
+
1355
+ `present_with:foo,bar`: the key is there when one of the other keys is.
1356
+
1357
+ #### present_with_all
1358
+
1359
+ `present_with_all:foo,bar`: the key is there when all of the other keys are.
1360
+
1361
+ #### prohibited
1362
+
1363
+ The field is missing or empty: `null`, blank text, `[]` or `{}`.
1364
+
1365
+ #### prohibited_if
1366
+
1367
+ `prohibited_if:anotherfield,value,...`: the field is missing or empty when the other field holds one of the values.
1368
+ `Rule.prohibitedIf(condition)` prohibits the field when the condition, or the function you give, is true.
1369
+
1370
+ #### prohibited_if_accepted
1371
+
1372
+ `prohibited_if_accepted:anotherfield`: the field is missing or empty when the other field is accepted.
1373
+
1374
+ #### prohibited_if_declined
1375
+
1376
+ `prohibited_if_declined:anotherfield`: the field is missing or empty when the other field is declined.
1377
+
1378
+ #### prohibited_unless
1379
+
1380
+ `prohibited_unless:anotherfield,value,...`: the field is missing or empty unless the other field holds one of the
1381
+ values.
1382
+
1383
+ #### prohibits
1384
+
1385
+ `prohibits:anotherfield,...`: when the field has a value, the others are missing or empty.
1386
+
1387
+ #### regex
1388
+
1389
+ `regex:pattern`: text the pattern matches, written as a JavaScript regular expression, with or without slashes and
1390
+ flags: `regex:/^[a-z]+$/i`. A pattern with a `|` must go in a list of rules, as a rule string splits on `|`.
1391
+
1392
+ #### required
1393
+
1394
+ The field is there and not empty: not `null`, not text made only of spaces, not `[]` and not `{}`. `0` and `false`
1395
+ pass. `Rule.requiredIf(condition)` requires the field when the condition, or the function you give, is true.
1396
+
1397
+ #### required_array_keys
1398
+
1399
+ `required_array_keys:foo,bar`: an object with every one of the keys. A list fails, `[]` included.
1400
+
1401
+ #### required_if
1402
+
1403
+ `required_if:anotherfield,value,...`: required when the other field holds one of the values. `true` and `false`
1404
+ match a boolean, and `'1'` and `'0'` too when the other field has the `boolean` rule; `null` matches a `null` field.
1405
+ Nothing is required while the other field is missing.
1406
+
1407
+ #### required_if_accepted
1408
+
1409
+ `required_if_accepted:anotherfield`: required when the other field is accepted.
1410
+
1411
+ #### required_if_declined
1412
+
1413
+ `required_if_declined:anotherfield`: required when the other field is declined.
1414
+
1415
+ #### required_unless
1416
+
1417
+ `required_unless:anotherfield,value,...`: required unless the other field holds one of the values.
1418
+
1419
+ #### required_with
1420
+
1421
+ `required_with:foo,bar`: required when one of the other fields has a value.
1422
+
1423
+ #### required_with_all
1424
+
1425
+ `required_with_all:foo,bar`: required when all of the other fields have a value.
1426
+
1427
+ #### required_without
1428
+
1429
+ `required_without:foo,bar`: required when one of the other fields is missing or empty.
1430
+
1431
+ #### required_without_all
1432
+
1433
+ `required_without_all:foo,bar`: required when all of the other fields are missing or empty.
1434
+
1435
+ #### same
1436
+
1437
+ `same:field`: the same value as the other field, compared exactly.
1438
+
1439
+ #### size
1440
+
1441
+ `size:value`: a size of exactly `value`. What a size is depends on the field:
1442
+
1443
+ - with `numeric`, `integer` or `decimal` among its rules, the number itself;
1444
+ - for a list, its number of items, and for an object, its number of keys;
1445
+ - for an uploaded file, its size in kilobytes;
1446
+ - for anything else, its length in characters. A number without a numeric rule is measured by its text: `150` is 3.
1447
+
1448
+ #### sometimes
1449
+
1450
+ The field's rules run only when its key is there: `email: 'sometimes|required|email'` checks an email that is sent,
1451
+ and lets a request leave it out.
1452
+
1453
+ #### starts_with
1454
+
1455
+ `starts_with:foo,bar`: text or a number that starts with one of the values.
1456
+
1457
+ #### string
1458
+
1459
+ A string.
1460
+
1461
+ #### timezone
1462
+
1463
+ A time zone name, in its own case: `Europe/London`, not `europe/london`. Without a parameter, a zone in one of PHP's
1464
+ regions, or `UTC`. `timezone:Europe` takes that region's zones, `timezone:utc` only UTC, `timezone:all_with_bc` any
1465
+ name the runtime knows (`US/Eastern` too), and `timezone:per_country,US` the zones of a country.
1466
+
1467
+ > [!NOTE]
1468
+ > Laravel checks the name against PHP's list of zones. PHP's lists can't be read from Node or Bun, so Oriel checks it
1469
+ > against the time zone data built into the runtime. Old names that the data keeps as links, such as
1470
+ > `Asia/Calcutta`, pass as zones. On Node, a link that the data reads as another zone is checked by its shape only, so
1471
+ > its case isn't checked: `Asia/ISTANBUL` passes on Node and fails on Bun.
1472
+
1473
+ #### ulid
1474
+
1475
+ A ULID.
1476
+
1477
+ #### unique
1478
+
1479
+ `unique:table,column`: no row of the table holds the value in the column. Without a column, the field's name is the
1480
+ column, or its last part for a list's items. To ignore one row, the user's own when they update their profile, use
1481
+ `Rule.unique()`:
1482
+
1483
+ ```ts
1484
+ declare const user: User
1485
+ // ---cut---
1486
+ const data = await request().validate({
1487
+ email: ['required', 'email', Rule.unique('users').ignore(user)],
1488
+ })
1489
+ ```
1490
+
1491
+ `ignore()` takes a model or an id, and the id's column as its second argument. The string rule takes them as
1492
+ `unique:users,email,5,id`. The query runs on the default connection. Laravel's rule also takes a model's class, a
1493
+ `connection.table` and extra `where` conditions, which Oriel's doesn't.
1494
+
1495
+ #### uppercase
1496
+
1497
+ Text or a number with no lower case letters.
1498
+
1499
+ #### url
1500
+
1501
+ A URL: one of Laravel's schemes followed by `://` and a host, so `javascript:` and `mailto:` URLs fail.
1502
+ `url:http,https` allows only those schemes.
1503
+
1504
+ #### uuid
1505
+
1506
+ A UUID of any version. `uuid:4` takes version 4 only; the versions are `1` to `8`, `nil` (or `0`) and `max`.
1507
+
1508
+ ## Conditionally adding rules
1509
+
1510
+ #### Skipping validation when fields have certain values
1511
+
1512
+ `exclude_if` and `exclude_unless` leave a field out of validation, and of the validated data, depending on another
1513
+ field (see [`exclude_if`](#exclude_if)).
1514
+
1515
+ #### Validating when present
1516
+
1517
+ `sometimes` validates a field only when the request sends it:
1518
+
1519
+ ```ts
1520
+ declare const data: Record<string, unknown>
1521
+ // ---cut---
1522
+ const validator = Validator.make(data, {
1523
+ email: 'sometimes|required|email',
1524
+ })
1525
+ ```
1526
+
1527
+ #### Complex conditional validation
1528
+
1529
+ `Rule.when()` chooses a field's rules from the data: its function receives the data, and the rules apply when it
1530
+ returns `true`. A third argument gives the rules for `false`, and `Rule.unless()` is the opposite:
1531
+
1532
+ ```ts
1533
+ const data = await request().validate({
1534
+ games: 'required|integer|min:0',
1535
+ reason: [Rule.when((input) => Number(input.games) >= 100, 'required|max:500', 'nullable')],
1536
+ })
1537
+ ```
1538
+
1539
+ Laravel's validator also has a `sometimes()` method that adds rules after the validator is made. Oriel's doesn't:
1540
+ give the field a `Rule.when()`.
1541
+
1542
+ ## Validating arrays
1543
+
1544
+ `array` with keys, `array:name,username`, checks that an object has only the keys you name. Without one, an object
1545
+ fails `array` (see [`array`](#array)):
1546
+
1547
+ ```ts
1548
+ declare const input: Record<string, unknown>
1549
+ // ---cut---
1550
+ const validator = Validator.make(input, {
1551
+ user: 'array:name,username',
1552
+ })
1553
+ ```
1554
+
1555
+ ### Validating nested array input
1556
+
1557
+ Dots name the fields inside an object, and `*` every item of a list or every value of an object:
1558
+
1559
+ ```ts
1560
+ declare const input: Record<string, unknown>
1561
+ // ---cut---
1562
+ const validator = Validator.make(input, {
1563
+ 'photos.profile': 'required|image',
1564
+ 'users.*.email': 'email|unique:users',
1565
+ 'users.*.first_name': 'required_with:users.*.last_name',
1566
+ })
1567
+ ```
1568
+
1569
+ Here `required_with` fills the `*` of `users.*.last_name` with the item's index, so each user's `first_name` is
1570
+ checked against the same user's `last_name`. The rules that compare two fields, such as `same`, `gt`, `after` and the
1571
+ `_if` and `_with` rules, do the same; `in_array` reads every item. Errors are named by the item, as `users.2.email`.
1572
+ Laravel's `:index` and `:position` placeholders, and `Rule::forEach()`, aren't in Oriel.
1573
+
1574
+ ## Validating files
1575
+
1576
+ The file rules check an uploaded file: `file`, `image`, `mimes`, `mimetypes`, `extensions` and `dimensions`, with
1577
+ `size`, `min`, `max` and `between` in kilobytes:
1578
+
1579
+ ```ts
1580
+ const data = await request().validate({
1581
+ photo: ['required', 'image', 'max:2048', Rule.dimensions().maxWidth(1000).maxHeight(500)],
1582
+ })
1583
+ ```
1584
+
1585
+ `image`, `mimes` and `mimetypes` judge a file by its bytes, not by the type the browser sent; `mimes` reads the
1586
+ name only for plain text and for the kinds the bytes don't show (see [`mimes`](#mimes)). A file named like a PHP
1587
+ script, such as `.php` or `.phtml`, fails `image`, and fails `mimes`, `mimetypes` and `extensions` unless they list
1588
+ `php`. A body larger than the [body size limit](requests.md#body-size-limits) is refused before your rules run.
1589
+ Laravel's `File::types()` builder isn't in Oriel: write the rules.
1590
+
1591
+ ## Validating passwords
1592
+
1593
+ `Rule.password()` checks a password against your app's password rule: by default, text of at least 8 characters.
1594
+ `PasswordRule` builds a stricter one:
1595
+
1596
+ ```ts
1597
+ // Require at least 8 characters...
1598
+ PasswordRule.min(8)
1599
+
1600
+ // Require at least one letter...
1601
+ PasswordRule.min(8).letters()
1602
+
1603
+ // Require at least one uppercase and one lowercase letter...
1604
+ PasswordRule.min(8).mixedCase()
1605
+
1606
+ // Require at least one number...
1607
+ PasswordRule.min(8).numbers()
1608
+
1609
+ // Require at least one symbol...
1610
+ PasswordRule.min(8).symbols()
1611
+ ```
1612
+
1613
+ `max()` sets a maximum length. Each check that fails adds its own message.
1614
+
1615
+ #### Defining default password rules
1616
+
1617
+ `PasswordRule.defaults()` sets the rule `Rule.password()` returns. Call it in a service provider's `boot()`:
1618
+
1619
+ ```ts title="app/Providers/AppServiceProvider.ts"
1620
+ import { app, PasswordRule, ServiceProvider } from '@orieljs/framework'
1621
+
1622
+ export class AppServiceProvider extends ServiceProvider {
1623
+ override boot(): void {
1624
+ PasswordRule.defaults(() => {
1625
+ const rule = PasswordRule.min(8)
1626
+
1627
+ return app().environment('production') ? rule.mixedCase().numbers() : rule
1628
+ })
1629
+ }
1630
+ }
1631
+ ```
1632
+
1633
+ Then `Rule.password()` returns that rule wherever you use it, as the starter kits do:
1634
+
1635
+ ```ts
1636
+ const data = await request().validate({
1637
+ password: ['required', 'confirmed', Rule.password()],
1638
+ })
1639
+ ```
1640
+
1641
+ Laravel's rule is the `Password` class; Oriel's `Password` is the password broker, so the rule is `PasswordRule`.
1642
+ Laravel's `uncompromised()`, which asks an outside service whether a password has leaked, and `rules()` aren't in
1643
+ Oriel.
1644
+
1645
+ ## Custom validation rules
1646
+
1647
+ ### Rule objects
1648
+
1649
+ A rule object is a class with a `validate()` method. `make:rule` writes one in `app/Rules`:
1650
+
1651
+ ```sh
1652
+ node oriel make:rule Uppercase
1653
+ ```
1654
+
1655
+ `validate()` receives the field's name, its value, a `fail()` function to call with a message, and all of the data
1656
+ under validation:
1657
+
1658
+ ```ts title="app/Rules/Uppercase.ts"
1659
+ import type { ValidationRule } from '@orieljs/framework/contracts'
1660
+
1661
+ export class Uppercase implements ValidationRule {
1662
+ validate(attribute: string, value: unknown, fail: (message: string) => void): void {
1663
+ if (typeof value !== 'string' || value.toUpperCase() !== value) {
1664
+ fail(`The ${attribute} field must be uppercase.`)
1665
+ }
1666
+ }
1667
+ }
1668
+ ```
1669
+
1670
+ Put an instance in the field's list of rules:
1671
+
1672
+ ```ts
1673
+ // @filename: app/Rules/Uppercase.ts
1674
+ export declare class Uppercase {
1675
+ validate(attribute: string, value: unknown, fail: (message: string) => void): void
1676
+ }
1677
+ // ---cut---
1678
+ import { Uppercase } from './app/Rules/Uppercase'
1679
+
1680
+ const data = await request().validate({
1681
+ name: ['required', 'string', new Uppercase()],
1682
+ })
1683
+ ```
1684
+
1685
+ `validate()` may be async. `:attribute` in the message becomes the field's name, as in the built-in messages, and
1686
+ your custom attribute names apply to it. Each call to `fail()` adds a message. The fourth argument, the data,
1687
+ replaces Laravel's `DataAwareRule`.
1688
+
1689
+ A rule object doesn't change the field's type unless it says what passes. Declare a `__typing` field, which is
1690
+ never set:
1691
+
1692
+ ```ts title="app/Rules/Uppercase.ts"
1693
+ import type { TypedValidationRule } from '@orieljs/framework/contracts'
1694
+
1695
+ export class Uppercase implements TypedValidationRule<{ value: string }> {
1696
+ declare readonly __typing?: { value: string }
1697
+
1698
+ validate(attribute: string, value: unknown, fail: (message: string) => void): void {
1699
+ if (typeof value !== 'string' || value.toUpperCase() !== value) {
1700
+ fail(`The ${attribute} field must be uppercase.`)
1701
+ }
1702
+ }
1703
+ }
1704
+ ```
1705
+
1706
+ ### Using closures
1707
+
1708
+ A rule you need once can be an object with a `validate()` method, written in place:
1709
+
1710
+ ```ts
1711
+ const validator = Validator.make(request().all(), {
1712
+ title: [
1713
+ 'required',
1714
+ 'max:255',
1715
+ {
1716
+ validate(attribute: string, value: unknown, fail: (message: string) => void) {
1717
+ if (value === 'foo') {
1718
+ fail(`The ${attribute} is invalid.`)
1719
+ }
1720
+ },
1721
+ },
1722
+ ],
1723
+ })
1724
+ ```
1725
+
1726
+ Laravel takes a closure in the list of rules; Oriel's list takes strings and rule objects, so the closure becomes an
1727
+ object's `validate()`.
1728
+
1729
+ ### Registering string rules
1730
+
1731
+ `Validator.extend()` registers a rule you write as a string, with its message, in a service provider's `boot()`. Its
1732
+ function receives the field's name, its value, the rule's parameters and the data, and returns whether the value
1733
+ passes:
1734
+
1735
+ ```ts title="app/Providers/AppServiceProvider.ts"
1736
+ import { ServiceProvider, Validator } from '@orieljs/framework'
1737
+
1738
+ export class AppServiceProvider extends ServiceProvider {
1739
+ override boot(): void {
1740
+ Validator.extend(
1741
+ 'slug',
1742
+ (attribute, value) => typeof value === 'string' && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value),
1743
+ 'The :attribute field must be a slug.',
1744
+ )
1745
+ }
1746
+ }
1747
+ ```
1748
+
1749
+ The rule's message fills `:attribute`, and custom messages replace it as they replace a built-in rule's. A rule
1750
+ registered with a built-in rule's name replaces the built-in rule. To type the fields that pass the rule, add it to
1751
+ `ValidationRuleTypes` in a declaration file:
1752
+
1753
+ ```ts title="types/validation.d.ts"
1754
+ import type {} from '@orieljs/framework/contracts'
1755
+
1756
+ declare module '@orieljs/framework/contracts' {
1757
+ interface ValidationRuleTypes<Parameters extends string = string> {
1758
+ slug: { value: string }
1759
+ }
1760
+ }
1761
+
1762
+ export {}
1763
+ ```
1764
+
1765
+ ### Implicit rules
1766
+
1767
+ Rule objects and the rules of `Validator.extend()` don't run on a field that is missing or blank text, as most
1768
+ built-in rules don't. Laravel's implicit rules, which run on such a field too, aren't in Oriel: combine your rule with
1769
+ `required`.
1770
+
1771
+ ## Rules that don't apply to Node
1772
+
1773
+ Some of Laravel's rules check what PHP does, and have no counterpart on Node and Bun:
1774
+
1775
+ - **`email`'s styles**: `rfc`, `strict`, `spoof`, `filter` and `filter_unicode` are the checks of PHP's
1776
+ `egulias/email-validator` and `filter_var()`. Oriel's `email` accepts them and doesn't act on them (see
1777
+ [`email`](#email)).
1778
+ - **The `uploaded` failure**: Laravel fails a file PHP's own body parser couldn't receive whole. Oriel reads the body
1779
+ itself, so a file that arrives is whole.
1780
+ - **`mimes` and `mimetypes`**: they judge the bytes with Oriel's own detector, not PHP's `finfo`. A file of a kind
1781
+ Oriel doesn't recognize passes `mimes` by its name's extension, and is `application/octet-stream` to `mimetypes`
1782
+ (see [`mimes`](#mimes)).
1783
+
1784
+ Others apply, with the runtime's data:
1785
+
1786
+ - **`timezone`** checks against the runtime's time zone data, not PHP's lists (see [`timezone`](#timezone)).
1787
+ - **`active_url`** asks the system's resolver (see [`active_url`](#active_url)).
1788
+ - **`date_format`** reads the format characters listed in [`date_format`](#date_format); PHP's others are an error.
1789
+
1790
+ And some aren't in Oriel yet: `email:dns`, `encoding`, `Password::uncompromised()`, Laravel's quoted parameters
1791
+ (`in:"a,b",c`), and the builders `Rule::forEach()`, `Rule::date()`, `Rule::email()`, `Rule::numeric()`,
1792
+ `Rule::file()`, `Rule::imageFile()`, `Rule::anyOf()` and `Rule::can()`.