Introduction
Accessors and mutators transform model attributes on read and write, using the modern Attribute class syntax introduced in Laravel 9 and still current in Laravel 13.
Key Concepts
Attribute::make: The factory that creates an accessor/mutator pair withgetandsetclosures.getclosure: Runs on attribute access, returning the transformed value.setclosure: Runs on attribute assignment, transforming the value before it hits the database.shouldCache: Caches the result of an accessor for the current request so expensive computations only run once.$appends: A model-level property listing accessors that should be included in array/JSON output.
Real World Context
Every app has derived values — full name from first/last, formatted price from cents, avatar URL from an S3 key. Accessors centralize that logic on the model instead of scattering it across views and controllers.
Deep Dive
Accessors and mutators allow you to transform Eloquent attribute values when retrieving or setting them on model instances. They're perfect for formatting data consistently.
Defining Accessors
Accessors transform attributes when you get them:
php<?php namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * Get the user's full name. * Combines first_name and last_name. */ protected function fullName(): Attribute { return Attribute::make( get: fn () => "{$this->first_name} {$this->last_name}", ); } /** * Always return email in lowercase. */ protected function email(): Attribute { return Attribute::make( get: fn (string $value) => strtolower($value), ); } /** * Format the created_at for display. */ protected function createdAtFormatted(): Attribute { return Attribute::make( get: fn () => $this->created_at->format('F j, Y'), ); } }
Using Accessors
php$user = User::find(1); // first_name: "John", last_name: "Doe" echo $user->full_name; // "John Doe" echo $user->email; // "john@example.com" (lowercase) echo $user->created_at_formatted; // "January 15, 2024"
Defining Mutators
Mutators transform attributes when you set them:
phpclass User extends Model { /** * Always store email in lowercase. */ protected function email(): Attribute { return Attribute::make( set: fn (string $value) => strtolower($value), ); } /** * Capitalize each word of the name when setting. * Note: for passwords, prefer the built-in 'hashed' cast (see the next lesson) * over a custom mutator — it's safer and idiomatic since Laravel 10. */ protected function name(): Attribute { return Attribute::make( set: fn (string $value) => ucwords(strtolower($value)), ); } /** * Set multiple attributes from one input. */ protected function fullName(): Attribute { return Attribute::make( set: function (string $value) { $parts = explode(' ', $value, 2); return [ 'first_name' => $parts[0], 'last_name' => $parts[1] ?? '', ]; }, ); } }
Using Mutators
php$user = new User; $user->email = 'JOHN@EXAMPLE.COM'; // Stored as: "john@example.com" $user->name = 'john DOE'; // Stored as: "John Doe" $user->full_name = 'John Doe'; // Sets first_name = "John", last_name = "Doe"
Combined Accessor and Mutator
phpprotected function phoneNumber(): Attribute { return Attribute::make( // Get: Format for display get: fn (string $value) => sprintf( '(%s) %s-%s', substr($value, 0, 3), substr($value, 3, 3), substr($value, 6) ), // Set: Store only digits set: fn (string $value) => preg_replace('/[^0-9]/', '', $value), ); }
php$user->phone_number = '(555) 123-4567'; // Stored as: "5551234567" echo $user->phone_number; // Displayed as: "(555) 123-4567"
Caching Accessors
For expensive computations:
phpprotected function avatarUrl(): Attribute { return Attribute::make( get: fn () => $this->calculateGravatarUrl(), )->shouldCache(); // Cache the result } // The calculation only runs once per request echo $user->avatar_url; // Calculates echo $user->avatar_url; // Uses cached value
Appending Accessors to JSON
phpclass User extends Model { /** * Accessors to append to model's array/JSON. */ protected $appends = ['full_name', 'avatar_url']; protected function fullName(): Attribute { return Attribute::make( get: fn () => "{$this->first_name} {$this->last_name}", ); } }
php$user->toArray(); // ['id' => 1, 'first_name' => 'John', ..., 'full_name' => 'John Doe'] $user->toJson(); // {"id": 1, "first_name": "John", ..., "full_name": "John Doe"}
Real-World Examples
phpclass Product extends Model { /** * Store price in cents, expose as dollars. */ protected function price(): Attribute { return Attribute::make( get: fn (int $value) => $value / 100, // Cents -> dollars set: fn (float $value) => $value * 100, // Dollars -> cents ); } /** * Format the dollar price as a currency string. * Note: $this->price already goes through the price() accessor above, * so it's in dollars by the time it reaches here — we do NOT divide again. */ protected function priceFormatted(): Attribute { return Attribute::make( get: fn () => '$' . number_format($this->price, 2), ); } } class Post extends Model { /** * Generate excerpt from body. */ protected function excerpt(): Attribute { return Attribute::make( get: fn () => Str::limit(strip_tags($this->body), 150), ); } /** * Calculate reading time. */ protected function readingTime(): Attribute { return Attribute::make( get: function () { $words = str_word_count(strip_tags($this->body)); $minutes = ceil($words / 200); // 200 words per minute return "{$minutes} min read"; }, ); } }
Common Pitfalls
- Mixing old and new accessor syntax — The legacy
getXxxAttribute/setXxxAttributemethods still work in Laravel 13 and are not deprecated, but theAttributeclass is the idiomatic modern style. Pick one style per model and stay consistent. - Forgetting
$appends— Derived attributes are missing fromtoArray()andtoJson()unless you list them. - Non-idempotent mutators — If
settransforms a value that's already been set, you get double-transformations.
Best Practices
- Use the
Attributeclass style —protected function name(): Attribute { return Attribute::make(...); }. - Cache expensive accessors with
shouldCache— For anything that does I/O or heavy computation. - Append only the values clients need —
$appendsbloats every JSON response; use it sparingly.
Summary
- Define accessors and mutators via
protected function name(): Attribute. Attribute::make(get: ..., set: ...)creates the pair.shouldCache()memoizes the result per request.$appendslists derived attributes fortoArray/toJsonoutput.- Use realistic transformations: formatting money, generating URLs, combining name parts.