@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.
- package/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
- package/dist/chunk-4MWG3B7G.js.map +1 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- 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()`.
|