Architecture
Facades
Facades give you Oriel's services by name, as Cache.get('key'), resolved from the current request or the
application each time you use them.
Introduction#
Code throughout these pages reaches Oriel's features through facades. A facade gives you a service of the service
container under a short name, as if its methods were static, and forwards each call to the service the
container holds. Oriel has a facade for most of its features, and exports them all from @orieljs/:
import { Cache, Route } from '@orieljs/framework'Route.get('/cache', () => Cache.get('key'))Helper functions#
Alongside the facades, Oriel has helper functions for the features you use most: view(), response(), config(),
request(), session() and others. Each feature's page documents its helper, and Helpers lists them all.
A helper and its facade reach the same service, so these two are the same:
const fromFacade = View.make('profile')const fromHelper = view('profile')When to use facades#
Facades are short to write: a feature's name, then its method, with nothing to inject or configure. Because they ask for nothing, a class can come to use many of them, and grow past one job without you noticing. When a class reaches for many facades, consider splitting it into smaller classes.
Facades vs. dependency injection#
A service you inject can be replaced in a test by a stand-in. A facade's service can be replaced the same way, since the facade resolves it from the container at each call: your tests give the container a fake, and the code under test uses it through the facade.
The facades of the services you most often check in a test have a fake(): Mail.fake(), Queue.fake(),
Bus.fake(), Event.fake(), Storage.fake() and Notification.fake(). Each puts a fake in place of the service
and returns it, and you check what your code did on the fake it returned:
import { Mail } from '@orieljs/framework'import { test } from 'vitest'import { OrderShipped } from '../../app/Mail/OrderShipped'import { TestCase } from '../TestCase'const t = TestCase.boot()test('shipping an order sends the mail', async () => { const mail = Mail.fake() await t.post('/orders/1/ship') mail.assertSent(OrderShipped)})Storage.fake('avatars') returns the fake disk. Fakes lists every fake and its assertions.
For any other facade, swap() puts an object of your own in place of the service:
import { RefreshDatabase } from '@orieljs/framework/testing'import { expect, test } from 'vitest'import { Podcasts } from '../../app/Facades/Podcasts'import { Podcast } from '../../app/Models/Podcast'import { TestCase } from '../TestCase'const t = TestCase.boot({ uses: [RefreshDatabase] })test('a podcast can be published', async () => { const podcast = await Podcast.create({ title: 'Episode 1', audio_path: 'podcasts/1.mp3' }) const published: number[] = [] Podcasts.swap({ publish: async (episode) => void published.push(episode.id) }) await t.post(`/podcasts/${podcast.id}/publish`) expect(published).toEqual([podcast.id])})swap() binds the object as an instance on the current container. In a test, outside any request, that is the test's
application, so every request the test makes uses it. Inside a request, it replaces the service for that request
only.
Facades vs. helper functions#
A helper does what its facade does: view('profile') and View.make('profile') both ask the view factory for the
view. Both resolve the same service from the container, so a fake or a swap() reaches the code that uses the
helper too:
Route.get('/cache', () => cache('key'))cache('key') calls get() on the cache manager the Cache facade stands for, so a test that swaps the Cache
facade's service changes what this route returns.
How facades work#
A facade is an object that facade() makes from a class or a
token. Each time you read one of its properties, it resolves the
service from the current container and returns the service's property, its methods bound to the service. The current
container is the one inject() uses (see Injecting with inject()): inside a
request, a queued job or a command, its scope; otherwise, the application. So Session reaches the current request's
session, and Mail the mail manager your app shares.
You can make a facade for a service of your own:
import { facade } from '@orieljs/framework'import { PodcastPublisher } from '../Services/PodcastPublisher'export const Podcasts = facade(PodcastPublisher)From then on, Podcasts.publish(podcast) calls publish() on the PodcastPublisher the container gives you, a new
one each time unless you bind it as a singleton. facade() takes a second argument, an object of extra methods,
which is how Mail has fake() and Hash has driver().
Typing#
A facade is typed as the service it stands for, with its extra methods and two of its own: getFacadeRoot(), which
returns the service, and swap(). TypeScript checks every call:
Route.get('/podcasts', () => 'Podcasts')// @ts-expect-error: the router has no post2()Route.post2('/podcasts', () => 'Created')Route is typed as Oriel's router, so a misspelt method or a wrong argument fails the type check, and your editor
completes the facade's methods. The Facade<T> type, exported from @orieljs/, names the type of a facade
you write yourself.
Facades at a module's top level#
A facade resolves its service when you use it, so it works only while your app is running. Don't use one at a module's top level, outside any function:
import { Config } from '@orieljs/framework'// Runs when the module is first imported: before your app exists, this throws.const appName = Config.string('app.name')export function greeting(): string { return `Welcome to ${appName}`}A module's top level runs once, when the module is first imported. Your providers, and what they import, are imported
with bootstrap/, before createApplication() has made the app, and a test file's imports run before its first
test has an app. Then no container is available, and the facade throws a ContainerNotAvailableError: "Cannot resolve
[Config.Repository]: no container is available. inject(), helpers and facades only work while the application is
running (inside a request, job, command, provider or app.run()). Don't call them at module top level."
When an app does exist, the top level resolves that app's service, once, and keeps it: your tests make a new app for each test, and every test would see the first test's value. Use the facade inside the function instead:
import { Config } from '@orieljs/framework'export function greeting(): string { return `Welcome to ${Config.string('app.name')}`}The route files are the exception: the app imports each one itself, afresh for every app and inside it, so a route
file calls the Route facade at its top level.
Real-time facades#
Oriel has no real-time facades, which Laravel makes from any class you import with a Facades\ namespace prefix.
Make a facade for your class with facade(), as in How facades work.
Facade class reference#
Every facade, and what it resolves. The bindings are the tokens of @orieljs/, which inject()
and app().make() also take:
| Facade | Resolves to | Binding |
|---|---|---|
Artisan | The console kernel: call() runs a command | ConsoleKernel |
Auth | The auth manager: guard(), and the default guard's user(), check(), attempt()… | AuthFactory |
Bus | The command bus: dispatch(), chain(), batch() | BusDispatcher |
Cache | The cache manager: store(), and the default store's methods | CacheManager |
Config | The configuration repository (see The Config facade) | ConfigRepository |
Cookie | The current request's cookie jar, plus get() and has() for the request's cookies | CookieJar (scoped) |
Crypt | The encrypter | Encrypter |
DB | The database manager: connection(), and the default connection's methods; DB.raw() | DatabaseManager |
Event | The event dispatcher | EventDispatcher |
Gate | The authorization gate | Gate |
Hash | The hash manager, which hashes with the default driver; driver() | Hasher |
Inertia | The current request's Inertia response factory | InertiaFactory (scoped) |
Live | The Live component manager: Live.component() registers a component; Live.test() mounts one in a test | LiveManager |
Log | The log manager: channel(), and the default channel's methods | LogManager |
Mail | The mail manager: mailer(), and the default mailer's methods | MailManager |
Notification | The notification dispatcher, through the statics of the Notification class | NotificationDispatcher |
Password | The default password broker; broker() | PasswordBroker |
Queue | The queue manager: connection(), and the default connection's methods | QueueManager |
RateLimiter | The rate limiter, which throttle uses: hit(), tooManyAttempts(), clear() | RateLimiter |
Route | The router | Router |
Schedule | The schedule: command(), job(), call() | Schedule |
Schema | The default database connection's schema builder | SchemaBuilderToken, from @orieljs/ |
Session | The current request's session; Session.extend() | Session (scoped) |
Storage | The filesystem manager: disk(), and the default disk's methods | FilesystemManager |
Validator | The validation factory: make(), extend() | ValidationFactory |
View | The view factory: make(), share(), composer() | ViewFactory |
A scoped facade needs a scope. Outside any, Cookie and Session throw a BindingResolutionError ("Scoped
binding [Session.Store] was resolved outside a scope. …"), and Inertia, outside a request, uses the application's
own response factory.
Inertia and Live are written by hand rather than with facade(), and Live is also the <Live> element of your
views. Notification is the base class of your notifications, whose statics are the facade (NotificationFacade,
also exported, is a plain facade over the same dispatcher). None of the three has getFacadeRoot(), and only
Notification has swap().
The Config facade#
Config resolves your app's configuration, as the config() helper does. It has the repository's get(), set(),
has(), push() and all(), and adds prepend(), getMany() and typed getters:
const name = Config.string('app.name')const debug = Config.boolean('app.debug', false)const values = Config.getMany(['app.name', 'app.env'])string(), integer(), float(), boolean() and array() return the value typed, or throw a TypeError when it
isn't of that type: "Configuration value for key [app.name] must be a string, undefined given.". float() takes any
number, and array() a list or an object. Configuration covers your configuration files.