@orieljs/framework 0.1.0-alpha.5 → 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 (87) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/chunk-7ZQZYDOC.js +3253 -0
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +535 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +2202 -5952
  16. package/dist/index.js +4900 -944
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +19 -5
  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-A5XRLEWM.js.map +0 -1
  83. package/dist/chunk-BSQKOLSK.js.map +0 -1
  84. package/dist/chunk-PM5YTG57.js +0 -880
  85. package/dist/chunk-PM5YTG57.js.map +0 -1
  86. package/dist/chunk-WNJP46B7.js.map +0 -1
  87. package/dist/index.d-DOlTeG-8.d.ts +0 -99
@@ -0,0 +1,1281 @@
1
+ # Helpers
2
+
3
+ Oriel includes Laravel's string and array helpers, `Str`, `Stringable` and `Arr`, and its helper functions, such as
4
+ `config()`, `route()` and `retry()`.
5
+
6
+ ## Introduction
7
+
8
+ The framework uses these helpers itself, and you can use them in your own code. Each one is a named export of
9
+ `@orieljs/framework`, so you import it as you import anything else:
10
+
11
+ ```ts title="app/Http/Controllers/PostController.ts"
12
+ import { redirect, request, Str } from '@orieljs/framework'
13
+ import { Post } from '../../Models/Post'
14
+ import { Controller } from './Controller'
15
+
16
+ export class PostController extends Controller {
17
+ async store() {
18
+ const title = request().string('title').trim().toString()
19
+ const post = await Post.create({ title, slug: Str.slug(title), body: '' })
20
+
21
+ return redirect().route('posts.show', post)
22
+ }
23
+ }
24
+ ```
25
+
26
+ > [!NOTE]
27
+ > Laravel's helpers are global PHP functions, written in snake case (`abort_if()`, `csrf_token()`), and its string
28
+ > and array classes are called with `::`. Oriel's are exports you import, in camel case (`abortIf()`, `csrfToken()`),
29
+ > and `Str` and `Arr` are objects: `Str.slug()` for `Str::slug()`.
30
+
31
+ ## Available methods
32
+
33
+ **Arrays and objects:** [`Arr.dot()`](#arrdot), [`Arr.except()`](#arrexcept), [`Arr.first()`](#arrfirst),
34
+ [`Arr.forget()`](#arrforget), [`Arr.get()`](#arrget), [`Arr.has()`](#arrhas), [`Arr.last()`](#arrlast),
35
+ [`Arr.only()`](#arronly), [`Arr.set()`](#arrset), [`Arr.undot()`](#arrundot), [`Arr.wrap()`](#arrwrap),
36
+ [`classNames()`](#classnames), [`styleString()`](#stylestring)
37
+
38
+ **Paths:** [`app().basePath()` and the other paths](#paths)
39
+
40
+ **Strings:** [`__()`](#translation-strings), [`e()`](#e), [`Str.after()`](#strafter),
41
+ [`Str.afterLast()`](#strafterlast), [`Str.ascii()`](#strascii), [`Str.before()`](#strbefore),
42
+ [`Str.beforeLast()`](#strbeforelast), [`Str.between()`](#strbetween), [`Str.camel()`](#strcamel),
43
+ [`Str.contains()`](#strcontains), [`Str.endsWith()`](#strendswith), [`Str.finish()`](#strfinish), [`Str.is()`](#stris),
44
+ [`Str.isUlid()`](#strisulid), [`Str.isUuid()`](#strisuuid), [`Str.kebab()`](#strkebab), [`Str.lcfirst()`](#strlcfirst),
45
+ [`Str.limit()`](#strlimit), [`Str.of()`](#strof), [`Str.plural()`](#strplural), [`Str.random()`](#strrandom),
46
+ [`Str.singular()`](#strsingular), [`Str.slug()`](#strslug), [`Str.snake()`](#strsnake), [`Str.start()`](#strstart),
47
+ [`Str.startsWith()`](#strstartswith), [`Str.studly()`](#strstudly), [`Str.title()`](#strtitle),
48
+ [`Str.ucfirst()`](#strucfirst), [`Str.ulid()`](#strulid), [`Str.uuid()`](#struuid), [`Str.uuid7()`](#struuid7)
49
+
50
+ **Fluent strings:** the methods of a `Stringable`, listed in [Fluent strings](#fluent-strings)
51
+
52
+ **URLs:** [`route()`](#route), [`url()`](#url)
53
+
54
+ **Miscellaneous:** [`abort()`](#abort), [`abortIf()`](#abortif), [`abortUnless()`](#abortunless), [`app()`](#app),
55
+ [`auth()`](#auth), [`back()`](#back), [`bcrypt()`](#bcrypt), [`cache()`](#cache), [`collect()`](#collect),
56
+ [`config()`](#config), [`cookie()`](#cookie), [`csrfField()`](#csrffield), [`csrfToken()`](#csrftoken),
57
+ [`decrypt()`](#decrypt), [`dispatch()`](#dispatch), [`dispatchSync()`](#dispatchsync), [`encrypt()`](#encrypt),
58
+ [`env()`](#env), [`errors()`](#errors), [`event()`](#event), [`fake()`](#fake), [`inertia()`](#inertia),
59
+ [`inject()`](#inject), [`live()`](#live), [`logger()`](#logger), [`methodField()`](#methodfield), [`now()`](#now),
60
+ [`old()`](#old), [`raw()`](#raw), [`redirect()`](#redirect), [`request()`](#request), [`response()`](#response),
61
+ [`retry()`](#retry), [`session()`](#session), [`tap()`](#tap), [`value()`](#value), [`view()`](#view),
62
+ [`vite()`](#vite)
63
+
64
+ **Other utilities:** [Pipeline](#pipeline)
65
+
66
+ ## Arrays and objects
67
+
68
+ The `Arr` methods work on plain objects and arrays. The ones that take a key read it with dots, so `'products.desk'`
69
+ is the `desk` key of the `products` object, and `'users.0.name'` the `name` of the first user.
70
+
71
+ #### `Arr.dot()`
72
+
73
+ `Arr.dot()` flattens a nested object into one level, with dotted keys:
74
+
75
+ ```ts
76
+ const flattened = Arr.dot({ products: { desk: { price: 100 } } })
77
+
78
+ // { 'products.desk.price': 100 }
79
+ ```
80
+
81
+ Arrays are flattened by index (`'tags.0'`), and an empty object or array stays a value.
82
+
83
+ #### `Arr.except()`
84
+
85
+ `Arr.except()` returns a copy of the object without the keys you list:
86
+
87
+ ```ts
88
+ const filtered = Arr.except({ name: 'Desk', price: 100 }, ['price'])
89
+
90
+ // { name: 'Desk' }
91
+ ```
92
+
93
+ The keys are top-level keys, where Laravel's `Arr::except()` also reads a dotted key as a path.
94
+
95
+ #### `Arr.first()`
96
+
97
+ `Arr.first()` returns the first item of an array that passes a test, or the default when none does:
98
+
99
+ ```ts
100
+ const first = Arr.first([100, 200, 300], (value) => value >= 150)
101
+
102
+ // 200
103
+
104
+ const none = Arr.first([100, 200, 300], (value) => value >= 500, 'none')
105
+
106
+ // 'none'
107
+ ```
108
+
109
+ The test receives the item and its index. Without a test, it returns the first item, or the default for an empty
110
+ array.
111
+
112
+ #### `Arr.forget()`
113
+
114
+ `Arr.forget()` deletes a key, given with dots, from the object itself:
115
+
116
+ ```ts
117
+ const data = { products: { desk: { price: 100 } } }
118
+
119
+ Arr.forget(data, 'products.desk')
120
+
121
+ // { products: {} }
122
+ ```
123
+
124
+ #### `Arr.get()`
125
+
126
+ `Arr.get()` reads a value with dots, and returns the default when a key on the way is missing:
127
+
128
+ ```ts
129
+ const price = Arr.get({ products: { desk: { price: 100 } } }, 'products.desk.price')
130
+
131
+ // 100
132
+
133
+ const discount = Arr.get({ products: { desk: { price: 100 } } }, 'products.desk.discount', 0)
134
+
135
+ // 0
136
+ ```
137
+
138
+ A type argument types the value, as a cast, not a check: `Arr.get<number>(data, 'products.desk.price')`.
139
+
140
+ #### `Arr.has()`
141
+
142
+ `Arr.has()` tells you whether a key, given with dots, exists:
143
+
144
+ ```ts
145
+ const has = Arr.has({ product: { name: 'Desk', price: 100 } }, 'product.name')
146
+
147
+ // true
148
+ ```
149
+
150
+ A key that holds `null` or `undefined` exists.
151
+
152
+ #### `Arr.last()`
153
+
154
+ `Arr.last()` returns the last item of an array that passes a test, or the default when none does:
155
+
156
+ ```ts
157
+ const last = Arr.last([100, 200, 300, 110], (value) => value >= 150)
158
+
159
+ // 300
160
+ ```
161
+
162
+ #### `Arr.only()`
163
+
164
+ `Arr.only()` returns a copy of the object with only the keys you list:
165
+
166
+ ```ts
167
+ const slice = Arr.only({ name: 'Desk', price: 100, orders: 10 }, ['name', 'price'])
168
+
169
+ // { name: 'Desk', price: 100 }
170
+ ```
171
+
172
+ #### `Arr.set()`
173
+
174
+ `Arr.set()` sets a value with dots, in the object itself, and creates the objects on the way:
175
+
176
+ ```ts
177
+ const data = { products: { desk: { price: 100 } } }
178
+
179
+ Arr.set(data, 'products.desk.price', 200)
180
+
181
+ // { products: { desk: { price: 200 } } }
182
+ ```
183
+
184
+ It returns the object. A key with a `__proto__`, `prototype` or `constructor` part throws, so data from a request
185
+ can't change an object's prototype.
186
+
187
+ #### `Arr.undot()`
188
+
189
+ `Arr.undot()` turns dotted keys back into nested objects:
190
+
191
+ ```ts
192
+ const nested = Arr.undot({ 'user.name': 'Kevin Malone', 'user.occupation': 'Accountant' })
193
+
194
+ // { user: { name: 'Kevin Malone', occupation: 'Accountant' } }
195
+ ```
196
+
197
+ #### `Arr.wrap()`
198
+
199
+ `Arr.wrap()` wraps a value in an array. An array is returned as it is, and `null` or `undefined` gives an empty
200
+ array:
201
+
202
+ ```ts
203
+ Arr.wrap('Laravel') // ['Laravel']
204
+ Arr.wrap(['Laravel']) // ['Laravel']
205
+ Arr.wrap(null) // []
206
+ ```
207
+
208
+ #### `classNames()`
209
+
210
+ `classNames()` builds a `class` attribute from strings, lists and `{ name: condition }` objects, as Laravel's
211
+ `Arr::toCssClasses()` does. The `class` attribute of a TSX view takes the same values:
212
+
213
+ ```ts
214
+ const isActive = false
215
+ const hasError = true
216
+
217
+ const classes = classNames(['p-4', { 'font-bold': isActive, 'bg-red': hasError }])
218
+
219
+ // 'p-4 bg-red'
220
+ ```
221
+
222
+ #### `styleString()`
223
+
224
+ `styleString()` builds a `style` attribute, as Laravel's `Arr::toCssStyles()` does, but from property and value pairs,
225
+ as React's `style` takes them, where Laravel's takes `property: value` strings. Property names are written in kebab
226
+ case, and a number gets `px` unless the property takes a plain number:
227
+
228
+ ```ts
229
+ const style = styleString({ backgroundColor: 'red', marginTop: 4, opacity: 0.5, color: false })
230
+
231
+ // 'background-color:red;margin-top:4px;opacity:0.5'
232
+ ```
233
+
234
+ A property whose value is `false`, `null`, `undefined` or `''` is left out.
235
+
236
+ ## Paths
237
+
238
+ The application knows where its folders are. `app()` returns it, and each method takes a path to add:
239
+
240
+ ```ts
241
+ const base = app().basePath()
242
+ const path = app().appPath('Models/User.ts')
243
+ const bootstrap = app().bootstrapPath('app.ts')
244
+ const config = app().configPath('app.ts')
245
+ const migrations = app().databasePath('migrations')
246
+ const css = app().publicPath('css/app.css')
247
+ const views = app().resourcePath('views')
248
+ const logs = app().storagePath('logs/oriel.log')
249
+ ```
250
+
251
+ `basePath()` is the `basePath` your `bootstrap/app.ts` gives `Application.configure()`, your app's root folder. The
252
+ others are its `app`, `bootstrap`, `config`, `database`, `public`, `resources` and `storage` folders.
253
+
254
+ > [!NOTE]
255
+ > Laravel has a helper function for each path, such as `storage_path()`. Oriel has the application's methods only:
256
+ > `app().storagePath()`.
257
+
258
+ ## Strings
259
+
260
+ #### Translation strings
261
+
262
+ `__()` returns the line for a translation key, with its placeholders filled:
263
+
264
+ ```ts
265
+ const message = __('auth.throttle', { seconds: 30 })
266
+
267
+ // 'Too many login attempts. Please try again in 30 seconds.'
268
+ ```
269
+
270
+ A key with no line comes back as it is, so a sentence can be its own key, with its placeholders. `:name` takes the
271
+ value as given, `:Name` with its first letter upper-cased, and `:NAME` upper-cased:
272
+
273
+ ```ts
274
+ const greeting = __('Welcome, :Name', { name: 'dayle' })
275
+
276
+ // 'Welcome, Dayle'
277
+ ```
278
+
279
+ > [!NOTE]
280
+ > Laravel reads the lines from your app's language files. Oriel has no language files: `__()` knows the English lines
281
+ > of Laravel's `auth` and `passwords` files (`auth.failed`, `auth.password`, `auth.throttle`, `passwords.reset`,
282
+ > `passwords.sent`, `passwords.throttled`, `passwords.token` and `passwords.user`), which the starter kits use, and
283
+ > returns any other key as it is.
284
+
285
+ #### `e()`
286
+
287
+ `e()` escapes `&`, `<`, `>`, `"` and `'` for HTML:
288
+
289
+ ```ts
290
+ const html = e('<html>foo</html>')
291
+
292
+ // '&lt;html&gt;foo&lt;/html&gt;'
293
+ ```
294
+
295
+ `null` and `undefined` give `''`, and markup marked with [`raw()`](#raw) comes back unescaped. TSX views escape what
296
+ they print themselves, so you need `e()` only for HTML you build in a string.
297
+
298
+ #### `Str.after()`
299
+
300
+ `Str.after()` returns everything after the first occurrence of a value, or the whole string when the value isn't in
301
+ it:
302
+
303
+ ```ts
304
+ const slice = Str.after('This is my name', 'This is')
305
+
306
+ // ' my name'
307
+ ```
308
+
309
+ #### `Str.afterLast()`
310
+
311
+ `Str.afterLast()` returns everything after the last occurrence of a value, or the whole string when the value isn't
312
+ in it:
313
+
314
+ ```ts
315
+ const slice = Str.afterLast('app/Http/Controllers/Controller.ts', '/')
316
+
317
+ // 'Controller.ts'
318
+ ```
319
+
320
+ #### `Str.ascii()`
321
+
322
+ `Str.ascii()` transliterates a string into ASCII, as Laravel's does for English:
323
+
324
+ ```ts
325
+ const ascii = Str.ascii('Café crème')
326
+
327
+ // 'Cafe creme'
328
+ ```
329
+
330
+ What has no ASCII form, such as Chinese characters or an emoji, is dropped. [`Str.slug()`](#strslug) has the
331
+ details.
332
+
333
+ #### `Str.before()`
334
+
335
+ `Str.before()` returns everything before the first occurrence of a value:
336
+
337
+ ```ts
338
+ const slice = Str.before('This is my name', 'my name')
339
+
340
+ // 'This is '
341
+ ```
342
+
343
+ #### `Str.beforeLast()`
344
+
345
+ `Str.beforeLast()` returns everything before the last occurrence of a value:
346
+
347
+ ```ts
348
+ const slice = Str.beforeLast('This is my name', 'is')
349
+
350
+ // 'This '
351
+ ```
352
+
353
+ #### `Str.between()`
354
+
355
+ `Str.between()` returns the part between two values: after the first occurrence of the first, and before the last
356
+ occurrence of the second:
357
+
358
+ ```ts
359
+ const slice = Str.between('This is my name', 'This', 'name')
360
+
361
+ // ' is my '
362
+ ```
363
+
364
+ #### `Str.camel()`
365
+
366
+ `Str.camel()` converts a string to camel case:
367
+
368
+ ```ts
369
+ const converted = Str.camel('foo_bar')
370
+
371
+ // 'fooBar'
372
+ ```
373
+
374
+ #### `Str.contains()`
375
+
376
+ `Str.contains()` tells you whether a string contains a value, or any of a list of values. It is case-sensitive:
377
+
378
+ ```ts
379
+ const contains = Str.contains('This is my name', 'my')
380
+
381
+ // true
382
+
383
+ const containsAny = Str.contains('This is my name', ['my', 'foo'])
384
+
385
+ // true
386
+ ```
387
+
388
+ #### `Str.endsWith()`
389
+
390
+ `Str.endsWith()` tells you whether a string ends with a value, or with any of a list of values:
391
+
392
+ ```ts
393
+ const result = Str.endsWith('This is my name', 'name')
394
+
395
+ // true
396
+ ```
397
+
398
+ #### `Str.finish()`
399
+
400
+ `Str.finish()` adds a value to the end of a string, unless it already ends with it:
401
+
402
+ ```ts
403
+ const adjusted = Str.finish('this/string', '/')
404
+
405
+ // 'this/string/'
406
+ ```
407
+
408
+ #### `Str.is()`
409
+
410
+ `Str.is()` tells you whether a string matches a pattern, in which `*` matches anything and every other character is
411
+ literal:
412
+
413
+ ```ts
414
+ const matches = Str.is('foo*', 'foobar')
415
+
416
+ // true
417
+ ```
418
+
419
+ #### `Str.isUlid()`
420
+
421
+ `Str.isUlid()` tells you whether a value is a ULID, in either case:
422
+
423
+ ```ts
424
+ const isUlid = Str.isUlid('01gd6r360bp37zj17nxb55yv40')
425
+
426
+ // true
427
+ ```
428
+
429
+ #### `Str.isUuid()`
430
+
431
+ `Str.isUuid()` tells you whether a value is a UUID, of any version:
432
+
433
+ ```ts
434
+ const isUuid = Str.isUuid('a0a2a2d2-0b87-4a18-83f2-2529882be2de')
435
+
436
+ // true
437
+ ```
438
+
439
+ Both checks narrow the value's type to `string`.
440
+
441
+ #### `Str.kebab()`
442
+
443
+ `Str.kebab()` converts a string to kebab case:
444
+
445
+ ```ts
446
+ const converted = Str.kebab('fooBar')
447
+
448
+ // 'foo-bar'
449
+ ```
450
+
451
+ #### `Str.lcfirst()`
452
+
453
+ `Str.lcfirst()` lower-cases the first character of a string:
454
+
455
+ ```ts
456
+ const string = Str.lcfirst('Foo Bar')
457
+
458
+ // 'foo Bar'
459
+ ```
460
+
461
+ #### `Str.limit()`
462
+
463
+ `Str.limit()` cuts a string to a number of characters and adds `...` when it had more:
464
+
465
+ ```ts
466
+ const truncated = Str.limit('The quick brown fox jumps over the lazy dog', 20)
467
+
468
+ // 'The quick brown fox...'
469
+ ```
470
+
471
+ The third argument replaces the `...`:
472
+
473
+ ```ts
474
+ const truncated = Str.limit('The quick brown fox jumps over the lazy dog', 20, ' (...)')
475
+
476
+ // 'The quick brown fox (...)'
477
+ ```
478
+
479
+ #### `Str.of()`
480
+
481
+ `Str.of()` wraps a string in a `Stringable`, so you can chain the string methods. `null` and `undefined` give an
482
+ empty string, and any other value is turned into a string:
483
+
484
+ ```ts
485
+ const slug = Str.of(' Laravel ').trim().append(' Framework').slug().toString()
486
+
487
+ // 'laravel-framework'
488
+ ```
489
+
490
+ [Fluent strings](#fluent-strings) lists the methods.
491
+
492
+ #### `Str.plural()`
493
+
494
+ `Str.plural()` returns the plural form of an English word:
495
+
496
+ ```ts
497
+ const plural = Str.plural('car')
498
+
499
+ // 'cars'
500
+
501
+ const children = Str.plural('child')
502
+
503
+ // 'children'
504
+ ```
505
+
506
+ A count as the second argument gives the singular for 1 and the plural for any other number:
507
+
508
+ ```ts
509
+ const singular = Str.plural('child', 1)
510
+
511
+ // 'child'
512
+ ```
513
+
514
+ #### `Str.random()`
515
+
516
+ `Str.random()` returns a random string of letters and digits, 16 characters long unless you give a length. It uses
517
+ Node's cryptographically secure random bytes:
518
+
519
+ ```ts
520
+ const random = Str.random(40)
521
+ ```
522
+
523
+ #### `Str.singular()`
524
+
525
+ `Str.singular()` returns the singular form of an English word:
526
+
527
+ ```ts
528
+ const singular = Str.singular('cars')
529
+
530
+ // 'car'
531
+
532
+ const child = Str.singular('children')
533
+
534
+ // 'child'
535
+ ```
536
+
537
+ #### `Str.slug()`
538
+
539
+ `Str.slug()` makes a URL-friendly slug of a string: lower-case letters and digits joined by the separator, `-` unless
540
+ you give another:
541
+
542
+ ```ts
543
+ const slug = Str.slug('Laravel 5 Framework', '-')
544
+
545
+ // 'laravel-5-framework'
546
+ ```
547
+
548
+ The string is transliterated to ASCII first, as Laravel's is for English, so accented and non-Latin letters keep a
549
+ readable form:
550
+
551
+ ```ts
552
+ Str.slug('Café crème brûlée') // 'cafe-creme-brulee'
553
+ Str.slug('Привет, мир!') // 'privet-mir'
554
+ Str.slug('über@home') // 'uber-at-home'
555
+ ```
556
+
557
+ `@` is read as `at`, and underscores and dashes become the separator. What has no ASCII form is dropped, so
558
+ `Str.slug('日本語')` is `''`: give such a title a slug of your own.
559
+
560
+ The third argument is the language, `'en'`; `null` skips the transliteration, and keeps every letter and digit. The
561
+ fourth is the dictionary of words to put in, `{ '@': 'at' }` unless you give another:
562
+
563
+ ```ts
564
+ Str.slug('Café crème', '-', null) // 'café-crème'
565
+ Str.slug('5 & 6', '-', 'en', { '&': 'and' }) // '5-and-6'
566
+ ```
567
+
568
+ #### `Str.snake()`
569
+
570
+ `Str.snake()` converts a string to snake case, with `_` or the delimiter you give:
571
+
572
+ ```ts
573
+ const converted = Str.snake('fooBar')
574
+
575
+ // 'foo_bar'
576
+
577
+ const dashed = Str.snake('fooBar', '-')
578
+
579
+ // 'foo-bar'
580
+ ```
581
+
582
+ #### `Str.start()`
583
+
584
+ `Str.start()` adds a value to the start of a string, unless it already starts with it:
585
+
586
+ ```ts
587
+ const adjusted = Str.start('this/string', '/')
588
+
589
+ // '/this/string'
590
+ ```
591
+
592
+ #### `Str.startsWith()`
593
+
594
+ `Str.startsWith()` tells you whether a string starts with a value, or with any of a list of values:
595
+
596
+ ```ts
597
+ const result = Str.startsWith('This is my name', 'This')
598
+
599
+ // true
600
+ ```
601
+
602
+ #### `Str.studly()`
603
+
604
+ `Str.studly()` converts a string to studly case:
605
+
606
+ ```ts
607
+ const converted = Str.studly('foo_bar')
608
+
609
+ // 'FooBar'
610
+ ```
611
+
612
+ #### `Str.title()`
613
+
614
+ `Str.title()` lower-cases a string, then upper-cases the first letter of each word:
615
+
616
+ ```ts
617
+ const converted = Str.title('a nice title uses the correct case')
618
+
619
+ // 'A Nice Title Uses The Correct Case'
620
+ ```
621
+
622
+ A word starts after a space, so `'hello-world'` gives `'Hello-world'`.
623
+
624
+ #### `Str.ucfirst()`
625
+
626
+ `Str.ucfirst()` upper-cases the first character of a string:
627
+
628
+ ```ts
629
+ const string = Str.ucfirst('foo bar')
630
+
631
+ // 'Foo bar'
632
+ ```
633
+
634
+ #### `Str.ulid()`
635
+
636
+ `Str.ulid()` returns a new ULID: 26 upper-case characters, the time first, so a later ULID sorts after an earlier
637
+ one. It takes a `Date` to use instead of now:
638
+
639
+ ```ts
640
+ const ulid = Str.ulid()
641
+
642
+ // '01M59V508YR4SZQ9PPYB9ZBX8P'
643
+ ```
644
+
645
+ #### `Str.uuid()`
646
+
647
+ `Str.uuid()` returns a random version 4 UUID:
648
+
649
+ ```ts
650
+ const uuid = Str.uuid()
651
+ ```
652
+
653
+ #### `Str.uuid7()`
654
+
655
+ `Str.uuid7()` returns a version 7 UUID, which starts with the time, so a later one sorts after an earlier one. It
656
+ takes a `Date` to use instead of now:
657
+
658
+ ```ts
659
+ const uuid = Str.uuid7()
660
+ ```
661
+
662
+ `Str.ulid()` and `Str.uuid7()` throw a `TypeError` for an invalid date, or one before 1970 or after the year 10889.
663
+
664
+ ## Fluent strings
665
+
666
+ A `Stringable` is a string with the string methods as chainable methods. `Str.of()` makes one, and
667
+ `request().string('name')` returns one:
668
+
669
+ ```ts
670
+ const slug = Str.of(' Hello World ').trim().slug().toString()
671
+
672
+ // 'hello-world'
673
+ ```
674
+
675
+ Each method returns a new `Stringable`, except those in the second table, which return an answer. A `Stringable`
676
+ prints as its string in a template literal and in JSON; `toString()` or `value()` gives you the string itself.
677
+
678
+ | Method | Returns |
679
+ | --- | --- |
680
+ | `after(search)`, `afterLast(search)` | As [`Str.after()`](#strafter) and [`Str.afterLast()`](#strafterlast) |
681
+ | `append(...values)` | The values added to the end |
682
+ | `ascii()` | As [`Str.ascii()`](#strascii) |
683
+ | `before(search)`, `beforeLast(search)` | As [`Str.before()`](#strbefore) and [`Str.beforeLast()`](#strbeforelast) |
684
+ | `between(from, to)` | As [`Str.between()`](#strbetween) |
685
+ | `camel()`, `kebab()`, `snake(delimiter)`, `studly()` | As [`Str.camel()`](#strcamel), [`Str.kebab()`](#strkebab), [`Str.snake()`](#strsnake) and [`Str.studly()`](#strstudly) |
686
+ | `finish(cap)`, `start(prefix)` | As [`Str.finish()`](#strfinish) and [`Str.start()`](#strstart) |
687
+ | `lcfirst()`, `ucfirst()` | As [`Str.lcfirst()`](#strlcfirst) and [`Str.ucfirst()`](#strucfirst) |
688
+ | `limit(limit, end)` | As [`Str.limit()`](#strlimit) |
689
+ | `lower()`, `upper()` | The string lower-cased or upper-cased |
690
+ | `ltrim(characters)`, `rtrim(characters)`, `trim(characters)` | The string without whitespace at the start, the end or both ends, or without the characters you give |
691
+ | `plural(count)`, `singular()` | As [`Str.plural()`](#strplural) and [`Str.singular()`](#strsingular) |
692
+ | `prepend(...values)` | The values added to the start |
693
+ | `replace(search, replace)` | Every occurrence of the value, or of each value of a list, replaced |
694
+ | `slug(separator)` | As [`Str.slug()`](#strslug) |
695
+ | `title()` | As [`Str.title()`](#strtitle) |
696
+
697
+ | Method | Returns |
698
+ | --- | --- |
699
+ | `contains(needles)`, `endsWith(needles)`, `startsWith(needles)` | As [`Str.contains()`](#strcontains), [`Str.endsWith()`](#strendswith) and [`Str.startsWith()`](#strstartswith) |
700
+ | `exactly(value)` | Whether the string is exactly the value, a string or a `Stringable` |
701
+ | `explode(delimiter)` | The parts between the delimiters, as a [collection](collections.md) |
702
+ | `is(pattern)` | As [`Str.is()`](#stris) |
703
+ | `isEmpty()`, `isNotEmpty()` | Whether the string is `''`, or isn't |
704
+ | `length()` | The number of characters, as Unicode code points |
705
+ | `toBoolean()` | `true` for `'1'`, `'true'`, `'on'` and `'yes'`, in any case, else `false` |
706
+ | `toInteger()`, `toFloat()` | The number the string starts with, or `0` |
707
+ | `toString()`, `value()`, `toJSON()` | The string |
708
+
709
+ `trim()` with no characters removes the invisible characters Laravel's `Str::trim()` removes too, such as a
710
+ zero-width space, along with the whitespace.
711
+
712
+ ## URLs
713
+
714
+ #### `route()`
715
+
716
+ `route()` returns the URL of a named route, with its parameters:
717
+
718
+ ```ts
719
+ declare const post: Post
720
+ // ---cut---
721
+ const url = route('posts.show', post)
722
+
723
+ const urlWithQuery = route('posts.show', { post, page: 2 })
724
+ ```
725
+
726
+ The URL is absolute unless you pass `false` as the third argument. [Routing](routing.md#generating-urls-to-named-routes)
727
+ covers the parameters and the types `route:types` adds.
728
+
729
+ #### `url()`
730
+
731
+ `url()` returns an absolute URL for a path, with a query string from an object:
732
+
733
+ ```ts
734
+ const profile = url('user/profile')
735
+
736
+ // 'http://localhost:8000/user/profile'
737
+
738
+ const withQuery = url('posts', { page: 2 })
739
+
740
+ // 'http://localhost:8000/posts?page=2'
741
+ ```
742
+
743
+ Pass `false` as the third argument for the path alone. With no arguments, `url()` returns the URL generator:
744
+
745
+ ```ts
746
+ const current = url().current()
747
+ const previous = url().previous()
748
+ const asset = url().asset('img/photo.jpg')
749
+ ```
750
+
751
+ `current()` is the current URL without its query string, and `previous()` the page the user came from. `asset()` is the
752
+ URL of a file in `public`, on `app.asset_url` when your config sets one.
753
+
754
+ ## Miscellaneous
755
+
756
+ #### `abort()`
757
+
758
+ `abort()` throws an HTTP error, which Oriel renders as the response with that status:
759
+
760
+ ```ts
761
+ abort(403)
762
+ abort(403, 'Unauthorized.', { 'X-Reason': 'banned' })
763
+ ```
764
+
765
+ Pass a `Response` to send it as it is. [Error handling](errors.md) has the details.
766
+
767
+ #### `abortIf()`
768
+
769
+ `abortIf()` throws the HTTP error when the condition is truthy:
770
+
771
+ ```ts
772
+ declare const user: User
773
+ // ---cut---
774
+ abortIf(user.email_verified_at === null, 403)
775
+ ```
776
+
777
+ #### `abortUnless()`
778
+
779
+ `abortUnless()` throws the HTTP error when the condition is falsy:
780
+
781
+ ```ts
782
+ declare const user: User
783
+ // ---cut---
784
+ abortUnless(user.email_verified_at !== null, 403)
785
+ ```
786
+
787
+ #### `app()`
788
+
789
+ `app()` returns the application, the service container. With a token or a class, it resolves that from the
790
+ container:
791
+
792
+ ```ts
793
+ import { app } from '@orieljs/framework'
794
+ import { Hasher } from '@orieljs/framework/contracts'
795
+
796
+ const container = app()
797
+
798
+ const hasher = app(Hasher)
799
+ ```
800
+
801
+ A contract's token comes from `@orieljs/framework/contracts`. Inside a request, `app().make()` resolves from the
802
+ request's scope.
803
+
804
+ #### `auth()`
805
+
806
+ `auth()` returns the auth manager, and `auth('web')` a guard:
807
+
808
+ ```ts
809
+ const signedIn = await auth().check()
810
+
811
+ const user = await auth().user()
812
+ ```
813
+
814
+ #### `back()`
815
+
816
+ `back()` returns a redirect to the previous page, or to the fallback when there is none:
817
+
818
+ ```ts
819
+ const response = back()
820
+
821
+ const withFallback = back('/dashboard')
822
+ ```
823
+
824
+ #### `bcrypt()`
825
+
826
+ `bcrypt()` hashes a value with bcrypt, whatever your default hashing driver is. It returns a promise:
827
+
828
+ ```ts
829
+ const password = await bcrypt('my-secret-password')
830
+ ```
831
+
832
+ `{ rounds: 12 }` as the second argument sets the cost.
833
+
834
+ #### `cache()`
835
+
836
+ `cache()` with a key reads the value from the default cache store. With an object, it stores each key, for the
837
+ number of seconds you give, or forever:
838
+
839
+ ```ts
840
+ const value = await cache('key')
841
+
842
+ await cache({ key: 'value' }, 300)
843
+ ```
844
+
845
+ With no arguments, it returns the cache manager: `await cache().remember('users', 60, () => User.all())`. Every
846
+ call returns a promise.
847
+
848
+ #### `collect()`
849
+
850
+ `collect()` makes a collection of an array, or of any iterable, as [Collections](collections.md) describes:
851
+
852
+ ```ts
853
+ const collection = collect(['Taylor', 'Abigail'])
854
+ ```
855
+
856
+ #### `config()`
857
+
858
+ `config()` reads a configuration value, with dots, and returns the default when it isn't set:
859
+
860
+ ```ts
861
+ const name = config('app.name')
862
+
863
+ const timezone = config('app.timezone', 'UTC')
864
+ ```
865
+
866
+ An object sets values in the running application's configuration, and no arguments return the configuration
867
+ repository:
868
+
869
+ ```ts
870
+ config({ 'app.debug': true })
871
+ ```
872
+
873
+ A type argument types the value, as a cast: `config<string>('app.name')`.
874
+
875
+ #### `cookie()`
876
+
877
+ `cookie()` makes a cookie, for `minutes` minutes, which you queue or attach to a response:
878
+
879
+ ```ts
880
+ const theme = cookie('theme', 'dark', 60)
881
+ ```
882
+
883
+ With no arguments, it returns the cookie jar: `cookie().queue(cookie('theme', 'dark', 60))` sends the cookie with
884
+ the response.
885
+
886
+ #### `csrfField()`
887
+
888
+ `csrfField()` returns the hidden `_token` input of a form, with the session's CSRF token:
889
+
890
+ ```tsx
891
+ const form = (
892
+ <form method="post" action="/profile">
893
+ {csrfField()}
894
+ </form>
895
+ )
896
+ ```
897
+
898
+ #### `csrfToken()`
899
+
900
+ `csrfToken()` returns the session's CSRF token:
901
+
902
+ ```ts
903
+ const token = csrfToken()
904
+ ```
905
+
906
+ #### `decrypt()`
907
+
908
+ `decrypt()` decrypts a value made by [`encrypt()`](#encrypt), and throws a `DecryptError` when it can't:
909
+
910
+ ```ts
911
+ declare const encrypted: string
912
+ // ---cut---
913
+ const value = decrypt<{ id: number }>(encrypted)
914
+ ```
915
+
916
+ #### `dispatch()`
917
+
918
+ `dispatch()` sends a job to its queue, or runs it now when it isn't a queued job:
919
+
920
+ ```ts
921
+ declare const podcast: Podcast
922
+ // ---cut---
923
+ await dispatch(new ProcessPodcast(podcast))
924
+ ```
925
+
926
+ It returns the pending dispatch, so you can chain `onQueue()`, `delay()` and the other options. [Queues](queues.md)
927
+ covers jobs.
928
+
929
+ #### `dispatchSync()`
930
+
931
+ `dispatchSync()` runs a job now, in the current process, and returns a promise that settles when it is done:
932
+
933
+ ```ts
934
+ declare const podcast: Podcast
935
+ // ---cut---
936
+ await dispatchSync(new ProcessPodcast(podcast))
937
+ ```
938
+
939
+ #### `encrypt()`
940
+
941
+ `encrypt()` encrypts a value with your `APP_KEY`. The value is turned into JSON first, unless you pass `false` as the
942
+ second argument:
943
+
944
+ ```ts
945
+ const secret = encrypt({ id: 1 })
946
+ ```
947
+
948
+ #### `env()`
949
+
950
+ `env()` reads an environment variable, or returns the default when it isn't set. The default may be a function:
951
+
952
+ ```ts
953
+ const environment = env('APP_ENV', 'production')
954
+ ```
955
+
956
+ `true`, `false`, `null` and `empty`, in any case and with or without parentheses, become `true`, `false`, `null` and
957
+ `''`. A value in quotes loses them. Every other value stays a string: `PORT=8000` gives `'8000'`, not `8000`. Read
958
+ `env()` in your config files, and `config()` everywhere else.
959
+
960
+ #### `errors()`
961
+
962
+ `errors()` returns the validation errors the previous request flashed, as a message bag. It is empty when there are
963
+ none, or outside a request:
964
+
965
+ ```ts
966
+ const message = errors().first('email')
967
+ ```
968
+
969
+ `errors('login')` reads a named bag. [Validation](validation.md#displaying-the-validation-errors) shows it in a view.
970
+
971
+ #### `event()`
972
+
973
+ `event()` dispatches an event to its listeners, and returns a promise of their results:
974
+
975
+ ```ts
976
+ declare const order: object
977
+ // ---cut---
978
+ await event(order)
979
+ ```
980
+
981
+ #### `fake()`
982
+
983
+ `fake()` returns the fake data generator that factories use, shared by your whole process:
984
+
985
+ ```ts
986
+ const name = fake().name()
987
+
988
+ const email = fake().unique().safeEmail()
989
+ ```
990
+
991
+ #### `inertia()`
992
+
993
+ `inertia()` returns an Inertia response for a page, with its props:
994
+
995
+ ```ts
996
+ declare const posts: Post[]
997
+ // ---cut---
998
+ const response = inertia('Posts/Index', { posts })
999
+ ```
1000
+
1001
+ With no arguments, it returns the Inertia response factory.
1002
+
1003
+ #### `inject()`
1004
+
1005
+ `inject()` resolves a token or a class from the current container, the request's scope inside a request. It is how
1006
+ a class takes a service in a field:
1007
+
1008
+ ```ts
1009
+ import { inject } from '@orieljs/framework'
1010
+ import { Hasher } from '@orieljs/framework/contracts'
1011
+
1012
+ class PasswordService {
1013
+ private readonly hasher = inject(Hasher)
1014
+ }
1015
+ ```
1016
+
1017
+ #### `live()`
1018
+
1019
+ `live()` returns a Live component's first render, as HTML for a view, with its parameters:
1020
+
1021
+ ```ts
1022
+ declare const Counter: new () => Component
1023
+ // ---cut---
1024
+ const html = await live(Counter, { count: 3 })
1025
+ ```
1026
+
1027
+ In a TSX view, the `<Live component={Counter} />` element does the same.
1028
+
1029
+ #### `logger()`
1030
+
1031
+ `logger()` writes a `debug` message to the log, with its context:
1032
+
1033
+ ```ts
1034
+ declare const user: User
1035
+ // ---cut---
1036
+ logger('User dashboard visited.', { id: user.id })
1037
+ ```
1038
+
1039
+ With no arguments, it returns the log manager: `logger().error('Something went wrong.')`.
1040
+
1041
+ #### `methodField()`
1042
+
1043
+ `methodField()` returns the hidden `_method` input that lets a form reach a `PUT`, `PATCH` or `DELETE` route:
1044
+
1045
+ ```tsx
1046
+ const form = (
1047
+ <form method="post" action="/posts/1">
1048
+ {methodField('DELETE')}
1049
+ </form>
1050
+ )
1051
+ ```
1052
+
1053
+ #### `now()`
1054
+
1055
+ `now()` returns the current time as a `Date`:
1056
+
1057
+ ```ts
1058
+ const time = now()
1059
+ ```
1060
+
1061
+ > [!NOTE]
1062
+ > Laravel's `now()` and `today()` return a Carbon instance, with its date methods. Oriel's `now()` returns a plain
1063
+ > `Date`, and there is no `today()`.
1064
+
1065
+ #### `old()`
1066
+
1067
+ `old()` returns the input the previous request flashed to the session, or the default:
1068
+
1069
+ ```ts
1070
+ const title = old('title', '')
1071
+ ```
1072
+
1073
+ Outside a request, it returns the default. [Requests](requests.md#old-input) covers old input.
1074
+
1075
+ #### `raw()`
1076
+
1077
+ `raw()` marks HTML as safe, so a TSX view prints it without escaping it:
1078
+
1079
+ ```tsx
1080
+ declare const html: string
1081
+ // ---cut---
1082
+ const body = <article>{raw(html)}</article>
1083
+ ```
1084
+
1085
+ Never pass it text a user wrote.
1086
+
1087
+ #### `redirect()`
1088
+
1089
+ `redirect()` returns a redirect to a path, with a status, 302 unless you give one. With no arguments, it returns the
1090
+ redirector:
1091
+
1092
+ ```ts
1093
+ const response = redirect('/home')
1094
+
1095
+ const toRoute = redirect().route('dashboard')
1096
+ ```
1097
+
1098
+ [Responses](responses.md) covers redirects.
1099
+
1100
+ #### `request()`
1101
+
1102
+ `request()` returns the current request. With a key, it returns that input, or the default:
1103
+
1104
+ ```ts
1105
+ const current = request()
1106
+
1107
+ const name = request('name', 'Guest')
1108
+ ```
1109
+
1110
+ [Requests](requests.md) covers the request.
1111
+
1112
+ #### `response()`
1113
+
1114
+ `response()` returns a `Response` with the content, a status and headers. With no arguments, it returns the response
1115
+ factory:
1116
+
1117
+ ```ts
1118
+ const text = response('Hello World', 200, { 'Content-Type': 'text/plain' })
1119
+
1120
+ const json = response().json({ name: 'Abigail' })
1121
+ ```
1122
+
1123
+ #### `retry()`
1124
+
1125
+ `retry()` calls a function until it succeeds, up to a number of times, and returns what it returns. When the last
1126
+ attempt throws, it throws that error:
1127
+
1128
+ ```ts
1129
+ declare const callService: (attempt: number) => Promise<string>
1130
+ // ---cut---
1131
+ const result = await retry(5, (attempt) => callService(attempt), 100)
1132
+ ```
1133
+
1134
+ The function receives the attempt's number, from 1. The third argument is the number of milliseconds to wait
1135
+ between attempts, or a function of the attempt's number that returns them:
1136
+
1137
+ ```ts
1138
+ declare const callService: () => Promise<string>
1139
+ // ---cut---
1140
+ const result = await retry(3, () => callService(), (attempt) => attempt * 100)
1141
+ ```
1142
+
1143
+ The fourth argument decides which errors to retry: when it returns `false` for an error, `retry()` throws it at once:
1144
+
1145
+ ```ts
1146
+ declare const callService: () => Promise<string>
1147
+ // ---cut---
1148
+ const result = await retry(5, () => callService(), 100, (error) => error instanceof TypeError)
1149
+ ```
1150
+
1151
+ #### `session()`
1152
+
1153
+ `session()` reads a value from the session, or returns the default. An object stores its values, and no arguments
1154
+ return the session:
1155
+
1156
+ ```ts
1157
+ const value = session('key', 'default')
1158
+
1159
+ session({ chairs: 7, instruments: 3 })
1160
+
1161
+ const all = session().all()
1162
+ ```
1163
+
1164
+ [HTTP session](session.md) covers the session.
1165
+
1166
+ #### `tap()`
1167
+
1168
+ `tap()` calls a function with a value, then returns the value:
1169
+
1170
+ ```ts
1171
+ declare const user: User
1172
+ // ---cut---
1173
+ const saved = tap(user, (user) => {
1174
+ user.name = 'Taylor'
1175
+ })
1176
+ ```
1177
+
1178
+ If the function returns a promise, `tap()` doesn't wait for it.
1179
+
1180
+ #### `value()`
1181
+
1182
+ `value()` returns the value it is given, or calls it when it is a function and returns the result:
1183
+
1184
+ ```ts
1185
+ const five = value(5)
1186
+
1187
+ const six = value(() => 6)
1188
+ ```
1189
+
1190
+ #### `view()`
1191
+
1192
+ `view()` returns a view, with its data:
1193
+
1194
+ ```ts
1195
+ declare const user: User
1196
+ // ---cut---
1197
+ const profile = view('user.profile', { user })
1198
+ ```
1199
+
1200
+ With no arguments, it returns the view factory. [Views](views.md) covers views.
1201
+
1202
+ #### `vite()`
1203
+
1204
+ `vite()` returns the `<script>` and `<link>` tags of your Vite entry points, for a root view:
1205
+
1206
+ ```tsx
1207
+ const head = <head>{vite(['resources/css/app.css', 'resources/js/app.tsx'])}</head>
1208
+ ```
1209
+
1210
+ With no arguments, it returns the `Vite` instance.
1211
+
1212
+ ## Other utilities
1213
+
1214
+ ### Pipeline
1215
+
1216
+ A pipeline passes a value through a list of steps, each of which can inspect or change it and then call the next.
1217
+ It works as [middleware](middleware.md) does. Make one from the pipeline factory:
1218
+
1219
+ ```ts
1220
+ declare const user: User
1221
+ // ---cut---
1222
+ const result = await app(PipelineFactory)
1223
+ .make<User>()
1224
+ .send(user)
1225
+ .through([
1226
+ async (user, next) => {
1227
+ // ...
1228
+
1229
+ return next(user)
1230
+ },
1231
+ async (user, next) => {
1232
+ // ...
1233
+
1234
+ return next(user)
1235
+ },
1236
+ ])
1237
+ .then((user) => user)
1238
+ ```
1239
+
1240
+ Each step receives the value and `next`, which runs the steps after it and returns a promise of their result. A step
1241
+ that doesn't call `next` stops the pipeline: what it returns is the result. Once the last step calls `next`, the
1242
+ function you give `then()` runs. `thenReturn()` returns the value as the last step passed it, and `pipe()` adds steps
1243
+ after those of `through()`.
1244
+
1245
+ A step may also be a class with a `handle()` method. The pipeline makes it from the container, so its fields can use
1246
+ `inject()`:
1247
+
1248
+ ```ts title="app/Pipes/GenerateProfilePhoto.ts"
1249
+ import type { PipeNext } from '@orieljs/framework/contracts'
1250
+ import type { User } from '../Models/User'
1251
+
1252
+ export class GenerateProfilePhoto {
1253
+ async handle(user: User, next: PipeNext<User>) {
1254
+ // ...
1255
+
1256
+ return next(user)
1257
+ }
1258
+ }
1259
+ ```
1260
+
1261
+ ```ts
1262
+ declare const user: User
1263
+ declare class GenerateProfilePhoto {}
1264
+ declare class ActivateSubscription {}
1265
+ declare class SendWelcomeEmail {}
1266
+ // ---cut---
1267
+ const result = await app(PipelineFactory)
1268
+ .make<User>()
1269
+ .send(user)
1270
+ .through([GenerateProfilePhoto, ActivateSubscription, SendWelcomeEmail])
1271
+ .thenReturn()
1272
+ ```
1273
+
1274
+ `via('process')` calls another method on the classes. A class without the method fails the pipeline with a
1275
+ `PipelineError`: "Pipe [GenerateProfilePhoto] has no [handle] method.". `new BasePipeline<User>()` makes a pipeline
1276
+ without the factory.
1277
+
1278
+ > [!NOTE]
1279
+ > Laravel's pipeline runs its steps synchronously and has `withinTransaction()`. Oriel's steps may be async, so
1280
+ > `then()` and `thenReturn()` return a promise; there is no `withinTransaction()`: call `DB.transaction()` around the
1281
+ > pipeline instead.