Digging deeper
Helpers
Oriel includes Laravel's string and array helpers, Str, Stringable and Arr, and its helper functions, such as
config(), route() and retry().
Introduction#
The framework uses these helpers itself, and you can use them in your own code. Each one is a named export of
@orieljs/, so you import it as you import anything else:
import { redirect, request, Str } from '@orieljs/framework'import { Post } from '../../Models/Post'import { Controller } from './Controller'export class PostController extends Controller { async store() { const title = request().string('title').trim().toString() const post = await Post.create({ title, slug: Str.slug(title), body: '' }) return redirect().route('posts.show', post) }}Available methods#
Arrays and objects: Arr.dot(), Arr.except(), Arr.first(),
Arr.forget(), Arr.get(), Arr.has(), Arr.last(),
Arr.only(), Arr.set(), Arr.undot(), Arr.wrap(),
classNames(), styleString()
Paths: app().basePath() and the other paths
Strings: __(), e(), Str.after(),
Str.afterLast(), Str.ascii(), Str.before(),
Str.beforeLast(), Str.between(), Str.camel(),
Str.contains(), Str.endsWith(), Str.finish(), Str.is(),
Str.isUlid(), Str.isUuid(), Str.kebab(), Str.lcfirst(),
Str.limit(), Str.of(), Str.plural(), Str.random(),
Str.singular(), Str.slug(), Str.snake(), Str.start(),
Str.startsWith(), Str.studly(), Str.title(),
Str.ucfirst(), Str.ulid(), Str.uuid(), Str.uuid7()
Fluent strings: the methods of a Stringable, listed in Fluent strings
Miscellaneous: abort(), abortIf(), abortUnless(), app(),
auth(), back(), bcrypt(), cache(), collect(),
config(), cookie(), csrfField(), csrfToken(),
decrypt(), dispatch(), dispatchSync(), encrypt(),
env(), errors(), event(), fake(), inertia(),
inject(), live(), logger(), methodField(), now(),
old(), raw(), redirect(), request(), response(),
retry(), session(), tap(), value(), view(),
vite()
Other utilities: Pipeline
Arrays and objects#
The Arr methods work on plain objects and arrays. The ones that take a key read it with dots, so 'products.desk'
is the desk key of the products object, and 'users.0.name' the name of the first user.
Arr.dot()#
Arr.dot() flattens a nested object into one level, with dotted keys:
const flattened = Arr.dot({ products: { desk: { price: 100 } } })// { 'products.desk.price': 100 }Arrays are flattened by index ('tags.0'), and an empty object or array stays a value.
Arr.except()#
Arr.except() returns a copy of the object without the keys you list:
const filtered = Arr.except({ name: 'Desk', price: 100 }, ['price'])// { name: 'Desk' }The keys are top-level keys, where Laravel's Arr::except() also reads a dotted key as a path.
Arr.first()#
Arr.first() returns the first item of an array that passes a test, or the default when none does:
const first = Arr.first([100, 200, 300], (value) => value >= 150)// 200const none = Arr.first([100, 200, 300], (value) => value >= 500, 'none')// 'none'The test receives the item and its index. Without a test, it returns the first item, or the default for an empty array.
Arr.forget()#
Arr.forget() deletes a key, given with dots, from the object itself:
const data = { products: { desk: { price: 100 } } }Arr.forget(data, 'products.desk')// { products: {} }Arr.get()#
Arr.get() reads a value with dots, and returns the default when a key on the way is missing:
const price = Arr.get({ products: { desk: { price: 100 } } }, 'products.desk.price')// 100const discount = Arr.get({ products: { desk: { price: 100 } } }, 'products.desk.discount', 0)// 0A type argument types the value, as a cast, not a check: Arr.get<number>(data, 'products.desk.price').
Arr.has()#
Arr.has() tells you whether a key, given with dots, exists:
const has = Arr.has({ product: { name: 'Desk', price: 100 } }, 'product.name')// trueA key that holds null or undefined exists.
Arr.last()#
Arr.last() returns the last item of an array that passes a test, or the default when none does:
const last = Arr.last([100, 200, 300, 110], (value) => value >= 150)// 300Arr.only()#
Arr.only() returns a copy of the object with only the keys you list:
const slice = Arr.only({ name: 'Desk', price: 100, orders: 10 }, ['name', 'price'])// { name: 'Desk', price: 100 }Arr.set()#
Arr.set() sets a value with dots, in the object itself, and creates the objects on the way:
const data = { products: { desk: { price: 100 } } }Arr.set(data, 'products.desk.price', 200)// { products: { desk: { price: 200 } } }It returns the object. A key with a __proto__, prototype or constructor part throws, so data from a request
can't change an object's prototype.
Arr.undot()#
Arr.undot() turns dotted keys back into nested objects:
const nested = Arr.undot({ 'user.name': 'Kevin Malone', 'user.occupation': 'Accountant' })// { user: { name: 'Kevin Malone', occupation: 'Accountant' } }Arr.wrap()#
Arr.wrap() wraps a value in an array. An array is returned as it is, and null or undefined gives an empty
array:
Arr.wrap('Laravel') // ['Laravel']Arr.wrap(['Laravel']) // ['Laravel']Arr.wrap(null) // []classNames()#
classNames() builds a class attribute from strings, lists and { name: condition } objects, as Laravel's
Arr::toCssClasses() does. The class attribute of a TSX view takes the same values:
const isActive = falseconst hasError = trueconst classes = classNames(['p-4', { 'font-bold': isActive, 'bg-red': hasError }])// 'p-4 bg-red'styleString()#
styleString() builds a style attribute, as Laravel's Arr::toCssStyles() does, but from property and value pairs,
as React's style takes them, where Laravel's takes property: value strings. Property names are written in kebab
case, and a number gets px unless the property takes a plain number:
const style = styleString({ backgroundColor: 'red', marginTop: 4, opacity: 0.5, color: false })// 'background-color:red;margin-top:4px;opacity:0.5'A property whose value is false, null, undefined or '' is left out.
Paths#
The application knows where its folders are. app() returns it, and each method takes a path to add:
const base = app().basePath()const path = app().appPath('Models/User.ts')const bootstrap = app().bootstrapPath('app.ts')const config = app().configPath('app.ts')const migrations = app().databasePath('migrations')const css = app().publicPath('css/app.css')const views = app().resourcePath('views')const logs = app().storagePath('logs/oriel.log')basePath() is the basePath your bootstrap/ gives Application.configure(), your app's root folder. The
others are its app, bootstrap, config, database, public, resources and storage folders.
Strings#
Translation strings#
__() returns the line for a translation key, with its placeholders filled:
const message = __('auth.throttle', { seconds: 30 })// 'Too many login attempts. Please try again in 30 seconds.'A key with no line comes back as it is, so a sentence can be its own key, with its placeholders. :name takes the
value as given, :Name with its first letter upper-cased, and :NAME upper-cased:
const greeting = __('Welcome, :Name', { name: 'dayle' })// 'Welcome, Dayle'e()#
e() escapes &, <, >, " and ' for HTML:
const html = e('<html>foo</html>')// '<html>foo</html>'null and undefined give '', and markup marked with raw() comes back unescaped. TSX views escape what
they print themselves, so you need e() only for HTML you build in a string.
Str.after()#
Str.after() returns everything after the first occurrence of a value, or the whole string when the value isn't in
it:
const slice = Str.after('This is my name', 'This is')// ' my name'Str.afterLast()#
Str.afterLast() returns everything after the last occurrence of a value, or the whole string when the value isn't
in it:
const slice = Str.afterLast('app/Http/Controllers/Controller.ts', '/')// 'Controller.ts'Str.ascii()#
Str.ascii() transliterates a string into ASCII, as Laravel's does for English:
const ascii = Str.ascii('Café crème')// 'Cafe creme'What has no ASCII form, such as Chinese characters or an emoji, is dropped. Str.slug() has the
details.
Str.before()#
Str.before() returns everything before the first occurrence of a value:
const slice = Str.before('This is my name', 'my name')// 'This is 'Str.beforeLast()#
Str.beforeLast() returns everything before the last occurrence of a value:
const slice = Str.beforeLast('This is my name', 'is')// 'This 'Str.between()#
Str.between() returns the part between two values: after the first occurrence of the first, and before the last
occurrence of the second:
const slice = Str.between('This is my name', 'This', 'name')// ' is my 'Str.camel()#
Str.camel() converts a string to camel case:
const converted = Str.camel('foo_bar')// 'fooBar'Str.contains()#
Str.contains() tells you whether a string contains a value, or any of a list of values. It is case-sensitive:
const contains = Str.contains('This is my name', 'my')// trueconst containsAny = Str.contains('This is my name', ['my', 'foo'])// trueStr.endsWith()#
Str.endsWith() tells you whether a string ends with a value, or with any of a list of values:
const result = Str.endsWith('This is my name', 'name')// trueStr.finish()#
Str.finish() adds a value to the end of a string, unless it already ends with it:
const adjusted = Str.finish('this/string', '/')// 'this/string/'Str.is()#
Str.is() tells you whether a string matches a pattern, in which * matches anything and every other character is
literal:
const matches = Str.is('foo*', 'foobar')// trueStr.isUlid()#
Str.isUlid() tells you whether a value is a ULID, in either case:
const isUlid = Str.isUlid('01gd6r360bp37zj17nxb55yv40')// trueStr.isUuid()#
Str.isUuid() tells you whether a value is a UUID, of any version:
const isUuid = Str.isUuid('a0a2a2d2-0b87-4a18-83f2-2529882be2de')// trueBoth checks narrow the value's type to string.
Str.kebab()#
Str.kebab() converts a string to kebab case:
const converted = Str.kebab('fooBar')// 'foo-bar'Str.lcfirst()#
Str.lcfirst() lower-cases the first character of a string:
const string = Str.lcfirst('Foo Bar')// 'foo Bar'Str.limit()#
Str.limit() cuts a string to a number of characters and adds ... when it had more:
const truncated = Str.limit('The quick brown fox jumps over the lazy dog', 20)// 'The quick brown fox...'The third argument replaces the ...:
const truncated = Str.limit('The quick brown fox jumps over the lazy dog', 20, ' (...)')// 'The quick brown fox (...)'Str.of()#
Str.of() wraps a string in a Stringable, so you can chain the string methods. null and undefined give an
empty string, and any other value is turned into a string:
const slug = Str.of(' Laravel ').trim().append(' Framework').slug().toString()// 'laravel-framework'Fluent strings lists the methods.
Str.plural()#
Str.plural() returns the plural form of an English word:
const plural = Str.plural('car')// 'cars'const children = Str.plural('child')// 'children'A count as the second argument gives the singular for 1 and the plural for any other number:
const singular = Str.plural('child', 1)// 'child'Str.random()#
Str.random() returns a random string of letters and digits, 16 characters long unless you give a length. It uses
Node's cryptographically secure random bytes:
const random = Str.random(40)Str.singular()#
Str.singular() returns the singular form of an English word:
const singular = Str.singular('cars')// 'car'const child = Str.singular('children')// 'child'Str.slug()#
Str.slug() makes a URL-friendly slug of a string: lower-case letters and digits joined by the separator, - unless
you give another:
const slug = Str.slug('Laravel 5 Framework', '-')// 'laravel-5-framework'The string is transliterated to ASCII first, as Laravel's is for English, so accented and non-Latin letters keep a readable form:
Str.slug('Café crème brûlée') // 'cafe-creme-brulee'Str.slug('Привет, мир!') // 'privet-mir'Str.slug('über@home') // 'uber-at-home'@ is read as at, and underscores and dashes become the separator. What has no ASCII form is dropped, so
Str.slug('日本語') is '': give such a title a slug of your own.
The third argument is the language, 'en'; null skips the transliteration, and keeps every letter and digit. The
fourth is the dictionary of words to put in, { '@': 'at' } unless you give another:
Str.slug('Café crème', '-', null) // 'café-crème'Str.slug('5 & 6', '-', 'en', { '&': 'and' }) // '5-and-6'Str.snake()#
Str.snake() converts a string to snake case, with _ or the delimiter you give:
const converted = Str.snake('fooBar')// 'foo_bar'const dashed = Str.snake('fooBar', '-')// 'foo-bar'Str.start()#
Str.start() adds a value to the start of a string, unless it already starts with it:
const adjusted = Str.start('this/string', '/')// '/this/string'Str.startsWith()#
Str.startsWith() tells you whether a string starts with a value, or with any of a list of values:
const result = Str.startsWith('This is my name', 'This')// trueStr.studly()#
Str.studly() converts a string to studly case:
const converted = Str.studly('foo_bar')// 'FooBar'Str.title()#
Str.title() lower-cases a string, then upper-cases the first letter of each word:
const converted = Str.title('a nice title uses the correct case')// 'A Nice Title Uses The Correct Case'A word starts after a space, so 'hello-world' gives 'Hello-world'.
Str.ucfirst()#
Str.ucfirst() upper-cases the first character of a string:
const string = Str.ucfirst('foo bar')// 'Foo bar'Str.ulid()#
Str.ulid() returns a new ULID: 26 upper-case characters, the time first, so a later ULID sorts after an earlier
one. It takes a Date to use instead of now:
const ulid = Str.ulid()// '01M59V508YR4SZQ9PPYB9ZBX8P'Str.uuid()#
Str.uuid() returns a random version 4 UUID:
const uuid = Str.uuid()Str.uuid7()#
Str.uuid7() returns a version 7 UUID, which starts with the time, so a later one sorts after an earlier one. It
takes a Date to use instead of now:
const uuid = Str.uuid7()Str.ulid() and Str.uuid7() throw a TypeError for an invalid date, or one before 1970 or after the year 10889.
Fluent strings#
A Stringable is a string with the string methods as chainable methods. Str.of() makes one, and
request().string('name') returns one:
const slug = Str.of(' Hello World ').trim().slug().toString()// 'hello-world'Each method returns a new Stringable, except those in the second table, which return an answer. A Stringable
prints as its string in a template literal and in JSON; toString() or value() gives you the string itself.
| Method | Returns |
|---|---|
after(search), afterLast(search) | As Str.after() and Str.afterLast() |
append(...values) | The values added to the end |
ascii() | As Str.ascii() |
before(search), beforeLast(search) | As Str.before() and Str.beforeLast() |
between(from, to) | As Str.between() |
camel(), kebab(), snake(delimiter), studly() | As Str.camel(), Str.kebab(), Str.snake() and Str.studly() |
finish(cap), start(prefix) | As Str.finish() and Str.start() |
lcfirst(), ucfirst() | As Str.lcfirst() and Str.ucfirst() |
limit(limit, end) | As Str.limit() |
lower(), upper() | The string lower-cased or upper-cased |
ltrim(characters), rtrim(characters), trim(characters) | The string without whitespace at the start, the end or both ends, or without the characters you give |
plural(count), singular() | As Str.plural() and Str.singular() |
prepend(...values) | The values added to the start |
replace(search, replace) | Every occurrence of the value, or of each value of a list, replaced |
slug(separator) | As Str.slug() |
title() | As Str.title() |
| Method | Returns |
|---|---|
contains(needles), endsWith(needles), startsWith(needles) | As Str.contains(), Str.endsWith() and Str.startsWith() |
exactly(value) | Whether the string is exactly the value, a string or a Stringable |
explode(delimiter) | The parts between the delimiters, as a collection |
is(pattern) | As Str.is() |
isEmpty(), isNotEmpty() | Whether the string is '', or isn't |
length() | The number of characters, as Unicode code points |
toBoolean() | true for '1', 'true', 'on' and 'yes', in any case, else false |
toInteger(), toFloat() | The number the string starts with, or 0 |
toString(), value(), toJSON() | The string |
trim() with no characters removes the invisible characters Laravel's Str::trim() removes too, such as a
zero-width space, along with the whitespace.
URLs#
route()#
route() returns the URL of a named route, with its parameters:
const url = route('posts.show', post)const urlWithQuery = route('posts.show', { post, page: 2 })The URL is absolute unless you pass false as the third argument. Routing
covers the parameters and the types route:types adds.
url()#
url() returns an absolute URL for a path, with a query string from an object:
const profile = url('user/profile')// 'http://localhost:8000/user/profile'const withQuery = url('posts', { page: 2 })// 'http://localhost:8000/posts?page=2'Pass false as the third argument for the path alone. With no arguments, url() returns the URL generator:
const current = url().current()const previous = url().previous()const asset = url().asset('img/photo.jpg')current() is the current URL without its query string, and previous() the page the user came from. asset() is the
URL of a file in public, on app.asset_url when your config sets one.
Miscellaneous#
abort()#
abort() throws an HTTP error, which Oriel renders as the response with that status:
abort(403)abort(403, 'Unauthorized.', { 'X-Reason': 'banned' })Pass a Response to send it as it is. Error handling has the details.
abortIf()#
abortIf() throws the HTTP error when the condition is truthy:
abortIf(user.email_verified_at === null, 403)abortUnless()#
abortUnless() throws the HTTP error when the condition is falsy:
abortUnless(user.email_verified_at !== null, 403)app()#
app() returns the application, the service container. With a token or a class, it resolves that from the
container:
import { app } from '@orieljs/framework'import { Hasher } from '@orieljs/framework/contracts'const container = app()const hasher = app(Hasher)A contract's token comes from @orieljs/. Inside a request, app().make() resolves from the
request's scope.
auth()#
auth() returns the auth manager, and auth('web') a guard:
const signedIn = await auth().check()const user = await auth().user()back()#
back() returns a redirect to the previous page, or to the fallback when there is none:
const response = back()const withFallback = back('/dashboard')bcrypt()#
bcrypt() hashes a value with bcrypt, whatever your default hashing driver is. It returns a promise:
const password = await bcrypt('my-secret-password'){ rounds: 12 } as the second argument sets the cost.
cache()#
cache() with a key reads the value from the default cache store. With an object, it stores each key, for the
number of seconds you give, or forever:
const value = await cache('key')await cache({ key: 'value' }, 300)With no arguments, it returns the cache manager: await cache().remember('users', 60, () => User.all()). Every
call returns a promise.
collect()#
collect() makes a collection of an array, or of any iterable, as Collections describes:
const collection = collect(['Taylor', 'Abigail'])config()#
config() reads a configuration value, with dots, and returns the default when it isn't set:
const name = config('app.name')const timezone = config('app.timezone', 'UTC')An object sets values in the running application's configuration, and no arguments return the configuration repository:
config({ 'app.debug': true })A type argument types the value, as a cast: config<string>('app.name').
cookie()#
cookie() makes a cookie, for minutes minutes, which you queue or attach to a response:
const theme = cookie('theme', 'dark', 60)With no arguments, it returns the cookie jar: cookie().queue(cookie('theme', 'dark', 60)) sends the cookie with
the response.
csrfField()#
csrfField() returns the hidden _token input of a form, with the session's CSRF token:
const form = ( <form method="post" action="/profile"> {csrfField()} </form>)csrfToken()#
csrfToken() returns the session's CSRF token:
const token = csrfToken()decrypt()#
decrypt() decrypts a value made by encrypt(), and throws a DecryptError when it can't:
const value = decrypt<{ id: number }>(encrypted)dispatch()#
dispatch() sends a job to its queue, or runs it now when it isn't a queued job:
await dispatch(new ProcessPodcast(podcast))It returns the pending dispatch, so you can chain onQueue(), delay() and the other options. Queues
covers jobs.
dispatchSync()#
dispatchSync() runs a job now, in the current process, and returns a promise that settles when it is done:
await dispatchSync(new ProcessPodcast(podcast))encrypt()#
encrypt() encrypts a value with your APP_KEY. The value is turned into JSON first, unless you pass false as the
second argument:
const secret = encrypt({ id: 1 })env()#
env() reads an environment variable, or returns the default when it isn't set. The default may be a function:
const environment = env('APP_ENV', 'production')true, false, null and empty, in any case and with or without parentheses, become true, false, null and
''. A value in quotes loses them. Every other value stays a string: PORT=8000 gives '8000', not 8000. Read
env() in your config files, and config() everywhere else.
errors()#
errors() returns the validation errors the previous request flashed, as a message bag. It is empty when there are
none, or outside a request:
const message = errors().first('email')errors('login') reads a named bag. Validation shows it in a view.
event()#
event() dispatches an event to its listeners, and returns a promise of their results:
await event(order)fake()#
fake() returns the fake data generator that factories use, shared by your whole process:
const name = fake().name()const email = fake().unique().safeEmail()inertia()#
inertia() returns an Inertia response for a page, with its props:
const response = inertia('Posts/Index', { posts })With no arguments, it returns the Inertia response factory.
inject()#
inject() resolves a token or a class from the current container, the request's scope inside a request. It is how
a class takes a service in a field:
import { inject } from '@orieljs/framework'import { Hasher } from '@orieljs/framework/contracts'class PasswordService { private readonly hasher = inject(Hasher)}live()#
live() returns a Live component's first render, as HTML for a view, with its parameters:
const html = await live(Counter, { count: 3 })In a TSX view, the <Live component={Counter} / element does the same.
logger()#
logger() writes a debug message to the log, with its context:
logger('User dashboard visited.', { id: user.id })With no arguments, it returns the log manager: logger().error('Something went wrong.').
methodField()#
methodField() returns the hidden _method input that lets a form reach a PUT, PATCH or DELETE route:
const form = ( <form method="post" action="/posts/1"> {methodField('DELETE')} </form>)now()#
now() returns the current time as a Date:
const time = now()old()#
old() returns the input the previous request flashed to the session, or the default:
const title = old('title', '')Outside a request, it returns the default. Requests covers old input.
raw()#
raw() marks HTML as safe, so a TSX view prints it without escaping it:
const body = <article>{raw(html)}</article>Never pass it text a user wrote.
redirect()#
redirect() returns a redirect to a path, with a status, 302 unless you give one. With no arguments, it returns the
redirector:
const response = redirect('/home')const toRoute = redirect().route('dashboard')Responses covers redirects.
request()#
request() returns the current request. With a key, it returns that input, or the default:
const current = request()const name = request('name', 'Guest')Requests covers the request.
response()#
response() returns a Response with the content, a status and headers. With no arguments, it returns the response
factory:
const text = response('Hello World', 200, { 'Content-Type': 'text/plain' })const json = response().json({ name: 'Abigail' })retry()#
retry() calls a function until it succeeds, up to a number of times, and returns what it returns. When the last
attempt throws, it throws that error:
const result = await retry(5, (attempt) => callService(attempt), 100)The function receives the attempt's number, from 1. The third argument is the number of milliseconds to wait between attempts, or a function of the attempt's number that returns them:
const result = await retry(3, () => callService(), (attempt) => attempt * 100)The fourth argument decides which errors to retry: when it returns false for an error, retry() throws it at once:
const result = await retry(5, () => callService(), 100, (error) => error instanceof TypeError)session()#
session() reads a value from the session, or returns the default. An object stores its values, and no arguments
return the session:
const value = session('key', 'default')session({ chairs: 7, instruments: 3 })const all = session().all()HTTP session covers the session.
tap()#
tap() calls a function with a value, then returns the value:
const saved = tap(user, (user) => { user.name = 'Taylor'})If the function returns a promise, tap() doesn't wait for it.
value()#
value() returns the value it is given, or calls it when it is a function and returns the result:
const five = value(5)const six = value(() => 6)view()#
view() returns a view, with its data:
const profile = view('user.profile', { user })With no arguments, it returns the view factory. Views covers views.
vite()#
vite() returns the <script> and <link> tags of your Vite entry points, for a root view:
const head = <head>{vite(['resources/css/app.css', 'resources/js/app.tsx'])}</head>With no arguments, it returns the Vite instance.
Other utilities#
Pipeline#
A pipeline passes a value through a list of steps, each of which can inspect or change it and then call the next. It works as middleware does. Make one from the pipeline factory:
const result = await app(PipelineFactory) .make<User>() .send(user) .through([ async (user, next) => { // ... return next(user) }, async (user, next) => { // ... return next(user) }, ]) .then((user) => user)Each step receives the value and next, which runs the steps after it and returns a promise of their result. A step
that doesn't call next stops the pipeline: what it returns is the result. Once the last step calls next, the
function you give then() runs. thenReturn() returns the value as the last step passed it, and pipe() adds steps
after those of through().
A step may also be a class with a handle() method. The pipeline makes it from the container, so its fields can use
inject():
import type { PipeNext } from '@orieljs/framework/contracts'import type { User } from '../Models/User'export class GenerateProfilePhoto { async handle(user: User, next: PipeNext<User>) { // ... return next(user) }}const result = await app(PipelineFactory) .make<User>() .send(user) .through([GenerateProfilePhoto, ActivateSubscription, SendWelcomeEmail]) .thenReturn()via('process') calls another method on the classes. A class without the method fails the pipeline with a
PipelineError: "Pipe [GenerateProfilePhoto] has no [handle] method.". new BasePipeline<User>() makes a pipeline
without the factory.