Table Of Contents
- The Problem: Raw Database vs. Application Format
- Modern Laravel Attribute Patterns
- Advanced Accessor & Mutator Scenarios
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
Add Comment
No comments yet. Be the first to comment!