Navigation

Laravel

How to use Accessors & Mutators in modern Laravel

Transform Laravel model attributes in 2025 using modern Accessors and Mutators with Attribute casting. Format names, encrypt data, and create computed properties efficiently.

Table Of Contents

The Problem: Raw Database vs. Application Format

Database stores raw data, but your app needs formatted output. Old Laravel used getNameAttribute() syntax, but modern Laravel uses the cleaner Attribute class:

// OLD WAY (still works but deprecated)
class User extends Model
{
    public function getFullNameAttribute()
    {
        return $this->first_name . ' ' . $this->last_name;
    }
    
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = Hash::make($value);
    }
}

// MODERN WAY (Laravel 9+)
use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->first_name . ' ' . $this->last_name,
        );
    }
    
    protected function password(): Attribute
    {
        return Attribute::make(
            set: fn ($value) => Hash::make($value),
        );
    }
}

// Usage remains the same
$user = User::find(1);
echo $user->full_name; // "John Doe"
$user->password = 'secret123'; // Automatically hashed

Modern Laravel Attribute Patterns

Different scenarios for data transformation and computed properties:

class Product extends Model
{
    // Format currency display
    protected function price(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => '$' . number_format($value / 100, 2),
            set: fn ($value) => str_replace(['$', ','], '', $value) * 100,
        );
    }
    
    // Boolean flag based on stock
    protected function isInStock(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->stock_quantity > 0,
        );
    }
    
    // Complex computed property
    protected function discountedPrice(): Attribute
    {
        return Attribute::make(
            get: function () {
                if ($this->discount_percentage > 0) {
                    $discount = ($this->attributes['price'] * $this->discount_percentage) / 100;
                    return $this->attributes['price'] - $discount;
                }
                return $this->attributes['price'];
            }
        );
    }
    
    // URL slug generation
    protected function slug(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => $value ?: Str::slug($this->name),
            set: fn ($value) => Str::slug($value),
        );
    }
    
    // JSON data handling
    protected function metadata(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => json_decode($value, true) ?? [],
            set: fn ($value) => json_encode($value),
        );
    }
}

// Usage examples
$product = new Product([
    'name' => 'Gaming Laptop',
    'price' => '$1,299.99', // Set as string
    'stock_quantity' => 5,
    'discount_percentage' => 10,
    'metadata' => ['brand' => 'Dell', 'model' => 'G15']
]);

echo $product->price; // "$1,299.99"
echo $product->is_in_stock; // true
echo $product->discounted_price; // "$1,169.99"
echo $product->slug; // "gaming-laptop"
print_r($product->metadata); // Array with brand and model

Advanced Accessor & Mutator Scenarios

Handle complex business logic and data transformations:

class User extends Model
{
    // Secure email display
    protected function maskedEmail(): Attribute
    {
        return Attribute::make(
            get: function () {
                $email = $this->email;
                $parts = explode('@', $email);
                $username = $parts[0];
                $domain = $parts[1];
                
                if (strlen($username) <= 2) {
                    return str_repeat('*', strlen($username)) . '@' . $domain;
                }
                
                return substr($username, 0, 2) . str_repeat('*', strlen($username) - 2) . '@' . $domain;
            }
        );
    }
    
    // Age calculation from birthday
    protected function age(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->birth_date ? now()->diffInYears($this->birth_date) : null,
        );
    }
    
    // Timezone-aware date formatting
    protected function localCreatedAt(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->created_at
                ->setTimezone($this->timezone ?? config('app.timezone'))
                ->format('M j, Y g:i A T'),
        );
    }
    
    // Phone number formatting
    protected function phoneNumber(): Attribute
    {
        return Attribute::make(
            get: function ($value) {
                if (!$value) return null;
                
                // Format US phone numbers
                $cleaned = preg_replace('/[^0-9]/', '', $value);
                if (strlen($cleaned) === 10) {
                    return '(' . substr($cleaned, 0, 3) . ') ' . 
                           substr($cleaned, 3, 3) . '-' . 
                           substr($cleaned, 6);
                }
                return $value;
            },
            set: fn ($value) => preg_replace('/[^0-9]/', '', $value),
        );
    }
    
    // Encrypted sensitive data
    protected function socialSecurityNumber(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => $value ? decrypt($value) : null,
            set: fn ($value) => $value ? encrypt($value) : null,
        );
    }
}

