Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

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/framework:

routes/web.ts
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:

tests/Feature/ShipOrder.test.ts
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:

tests/Feature/PublishPodcast.test.ts
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:

app/Facades/Podcasts.ts
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/framework, 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:

app/Services/Greeting.ts
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/app.ts, 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:

app/Services/Greeting.ts
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/framework/contracts, which inject() and app().make() also take:

FacadeResolves toBinding
ArtisanThe console kernel: call() runs a commandConsoleKernel
AuthThe auth manager: guard(), and the default guard's user(), check(), attempt()…AuthFactory
BusThe command bus: dispatch(), chain(), batch()BusDispatcher
CacheThe cache manager: store(), and the default store's methodsCacheManager
ConfigThe configuration repository (see The Config facade)ConfigRepository
CookieThe current request's cookie jar, plus get() and has() for the request's cookiesCookieJar (scoped)
CryptThe encrypterEncrypter
DBThe database manager: connection(), and the default connection's methods; DB.raw()DatabaseManager
EventThe event dispatcherEventDispatcher
GateThe authorization gateGate
HashThe hash manager, which hashes with the default driver; driver()Hasher
InertiaThe current request's Inertia response factoryInertiaFactory (scoped)
LiveThe Live component manager: Live.component() registers a component; Live.test() mounts one in a testLiveManager
LogThe log manager: channel(), and the default channel's methodsLogManager
MailThe mail manager: mailer(), and the default mailer's methodsMailManager
NotificationThe notification dispatcher, through the statics of the Notification classNotificationDispatcher
PasswordThe default password broker; broker()PasswordBroker
QueueThe queue manager: connection(), and the default connection's methodsQueueManager
RateLimiterThe rate limiter, which throttle uses: hit(), tooManyAttempts(), clear()RateLimiter
RouteThe routerRouter
ScheduleThe schedule: command(), job(), call()Schedule
SchemaThe default database connection's schema builderSchemaBuilderToken, from @orieljs/framework
SessionThe current request's session; Session.extend()Session (scoped)
StorageThe filesystem manager: disk(), and the default disk's methodsFilesystemManager
ValidatorThe validation factory: make(), extend()ValidationFactory
ViewThe 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.

Privacy policy ·

esc