Appended Attributes in Eloquent: Transforming API Responses Without Extra Database Columns

Transforming Eloquent Models with Appended Attributes

Transforming Eloquent Models with Appended Attributes


When building REST APIs or modern web applications in Laravel, we often need to return computed or derived values alongside our raw database records. Common examples include formatting a user's full name from first_name and last_name, calculating a discounted price, generating full URLs for uploaded assets, or calculating human-readable reading times for articles.

Creating extra columns in your database tables just to store derived values is bad database design. Instead, Laravel Eloquent provides a feature called Appended Attributes (using accessors and the $appends property) that allows you to attach computed data directly to your JSON and array responses on the fly.

In this tutorial, we will learn how Eloquent accessors work, how to append custom attributes to your models, how to append them dynamically on specific queries, and best practices to avoid performance bottlenecks.

What are Appended Attributes in Eloquent?

By default, when you convert an Eloquent model or collection to JSON or an array (such as returning a model from a controller), Laravel only serializes the actual database columns present in the model's $attributes array.

An appended attribute is a virtual, custom attribute created using an Eloquent accessor that gets automatically included in the model's serialized array or JSON output without having a matching column in the database table.

Step 1: Creating an Accessor

Before you can append an attribute, you must define an accessor for it on your Eloquent model.

In modern Laravel, accessors are defined using the Illuminate\Database\Eloquent\Casts\Attribute class. Let's take a practical example with a User model that has first_name and last_name columns in the database:

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [
        'first_name',
        'last_name',
        'email',
    ];

    /**
     * Determine the user's full name.
     */
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn (mixed $value, array $attributes) => trim("{$attributes['first_name']} {$attributes['last_name']}")
        );
    }
}


With this accessor defined, you can access $user->full_name in PHP code. However, if you return this model directly in an API response:

return response()->json($user);


The output will not include full_name yet because Laravel does not serialize custom accessors by default.

Step 2: Appending the Attribute Globally Using $appends

To tell Eloquent to always include your custom attribute whenever the model is converted to an array or JSON, add the attribute name (in snake_case) to the protected $appends array property on your model:

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [
        'first_name',
        'last_name',
        'email',
    ];

    /**
     * The accessors to append to the model's array and JSON form.
     *
     * @var array<int, string>
     */
    protected $appends = [
        'full_name',
    ];

    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn (mixed $value, array $attributes) => trim("{$attributes['first_name']} {$attributes['last_name']}")
        );
    }
}


Now, whenever the User model is serialized, full_name is automatically attached to the JSON output:

{
    "id": 1,
    "first_name": "Kishan",
    "last_name": "Kumar",
    "email": "kishan@example.com",
    "full_name": "Kishan Kumar",
    "created_at": "2026-08-25T07:45:00.000000Z",
    "updated_at": "2026-08-25T07:45:00.000000Z"
}


Real-World Practical Examples

1. Formatted Asset URLs

If you store file paths like avatars/user_1.png in the database, you can append the fully qualified URL so your frontend or mobile app doesn't have to construct CDN or storage links manually:

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;

class User extends Model
{
    protected $fillable = ['name', 'avatar_path'];

    protected $appends = ['avatar_url'];

    protected function avatarUrl(): Attribute
    {
        return Attribute::make(
            get: fn (mixed $value, array $attributes) => $attributes['avatar_path']
                ? Storage::disk('public')->url($attributes['avatar_path'])
                : asset('images/default-avatar.png')
        );
    }
}


2. Calculating Discounted Pricing on Products

Instead of recalculating discount logic inside Blade views or controllers, compute it once inside an accessor:

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = ['title', 'price', 'discount_percentage'];

    protected $appends = ['final_price', 'is_on_sale'];

    protected function finalPrice(): Attribute
    {
        return Attribute::make(
            get: function (mixed $value, array $attributes) {
                $discount = ($attributes['price'] * ($attributes['discount_percentage'] ?? 0)) / 100;
                return round($attributes['price'] - $discount, 2);
            }
        );
    }

    protected function isOnSale(): Attribute
    {
        return Attribute::make(
            get: fn (mixed $value, array $attributes) => ($attributes['discount_percentage'] ?? 0) > 0
        );
    }
}


Dynamic Appending at Runtime

Adding attributes to the global $appends array means they will run on every single query and serialization of that model across your entire application. If an attribute calculation is only needed on a specific API endpoint, you can append it dynamically at runtime using the append() method.

Appending on a Single Model

$product = Product::find(1);

// Append only for this response
return response()->json(
    $product->append(['formatted_tax', 'shipping_estimate'])
);


Appending on a Collection

If you have an Eloquent collection of records, use the collection's each() or append() helper:

$products = Product::where('is_active', true)->get();
$products->append(['final_price']);
return response()->json($products);


Appended Attributes vs. API Resources (JsonResource)

In Laravel, developers often ask: Should I use $appends or Laravel API Resources (JsonResource)?

Feature $appends (Model Level) API Resources (Transformation Layer)
Scope Global across the entire model whenever serialized. Isolated to specific API endpoints/routes.
Use Case Lightweight calculations needed everywhere (e.g. full_name, avatar_url). Complex API response structures, versioning, and hiding internal keys.
Coupling Coupled to the Eloquent Model. Decoupled presentation layer.

Note: Use $appends when the attribute represents an intrinsic, lightweight property of the model that you want available everywhere. For complex transformations or conditional endpoint formats, prefer dedicated API Resource classes.

Important Things to Remember and Pitfalls

  • The N+1 Query Trap: Never execute database queries or load unloaded relationships inside an accessor that is globally appended in $appends. For example, if your user_rating accessor queries $this->reviews()->avg('rating') and you fetch 100 users, Laravel will execute 100 extra queries, crushing your application performance.
  • Missing Columns in Partial Queries: If your accessor relies on $attributes['first_name'] and you run a targeted query like User::select('id', 'email')->get(), the accessor will throw an Undefined array key warning because first_name was omitted from the SELECT clause.
  • Naming Conventions: The method in your model should be camelCase (e.g., protected function isVerified(): Attribute), but the entry in $appends must match the desired snake_case output key (e.g., 'is_verified').

Conclusion:

Appended attributes in Laravel Eloquent provide an elegant way to enrich your API responses and serialized models with calculated data without cluttering your database schema with redundant columns. Keep your accessor calculations lightweight, be cautious of N+1 database queries, and leverage runtime appending when data is only required on specific endpoints.

Thank you for reading this article 😊

For any query, do not hesitate to comment 💬



Previous Post Next Post

Contact Form