Models
Eloquent: mutators and casting
Accessors, mutators and casts change attribute values as your models read and write them: a JSON column comes back as
an object, a date as a Date, and a name in the case you chose.
Introduction#
A cast turns a column's stored value into the type your code works with, and back when the model is saved. An accessor is a method that computes an attribute when you read it, and a mutator one that sets it when you write it.
Accessors and mutators#
Defining an accessor#
An accessor is a method named get, the attribute's name in StudlyCase, and Attribute. It receives the stored value,
and returns what reading the attribute gives:
import { Model } from '@orieljs/framework'export class User extends Model { /** Get the user's first name. */ getFirstNameAttribute(value: unknown): string { const name = String(value ?? '') return name.charAt(0).toUpperCase() + name.slice(1) }}user.getAttribute('first_name') then calls it, and so does reading user.first_name. model:types keeps the column's
type for it; an accessor with no column, below, is typed from its return type.
An accessor can also make an attribute that has no column, computed from others:
export class User extends Model { getFullNameAttribute(): string { return `${String(this.getAttribute('first_name'))} ${String(this.getAttribute('last_name'))}` }}To have it in the model's toArray() and JSON, add it to appends (see
Appending values to JSON).
Defining a mutator#
A mutator is a method named set, the attribute's name in StudlyCase, and Attribute. It receives the value being
set, and stores what it decides in this.attributes:
import { Model } from '@orieljs/framework'export class User extends Model { /** Interact with the user's first name. */ setFirstNameAttribute(value: string): void { this.attributes.first_name = value.toLowerCase() }}Every write goes through it: user.first_name = 'Sally', fill(), create() and update().
Attribute casting#
casts maps attributes to the type they are read as:
import { Model, type CastDefinition } from '@orieljs/framework'export class User extends Model { protected override casts: Readonly<Record<string, CastDefinition>> = { is_admin: 'boolean', options: 'json', email_verified_at: 'datetime', }}A model with model:types types each attribute from its cast: is_admin as a boolean, email_verified_at as a
Date | null.
| Cast | Read as | Stored as |
|---|---|---|
int, integer | A number | The number |
float, double, real | A number | The number |
string | A string | The string |
bool, boolean | true or false | 1 or 0, or a boolean on PostgreSQL |
array, json, object | The parsed JSON | JSON text |
collection | A collection of the parsed JSON list | JSON text |
date | A Date at midnight UTC | '2026-03-04' |
datetime | A Date | '2026-01-02 03:04:05', in UTC |
immutable_date, immutable_datetime | A Date, as date and datetime | The same |
timestamp | A Unix time in seconds | The date and time |
decimal:2 | A string with that many decimals, '9.50' | The number |
hashed | The hash | A hash of the value, made when the model is saved |
A built-in cast of null stays null; a custom cast's get() receives it. The incrementing key is always cast as an
integer, and created_at and updated_at (and deleted_at with soft deletes) as datetimes.
A cast string Oriel doesn't know, such as Laravel's datetime:Y-m-d with a format, throws an InvalidCastError when
the attribute is read or written: "Call to undefined cast [datetime:Y-m-d] on column [launched_at] in model
[Product].".
Array and JSON casting#
A json attribute is read as the parsed value. Changing it in place counts as a change, so save() writes it:
user.options.theme = 'dark'await user.save()Date casting#
A date is stored in UTC, as '2026-01-02 03:04:05', and read as a Date. In toArray() and JSON, dates are written
as Laravel writes them: '2026-01-02T03:04:05.000000Z'.
Enum casting#
A TypeScript enum casts an attribute to its values:
import { Model, type CastDefinition } from '@orieljs/framework'export enum ServerStatus { Provisioned = 'provisioned', Ready = 'ready',}export class Server extends Model { protected override casts: Readonly<Record<string, CastDefinition>> = { status: ServerStatus }}Setting a value that isn't one of the enum's throws: ""nope" is not a valid backing value for the cast of [status] on [Server].".
Hashed casting#
The hashed cast hashes a value when the model is saved, with your hashing driver, so you can set a
plain password:
export class User extends Model { protected override casts: Readonly<Record<string, CastDefinition>> = { password: 'hashed' }}Until save(), reading the attribute gives what you set. A value that is already a hash made with your current
settings is kept as it is; one made with other settings is refused.
Query time casting#
withCasts() adds casts for one query, such as for a column a raw select adds:
const users = await User.query() .select('users.*') .selectRaw('(select max(created_at) from posts where posts.author_id = users.id) as last_posted_at') .withCasts({ last_posted_at: 'datetime' }) .get()Custom casts#
A custom cast is a class with a get() method, which turns the stored value into what the model gives, and a set()
method, which turns a value back into what is stored. make:cast writes one in app/:
node oriel make:cast Moneyimport type { CastsAttributes } from '@orieljs/framework'/** A price kept in cents, read in units: 1234 is 12.34. */export class Money implements CastsAttributes<number, number> { get(_model: object, _key: string, value: unknown): number { return Number(value) / 100 } set(_model: object, _key: string, value: number): unknown { return Math.round(value * 100) }}Name the class in casts, or give an instance:
import { Model, type CastDefinition } from '@orieljs/framework'import { Money } from '../Casts/Money'export class Product extends Model { protected override casts: Readonly<Record<string, CastDefinition>> = { price: Money }}Both methods also receive the model, the attribute's name and all its stored attributes.
Serialization#
Hiding attributes from JSON#
hidden lists the attributes left out of the model's toArray() and JSON, such as a password, and visible lists
the only ones left in:
export class User extends Model { protected override hidden = ['password', 'remember_token']}makeVisible() and makeHidden() change the lists for one model: user.makeVisible('password').toArray().
Appending values to JSON#
An accessor with no column isn't in toArray() and JSON unless you add it to appends:
export class User extends Model { protected override appends = ['is_admin'] getIsAdminAttribute(): boolean { return this.getAttribute('role') === 'admin' }}append() adds one for one model: user.append('is_admin').toArray().