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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. package/dist/index.d-BJyJx6U1.d.ts +0 -319
@@ -0,0 +1,247 @@
1
+ # Database: pagination
2
+
3
+ Split a long list into pages with one call on a query or a model, and show the page links in a view, an Inertia page
4
+ or a Live component.
5
+
6
+ ## Introduction
7
+
8
+ `paginate()` runs a query for one page of rows, reads the page number from the request's `page` parameter, and gives
9
+ back a paginator: the rows of that page, with the page numbers and the URLs of the other pages.
10
+
11
+ ## Basic usage
12
+
13
+ ### Paginating query builder results
14
+
15
+ `paginate()` takes the number of rows per page:
16
+
17
+ ```ts title="app/Http/Controllers/UserController.ts"
18
+ import { DB, view } from '@orieljs/framework'
19
+ import { Controller } from './Controller'
20
+
21
+ export class UserController extends Controller {
22
+ /** Show all application users. */
23
+ async index() {
24
+ const users = await DB.table('users').paginate(15)
25
+
26
+ return view('user.index', { users })
27
+ }
28
+ }
29
+ ```
30
+
31
+ It runs two queries: one counts the rows, so the paginator knows the last page, and one reads the page.
32
+
33
+ A page number that isn't a whole number of 1 or more is page 1. A page past the last one is empty.
34
+
35
+ #### Simple pagination
36
+
37
+ `simplePaginate()` reads one row more than a page, to tell whether there is a next page, and doesn't count the rows.
38
+ Use it for long lists where you show only "Previous" and "Next":
39
+
40
+ ```ts
41
+ const users = await DB.table('users').where('votes', '>', 100).simplePaginate(15)
42
+ ```
43
+
44
+ ### Paginating model results
45
+
46
+ Your models paginate in the same way, as a static method or at the end of a query. Without a number, a model's
47
+ `perPage`, 15, is the page size:
48
+
49
+ ```ts
50
+ declare const user: User
51
+ // ---cut---
52
+ const users = await User.paginate(15)
53
+
54
+ const verified = await User.whereNotNull('email_verified_at').paginate(15)
55
+
56
+ const posts = await user.posts().paginate(10)
57
+ ```
58
+
59
+ #### Multiple paginator instances per page
60
+
61
+ Two paginators on one page need their own parameter. The third argument names it, after the columns:
62
+
63
+ ```ts
64
+ const users = await User.whereNotNull('email_verified_at').paginate(15, ['*'], 'users')
65
+ ```
66
+
67
+ The fourth argument sets the page number yourself, in place of the request's: `paginate(15, ['*'], 'page', 2)`.
68
+
69
+ ### Cursor pagination
70
+
71
+ `cursorPaginate()` pages with a cursor, the values of the ordering columns of the last row, rather than with an offset.
72
+ The query needs an `orderBy()`; a model's query orders by its key when it has none:
73
+
74
+ ```ts
75
+ const users = await DB.table('users').orderBy('id').cursorPaginate(15)
76
+ ```
77
+
78
+ The next page's URL carries the cursor in the `cursor` parameter, such as
79
+ `http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0`.
80
+
81
+ #### Cursor vs. offset pagination
82
+
83
+ An offset query, `limit 15 offset 15000`, makes the database read and skip the rows before the page, which gets slow on
84
+ large tables, and a row added while someone pages can show twice or be skipped. A cursor query,
85
+ `where id > 15000 limit 15`, uses an index and is steady while rows change. In exchange, a cursor paginator has no page
86
+ numbers and no total, only next and previous, and its ordering columns must be unique together. `orderByRaw()` and
87
+ `inRandomOrder()` can't be used with it.
88
+
89
+ ### Manually creating a paginator
90
+
91
+ To paginate rows you already have, make a `LengthAwarePaginator` (`paginate()`'s), a `Paginator`
92
+ (`simplePaginate()`'s) or a `CursorPaginator` yourself, with the page's items, the total for a length-aware one, the
93
+ page size and the page:
94
+
95
+ ```ts
96
+ declare const pageOfItems: { id: number }[]
97
+ // ---cut---
98
+ const paginator = new LengthAwarePaginator(pageOfItems, 100, 15, 2)
99
+ ```
100
+
101
+ ### Customizing pagination URLs
102
+
103
+ The links point at the current URL, without its query string. `withPath()` points them elsewhere:
104
+
105
+ ```ts
106
+ const users = (await User.paginate(15)).withPath('/admin/users')
107
+ ```
108
+
109
+ #### Appending query string values
110
+
111
+ `appends()` adds values to every link, and `withQueryString()` keeps the current request's:
112
+
113
+ ```ts
114
+ const users = (await User.paginate(15)).appends('sort', 'votes')
115
+
116
+ const filtered = (await User.paginate(15)).withQueryString()
117
+ ```
118
+
119
+ #### Appending hash fragments
120
+
121
+ ```ts
122
+ const users = (await User.paginate(15)).fragment('users')
123
+ ```
124
+
125
+ ## Displaying pagination results
126
+
127
+ A paginator is iterable, so `for...of` goes through its items, and `items()` returns them as an array. Its other
128
+ methods describe the page:
129
+
130
+ | Method | Returns |
131
+ | --- | --- |
132
+ | `currentPage()` | The current page number |
133
+ | `firstItem()`, `lastItem()` | The number of the page's first and last item, or `null` for an empty page |
134
+ | `perPage()` | The page size |
135
+ | `count()` | The number of items on this page |
136
+ | `onFirstPage()`, `onLastPage()` | Whether this is the first or the last page (`onLastPage()`: not `cursorPaginate()`) |
137
+ | `hasPages()`, `hasMorePages()` | Whether there are pages to show (not `cursorPaginate()`), and pages after this one |
138
+ | `previousPageUrl()`, `nextPageUrl()` | The URL of the previous and next pages, or `null` |
139
+ | `url(page)` | The URL of a page |
140
+ | `total()`, `lastPage()` | The number of rows and of pages (`paginate()` only) |
141
+ | `cursor()`, `nextCursor()`, `previousCursor()` | The cursors (`cursorPaginate()` only) |
142
+ | `getCollection()`, `through(fn)` | The items as a collection, and a paginator of the items passed through `fn` |
143
+
144
+ ### In views
145
+
146
+ Oriel's views are TSX, so there is no `links()` that prints the links for you. `linkCollection()` gives them as objects,
147
+ with a `url` (`null` for the "..." gaps, and for Previous or Next when there is no such page), a `label` and whether the
148
+ link is `active`, from "« Previous" to "Next »":
149
+
150
+ ```tsx title="resources/views/user/index.tsx"
151
+ import type { LengthAwarePaginator } from '@orieljs/framework'
152
+
153
+ interface User {
154
+ id: number
155
+ name: string
156
+ }
157
+
158
+ export default function Index({ users }: { users: LengthAwarePaginator<User> }) {
159
+ return (
160
+ <div>
161
+ <ul>
162
+ {users.items().map((user) => (
163
+ <li>{user.name}</li>
164
+ ))}
165
+ </ul>
166
+ <nav>
167
+ {users.linkCollection().map((link) =>
168
+ link.url === null ? <span>{link.label}</span> : <a href={link.url} class={link.active ? 'active' : undefined}>{link.label}</a>,
169
+ )}
170
+ </nav>
171
+ </div>
172
+ )
173
+ }
174
+ ```
175
+
176
+ `onEachSide(count)` sets how many page links show on each side of the current one, 3 unless you set it.
177
+
178
+ ### In Inertia pages
179
+
180
+ Pass the paginator as a prop. It reaches the page as Laravel's JSON shape, which `oriel inertia:types` types: the
181
+ `data`, the page numbers and URLs, and the `links`. `Inertia.scroll(paginator)` makes it a prop for infinite scrolling
182
+ (see [More Inertia features](inertia.md#more-inertia-features)).
183
+
184
+ ### In Live components
185
+
186
+ A Live component pages with the `WithPagination` mixin, and shows its links with the `Pagination` component:
187
+
188
+ ```tsx title="app/Live/ShowPosts.tsx"
189
+ import { Component, Pagination, WithPagination } from '@orieljs/framework'
190
+ import { Post } from '../Models/Post'
191
+
192
+ export default class ShowPosts extends WithPagination(Component) {
193
+ search = ''
194
+
195
+ async updatedSearch() {
196
+ await this.resetPage()
197
+ }
198
+
199
+ async render() {
200
+ const posts = await Post.whereLike('title', `%${this.search}%`).paginate(10)
201
+
202
+ return (
203
+ <div>
204
+ <ul>
205
+ {posts.items().map((post) => (
206
+ <li>{post.title}</li>
207
+ ))}
208
+ </ul>
209
+ <Pagination paginator={posts} />
210
+ </div>
211
+ )
212
+ }
213
+ }
214
+ ```
215
+
216
+ The page number is the component's `page`: its actions `nextPage`, `previousPage`, `gotoPage(n)` and `resetPage`
217
+ move it. It isn't written to the address bar, so a reload shows page 1. `Pagination` takes `paginate()` and
218
+ `simplePaginate()` paginators, not cursor ones.
219
+
220
+ ## Converting results to JSON
221
+
222
+ A paginator returned from a route, or given to `JSON.stringify()`, becomes Laravel's JSON shape:
223
+
224
+ ```json
225
+ {
226
+ "current_page": 1,
227
+ "data": [{ "id": 1, "name": "Taylor" }],
228
+ "first_page_url": "http://localhost/users?page=1",
229
+ "from": 1,
230
+ "last_page": 4,
231
+ "last_page_url": "http://localhost/users?page=4",
232
+ "links": [{ "url": null, "label": "« Previous", "active": false }],
233
+ "next_page_url": "http://localhost/users?page=2",
234
+ "path": "http://localhost/users",
235
+ "per_page": 15,
236
+ "prev_page_url": null,
237
+ "to": 15,
238
+ "total": 50
239
+ }
240
+ ```
241
+
242
+ A simple paginator has no `last_page`, `last_page_url`, `links` or `total`, and a cursor paginator has `next_cursor`
243
+ and `prev_cursor` in place of page numbers.
244
+
245
+ > [!NOTE]
246
+ > Laravel renders the links with Blade views you can publish and theme (`links()`, Tailwind and Bootstrap views).
247
+ > Oriel builds them in your TSX from `linkCollection()`, or with Live's `Pagination` component.
@@ -0,0 +1,207 @@
1
+ # Resetting passwords
2
+
3
+ A user who forgot their password asks for a reset link by email, and sets a new password on the page it opens. The
4
+ `Password` facade does the work, and the starter kits come with the pages.
5
+
6
+ ## Introduction
7
+
8
+ Every starter kit lets a user reset their password: the kits with pages have a "Forgot password" page and a page to set
9
+ the new password, and the API kit has the two endpoints (see [Starter kits](starter-kits.md#authentication)). This page
10
+ explains what they do, so you can change them or write your own.
11
+
12
+ ### Model preparation
13
+
14
+ The user model must be able to give its email address and to receive the reset mail. A model built on
15
+ `AuthenticatableModel`, as a new app's `User` is, does both: `getEmailForPasswordReset()` returns its `email`
16
+ attribute, and `sendPasswordResetNotification(token)` sends it the `ResetPassword` notification by mail.
17
+
18
+ ### Database preparation
19
+
20
+ The reset tokens are stored in the `password_reset_tokens` table, which a new app's first migration creates: one row
21
+ per email address, with the token and when it was made. A new token for an address replaces the one before it.
22
+
23
+ ### Configuring trusted hosts
24
+
25
+ > [!WARNING]
26
+ > The reset link in the mail starts with the scheme and host of the request that asked for it, as Laravel's does.
27
+ > Laravel can restrict the hosts it answers (`trustHosts()`); Oriel can't. Have your web server or proxy answer only
28
+ > your app's own host names, or build the link from `APP_URL` yourself, as
29
+ > [Reset link customization](#reset-link-customization) shows, so that a request with a forged `Host` header can't
30
+ > send your users a link to another site.
31
+
32
+ ## Routing
33
+
34
+ ### Requesting the password reset link
35
+
36
+ #### The password reset link request form
37
+
38
+ The kits with pages show the form at `/forgot-password`, the `password.request` route, behind the `guest` middleware.
39
+ It asks for the email address and posts it to the same URI.
40
+
41
+ #### Handling the form submission
42
+
43
+ `Password.sendResetLink()` takes the credentials that find the user, the email address here, and sends the user a
44
+ reset link:
45
+
46
+ ```ts title="app/Http/Controllers/Auth/PasswordResetLinkController.ts"
47
+ import { __, back, Password, request } from '@orieljs/framework'
48
+ import { Controller } from '../Controller'
49
+
50
+ export class PasswordResetLinkController extends Controller {
51
+ /** Handle an incoming password reset link request. */
52
+ async store() {
53
+ await request().validate({
54
+ email: 'required|email',
55
+ })
56
+
57
+ await Password.sendResetLink(request().only('email'))
58
+
59
+ return back().with('status', __('A reset link will be sent if the account exists.'))
60
+ }
61
+ }
62
+ ```
63
+
64
+ It returns a status, which `__()` turns into a message:
65
+
66
+ | Status | Message |
67
+ | --- | --- |
68
+ | `passwords.sent` | "We have emailed your password reset link." |
69
+ | `passwords.user` | "We can't find a user with that email address." |
70
+ | `passwords.throttled` | "Please wait before retrying." |
71
+
72
+ The kits answer with the same message whatever the status, as above, so the form can't be used to find out who has an
73
+ account.
74
+
75
+ A new link for an address is refused while the last one is younger than `throttle` seconds, 60 in a new app, so the
76
+ form can't be used to flood a user's inbox. With a function as the second argument, `sendResetLink()` calls it with
77
+ the user and the token instead of sending the mail.
78
+
79
+ ### Resetting the password
80
+
81
+ #### The password reset form
82
+
83
+ The link in the mail opens `/reset-password/{token}?email=…`, the `password.reset` route. The page there asks for the
84
+ new password, twice, and posts it with the token and the email address to `/reset-password`, the `password.store`
85
+ route.
86
+
87
+ #### Handling the form submission
88
+
89
+ `Password.reset()` checks the token against the user's, then calls your function with the user and the new password,
90
+ and deletes the token. Your function saves the password, hashed:
91
+
92
+ ```ts title="app/Http/Controllers/Auth/NewPasswordController.ts"
93
+ import { randomBytes } from 'node:crypto'
94
+ import { __, event, Hash, Password, PasswordReset, redirect, request, Rule, ValidationException } from '@orieljs/framework'
95
+ import type { User } from '../../../Models/User'
96
+ import { Controller } from '../Controller'
97
+
98
+ export class NewPasswordController extends Controller {
99
+ /** Handle an incoming new password request. */
100
+ async store() {
101
+ await request().validate({
102
+ token: 'required',
103
+ email: 'required|email',
104
+ password: ['required', 'confirmed', Rule.password()],
105
+ })
106
+
107
+ const status = await Password.reset(
108
+ request().only('email', 'password', 'password_confirmation', 'token'),
109
+ async (user, password) => {
110
+ await (user as User)
111
+ .forceFill({
112
+ password: await Hash.make(password),
113
+ remember_token: randomBytes(30).toString('hex'),
114
+ })
115
+ .save()
116
+
117
+ await event(new PasswordReset(user))
118
+ },
119
+ )
120
+
121
+ if (status === 'passwords.reset') {
122
+ return redirect().route('login').with('status', __(status))
123
+ }
124
+
125
+ // An unknown email answers like a bad token, so the reset can't reveal whether an account exists.
126
+ throw ValidationException.withMessages({ email: [__(status === 'passwords.user' ? 'passwords.token' : status)] })
127
+ }
128
+ }
129
+ ```
130
+
131
+ `reset()` returns `passwords.reset` ("Your password has been reset."), `passwords.token` ("This password reset token is
132
+ invalid.") for a wrong or expired token, or `passwords.user` when no user has the address. A token lasts `expire`
133
+ minutes, 60 in a new app. The new `remember_token` signs the user out of the browsers where they chose "Remember me".
134
+
135
+ In the Live kit, the two pages are the `ForgotPassword` and `ResetPassword` components, which do the same in their
136
+ actions. The API kit's endpoints answer JSON, and its reset also revokes every API token of the user.
137
+
138
+ A model with the `hashed` cast on `password` can take the plain password in the callback, in place of
139
+ `Hash.make()`.
140
+
141
+ ## Deleting expired tokens
142
+
143
+ An expired token can't be used, but its row stays until the user asks for another link. Laravel's
144
+ `auth:clear-resets` command isn't in Oriel; the token repository's `deleteExpired()` deletes them, so schedule it in
145
+ `routes/console.ts` if you want the table kept small:
146
+
147
+ ```ts title="routes/console.ts"
148
+ import { Password, Schedule } from '@orieljs/framework'
149
+
150
+ Schedule.call(() => Password.broker().tokens.deleteExpired())
151
+ .everyFifteenMinutes()
152
+ .name('clear-password-resets')
153
+ ```
154
+
155
+ ## Customization
156
+
157
+ The broker's settings are under `passwords` in `config/auth.ts`:
158
+
159
+ ```ts
160
+ const auth = {
161
+ passwords: { users: { provider: 'users', table: 'password_reset_tokens', expire: 60, throttle: 60 } },
162
+ }
163
+ ```
164
+
165
+ `expire` is in minutes and `throttle` in seconds. `Password.broker(name)` uses another entry of `passwords`, with its
166
+ own table or user provider.
167
+
168
+ #### Reset link customization
169
+
170
+ `ResetPassword.createUrlUsing()` builds the link yourself, in a service provider's `boot()`. This one starts it with
171
+ `APP_URL`, whatever host the request came to:
172
+
173
+ ```ts title="app/Providers/AppServiceProvider.ts"
174
+ import { config, ResetPassword, ServiceProvider } from '@orieljs/framework'
175
+
176
+ export class AppServiceProvider extends ServiceProvider {
177
+ override boot(): void {
178
+ ResetPassword.createUrlUsing((user, token) => {
179
+ const email = encodeURIComponent(user.getEmailForPasswordReset())
180
+
181
+ return `${String(config('app.url'))}/reset-password/${token}?email=${email}`
182
+ })
183
+ }
184
+ }
185
+ ```
186
+
187
+ A client app on another domain, such as one using the API kit, points the link at its own page the same way.
188
+
189
+ #### Reset email customization
190
+
191
+ `ResetPassword.toMailUsing()` builds the mail yourself: return a `MailMessage` with your own lines, or a
192
+ [mailable](mail.md). Its function receives the user and the token:
193
+
194
+ ```ts title="app/Providers/AppServiceProvider.ts"
195
+ import { MailMessage, ResetPassword, ServiceProvider, url } from '@orieljs/framework'
196
+
197
+ export class AppServiceProvider extends ServiceProvider {
198
+ override boot(): void {
199
+ ResetPassword.toMailUsing((user, token) =>
200
+ new MailMessage()
201
+ .subject('Reset your password')
202
+ .line('Someone asked to reset the password of your account.')
203
+ .action('Choose a new password', url(`/reset-password/${token}`, { email: user.getEmailForPasswordReset() })),
204
+ )
205
+ }
206
+ }
207
+ ```
@@ -0,0 +1,187 @@
1
+ # Service providers
2
+
3
+ Service providers set up your app and every part of Oriel: they bind services in the container, then configure them
4
+ once every provider has registered.
5
+
6
+ ## Introduction
7
+
8
+ Service providers are the central place of your app's setup. Your own app, and each of Oriel's components, start
9
+ through them. Setting up here means registering things: bindings in the [service container](container.md), event
10
+ listeners, view composers, route patterns and bindings.
11
+
12
+ Oriel uses 22 service providers of its own, for the mailer, the queue, the cache and the rest. [Request
13
+ lifecycle](lifecycle.md#service-providers) lists them, and shows where providers fit in how your app starts. Your own
14
+ providers are listed in `bootstrap/providers.ts`. This page shows how to write them and register them.
15
+
16
+ ## Writing service providers
17
+
18
+ A service provider extends the `ServiceProvider` class. Most have a `register()` method and a `boot()` method. In
19
+ `register()`, only bind things into the service container: never listen for events or set anything else up there, as
20
+ the providers that offer what you would use may not have registered yet.
21
+
22
+ `make:provider` writes a new provider in `app/Providers`:
23
+
24
+ ```sh
25
+ node oriel make:provider Riak
26
+ ```
27
+
28
+ It adds `ServiceProvider` to the name when the name doesn't end with it, so this writes
29
+ `app/Providers/RiakServiceProvider.ts`, with an empty `register()` and `boot()`:
30
+
31
+ ```ts title="app/Providers/RiakServiceProvider.ts"
32
+ import { ServiceProvider } from '@orieljs/framework'
33
+
34
+ export class RiakServiceProvider extends ServiceProvider {
35
+ override register(): void {
36
+ //
37
+ }
38
+
39
+ override boot(): void {
40
+ //
41
+ }
42
+ }
43
+ ```
44
+
45
+ `make:provider` doesn't register the provider: add it to `bootstrap/providers.ts` yourself, as [Registering
46
+ providers](#registering-providers) shows. `--force` overwrites a provider that exists.
47
+
48
+ > [!NOTE]
49
+ > Laravel's `make:provider` adds the new provider to `bootstrap/providers.php` for you. Oriel's writes the class only.
50
+
51
+ ### The register method
52
+
53
+ In `register()`, bind things into the service container, and nothing else. Every provider method has the
54
+ application, which is also the container, in `this.app`:
55
+
56
+ ```ts title="app/Providers/RiakServiceProvider.ts"
57
+ // @filename: app/Services/Riak/Connection.ts
58
+ export declare class Connection {
59
+ constructor(host: string, port: number)
60
+ }
61
+ // ---cut---
62
+ import { Config, ServiceProvider } from '@orieljs/framework'
63
+ import { Connection } from '../Services/Riak/Connection'
64
+
65
+ export class RiakServiceProvider extends ServiceProvider {
66
+ override register(): void {
67
+ this.app.singleton(Connection, () => new Connection(Config.string('riak.host'), Config.integer('riak.port')))
68
+ }
69
+ }
70
+ ```
71
+
72
+ This provider only has a `register()` method, which tells the container how to build a `Connection`. [Service
73
+ container](container.md) covers the bindings.
74
+
75
+ `register()` is synchronous: Oriel doesn't wait for a promise it returns, and the next provider registers at once. Do
76
+ async work in `boot()`, which Oriel awaits. Your configuration is loaded before any provider registers, so
77
+ `register()` can read it.
78
+
79
+ > [!NOTE]
80
+ > Laravel's providers can list simple bindings in `$bindings` and `$singletons` properties. Oriel's have no such
81
+ > properties: call `this.app.bind()` and `this.app.singleton()` in `register()`.
82
+
83
+ #### Default configuration
84
+
85
+ A provider that ships with its own settings, as a package's does, gives them to your app with `mergeConfigFrom()` in
86
+ `register()`. It takes the defaults and the name of the configuration file they belong to:
87
+
88
+ ```ts title="app/Providers/RiakServiceProvider.ts"
89
+ import { ServiceProvider } from '@orieljs/framework'
90
+
91
+ export class RiakServiceProvider extends ServiceProvider {
92
+ override register(): void {
93
+ this.mergeConfigFrom({ host: '127.0.0.1', port: 8087, options: { timeout: 5 } }, 'riak')
94
+ }
95
+ }
96
+ ```
97
+
98
+ `config('riak.port')` is then 8087 unless your app's `config/riak.ts` sets a `port`, which wins. The merge is shallow,
99
+ as Laravel's is: each key your app sets replaces the default's whole value. If `config/riak.ts` sets
100
+ `options: { retries: 3 }`, the `options` are `{ retries: 3 }`, and `timeout` is gone.
101
+
102
+ ### The boot method
103
+
104
+ `boot()` runs once every provider has registered, so every binding of the framework and of your app is in place.
105
+ Set things up there, such as a [view composer](views.md#view-composers):
106
+
107
+ ```ts title="app/Providers/ComposerServiceProvider.ts"
108
+ import { ServiceProvider, View } from '@orieljs/framework'
109
+
110
+ export class ComposerServiceProvider extends ServiceProvider {
111
+ override boot(): void {
112
+ View.composer('profile', (view) => {
113
+ // ...
114
+ })
115
+ }
116
+ }
117
+ ```
118
+
119
+ `boot()` may be async: Oriel awaits it before it boots the next provider. The providers boot in the order they
120
+ registered, the framework's first, and your routes load after the last one has booted (see [Request
121
+ lifecycle](lifecycle.md#service-providers)).
122
+
123
+ An error thrown in `register()` or `boot()` stops your app from starting. Oriel throws a `ProviderRegisterError` or a
124
+ `ProviderBootError` that names the provider, with the original error as its `cause`: "Service provider
125
+ [RiakServiceProvider] failed to boot: Riak is down".
126
+
127
+ #### Boot method dependency injection
128
+
129
+ `boot()` takes no arguments. Resolve what it needs from the container, with `this.app.make()`:
130
+
131
+ ```ts title="app/Providers/AppServiceProvider.ts"
132
+ import { ServiceProvider } from '@orieljs/framework'
133
+ import { ConfigRepository } from '@orieljs/framework/contracts'
134
+
135
+ export class AppServiceProvider extends ServiceProvider {
136
+ override boot(): void {
137
+ const config = this.app.make(ConfigRepository)
138
+
139
+ if (config.get('app.debug')) {
140
+ // ...
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ > [!NOTE]
147
+ > Laravel injects the services that `boot()`'s parameters type-hint. TypeScript types are gone when your app runs,
148
+ > so Oriel calls `boot()` with no arguments: take what you need from `this.app`, or with `inject()`.
149
+
150
+ ## Registering providers
151
+
152
+ Your providers are listed in `bootstrap/providers.ts`. It exports a list of the provider classes, which
153
+ `bootstrap/app.ts` gives to `withProviders()`:
154
+
155
+ ```ts title="bootstrap/providers.ts"
156
+ import { AppServiceProvider } from '../app/Providers/AppServiceProvider'
157
+
158
+ export default [AppServiceProvider]
159
+ ```
160
+
161
+ To register a provider you wrote, add it to the list:
162
+
163
+ ```ts title="bootstrap/providers.ts"
164
+ // @filename: app/Providers/ComposerServiceProvider.ts
165
+ import { ServiceProvider } from '@orieljs/framework'
166
+ export class ComposerServiceProvider extends ServiceProvider {}
167
+ // ---cut---
168
+ import { AppServiceProvider } from '../app/Providers/AppServiceProvider'
169
+ import { ComposerServiceProvider } from '../app/Providers/ComposerServiceProvider'
170
+
171
+ export default [AppServiceProvider, ComposerServiceProvider]
172
+ ```
173
+
174
+ Oriel registers the framework's providers first, then yours, in the order of the list. A provider listed twice is
175
+ registered once.
176
+
177
+ `this.app.register()` registers a provider while your app runs, as a package's provider might for another one it
178
+ needs: `await this.app.register(RiakServiceProvider)`. If the app has booted already, the new provider boots at once.
179
+
180
+ ## Deferred providers
181
+
182
+ Every provider registers and boots when your app starts: Oriel has no deferred providers.
183
+
184
+ > [!NOTE]
185
+ > Laravel can defer a provider that only binds services until one of them is needed, through `DeferrableProvider` and
186
+ > `provides()`, so that a request doesn't load providers it never uses. An Oriel app boots once per process, not once
187
+ > per request, so each provider's cost is paid once: Oriel loads them all.