// E-commerce specific transformations
class Order extends Model
{
    // Order status with human-readable labels
    protected function statusLabel(): Attribute
    {
        return Attribute::make(
            get: fn () => match($this->status) {
                'pending' => 'Awaiting Payment',
                'processing' => 'Being Prepared',
                'shipped' => 'On Its Way',
                'delivered' => 'Delivered',
                'cancelled' => 'Cancelled',
                default => 'Unknown Status'
            }
        );
    }
    
    // Shipping estimate based on location
    protected function estimatedDelivery(): Attribute
    {
        return Attribute::make(
            get: function () {
                if ($this->status !== 'shipped') return null;
                
                $baseDate = $this->shipped_at ?? now();
                $daysToAdd = match($this->shipping_method) {
                    'express' => 1,
                    'standard' => 3,
                    'economy' => 7,
                    default => 5
                };
                
                return $baseDate->addDays($daysToAdd)->format('M j, Y');
            }
        );
    }
    
    // Tax calculation based on shipping address
    protected function calculatedTax(): Attribute
    {
        return Attribute::make(
            get: function () {
                if (!$this->shippingAddress) return 0;
                
                $taxRate = match($this->shippingAddress->state) {
                    'CA' => 0.0875,
                    'NY' => 0.08,
                    'TX' => 0.0625,
                    default => 0.05
                };
                
                return round($this->subtotal * $taxRate, 2);
            }
        );
    }
}

// Content management example
class Post extends Model
{
    // Reading time estimation
    protected function readingTime(): Attribute
    {
        return Attribute::make(
            get: function () {
                $wordCount = str_word_count(strip_tags($this->content));
                $minutes = ceil($wordCount / 200); // Average reading speed
                return $minutes . ' min read';
            }
        );
    }
    
    // SEO-friendly excerpt
    protected function excerpt(): Attribute
    {
        return Attribute::make(
            get: function ($value) {
                if ($value) return $value;
                
                // Auto-generate from content
                $content = strip_tags($this->content);
                return Str::limit($content, 160) . '...';
            },
            set: fn ($value) => Str::limit($value, 200),
        );
    }
    
    // Social media optimized title
    protected function socialTitle(): Attribute
    {
        return Attribute::make(
            get: fn () => Str::limit($this->title, 60) . ' | ' . config('app.name'),
        );
    }
}

// Performance consideration: Caching expensive computations
class User extends Model
{
    protected function reputationScore(): Attribute
    {
        return Attribute::make(
            get: function () {
                // Cache expensive calculation for 1 hour
                return Cache::remember(
                    "user_reputation_{$this->id}",
                    3600,
                    function () {
                        return $this->posts()->sum('votes') + 
                               $this->comments()->sum('likes') * 0.5;
                    }
                );
            }
        );
    }
}

// Usage in Blade templates
<!-- User profile -->
<div class="user-card">
    <h3>{{ $user->full_name }}</h3>
    <p>Email: {{ $user->masked_email }}</p>
    <p>Age: {{ $user->age }} years old</p>
    <p>Phone: {{ $user->phone_number }}</p>
    <p>Member since: {{ $user->local_created_at }}</p>
</div>

<!-- Product listing -->
<div class="product">
    <h4>{{ $product->name }}</h4>
    <p class="price">{{ $product->price }}</p>
    @if($product->discount_percentage > 0)
        <p class="sale-price">Sale: {{ $product->discounted_price }}</p>
    @endif
    <p class="stock">{{ $product->is_in_stock ? 'In Stock' : 'Out of Stock' }}</p>
</div>

<!-- Order details -->
<div class="order-status">
    <h3>Order #{{ $order->id }}</h3>
    <p>Status: {{ $order->status_label }}</p>
    @if($order->estimated_delivery)
        <p>Estimated delivery: {{ $order->estimated_delivery }}</p>
    @endif
    <p>Tax: ${{ number_format($order->calculated_tax, 2) }}</p>
</div>

Modern Laravel Accessors and Mutators use the Attribute class with get and set closures. They automatically handle caching and provide better IDE support than the old getAttributeAttribute() syntax. Use them for data formatting, computed properties, and secure data handling, but be mindful of performance implications for expensive operations.

Related: Laravel Collections: Beyond Basic Array Operations | Laravel Events and Listeners: Building Decoupled Applications | Building Multi-tenant Applications with Laravel: A Comprehensive Guide

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel