Eloquent: Mutators
Accessors, mutators, and attribute casts let you transform Eloquent model values automatically as they are read from or written to the database, without changing the calling code.
On this page
Introduction#
Accessors and mutators allow you to transform Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to lower-case an email address as it is set and format a phone number as it is read.
Accessors#
Accessors transform attribute values when you retrieve them. Define typed getter methods in your model:
class User extends Model {
// Raw attribute access
String? get rawName => getAttribute('name') as String?;
// Accessor with transformation
String get fullName {
final firstName = getAttribute('first_name') as String? ?? '';
final lastName = getAttribute('last_name') as String? ?? '';
return '$firstName $lastName'.trim();
}
// Accessor with formatting
String get formattedPhone {
final phone = getAttribute('phone') as String?;
if (phone == null) return '';
return '+1 (${phone.substring(0, 3)}) ${phone.substring(3, 6)}-${phone.substring(6)}';
}
}
Using Accessors#
final user = await User.find(1);
print(user?.fullName); // "John Doe"
print(user?.formattedPhone); // "+1 (555) 123-4567"
Mutators#
Mutators transform attribute values when you set them. Define setter methods:
class User extends Model {
// Raw setter
set name(String? value) => setAttribute('name', value);
// Mutator with transformation
set email(String? value) =>
setAttribute('email', value?.toLowerCase().trim());
// Mutator with hashing (example)
set password(String? value) =>
setAttribute('password', value != null ? hashPassword(value) : null);
}
Using Mutators#
final user = User();
user.email = ' [email protected] ';
print(user.getAttribute('email')); // "[email protected]"
user.password = 'secret123';
// Stored as hashed value
Attribute Casting#
The casts property provides automatic type conversion for attributes:
class Task extends Model {
@override
Map<String, String> get casts => const {
'is_completed': 'bool',
'priority': 'int',
'progress': 'double',
'due_date': 'datetime',
'settings': 'json',
};
// Typed accessors use the cast values
bool get isCompleted => getAttribute('is_completed') as bool? ?? false;
int get priority => getAttribute('priority') as int? ?? 0;
double get progress => getAttribute('progress') as double? ?? 0.0;
Carbon? get dueDate => getAttribute('due_date') as Carbon?;
Map<String, dynamic>? get settings =>
getAttribute('settings') as Map<String, dynamic>?;
}
Available Cast Types#
| Cast | Returns |
|---|---|
bool |
bool |
int |
int |
double |
double |
datetime |
Carbon |
json |
Map or List |
Custom Casts (CastsAttributes)#
For domain types like enums or value objects, implement CastsAttributes<T> and register the instance in casts:
enum MonitorStatus { active, paused, failed }
class Monitor extends Model {
@override
Map<String, dynamic> get casts => const {
'status': EnumCast(MonitorStatus.values), // enum round-trip
'statuses': ListCast(EnumCast(MonitorStatus.values)), // list of enums
'created_at': 'datetime', // built-ins still work
};
MonitorStatus? get status => getAttribute('status') as MonitorStatus?;
set status(MonitorStatus? value) => setAttribute('status', value);
}
Two casts ship with Magic:
EnumCast<T extends Enum>(values, {strict = false}): resolves a raw name to its enum value. Setstrict: trueto throwArgumentErroron unknown names instead of returningnull.ListCast<T>(inner): appliesinnerto every list element. Stored as a JSON-encoded string.
Roll your own by implementing CastsAttributes<T>:
class MoneyCast implements CastsAttributes<Money> {
const MoneyCast();
@override
Money? get(Model model, String key, Object? raw) =>
raw == null ? null : Money.fromCents(raw as int);
@override
Object? set(Model model, String key, Object? value) =>
value is Money ? value.cents : value;
}
Date Casting#
Date attributes are automatically converted to Carbon instances:
class Event extends Model {
@override
Map<String, String> get casts => const {
'starts_at': 'datetime',
'ends_at': 'datetime',
'published_at': 'datetime',
};
Carbon? get startsAt => getAttribute('starts_at') as Carbon?;
Carbon? get endsAt => getAttribute('ends_at') as Carbon?;
Carbon? get publishedAt => getAttribute('published_at') as Carbon?;
// Duration helper
int? get durationInMinutes {
if (startsAt == null || endsAt == null) return null;
return endsAt!.diffInMinutes(startsAt!);
}
// Check if event is happening now
bool get isHappeningNow {
final now = Carbon.now();
return startsAt?.isBefore(now) == true &&
endsAt?.isAfter(now) == true;
}
}
Working with Dates#
final event = await Event.find(1);
// Format dates
print(event?.startsAt?.format('MMMM d, yyyy')); // "January 15, 2024"
// Human readable
print(event?.startsAt?.diffForHumans()); // "in 2 days"
// Comparison
if (event?.startsAt?.isFuture() == true) {
print('Upcoming event');
}
JSON Casting#
JSON attributes are serialised/deserialised automatically:
class Monitor extends Model {
@override
Map<String, String> get casts => const {
'settings': 'json',
'tags': 'json',
};
// Access as Map
Map<String, dynamic> get settings =>
(getAttribute('settings') as Map<String, dynamic>?) ?? {};
// Access as List
List<String> get tags =>
(getAttribute('tags') as List?)?.cast<String>() ?? [];
// Setters
set settings(Map<String, dynamic> value) =>
setAttribute('settings', value);
set tags(List<String> value) =>
setAttribute('tags', value);
}
Using JSON Attributes#
final monitor = Monitor();
// Set complex data
monitor.settings = {
'timeout': 30,
'retries': 3,
'headers': {'Authorization': 'Bearer token'},
};
monitor.tags = ['production', 'critical', 'api'];
await monitor.save();
// Retrieve
print(monitor.settings['timeout']); // 30
print(monitor.tags.first); // "production"