Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Computed Attributes Laravel Package

korridor/laravel-computed-attributes

A Laravel package for adding computed attributes to Eloquent models. Define derived values from existing model attributes and expose them cleanly alongside database columns, helping keep model logic organized, reusable, and easy to access.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a trait plus two artisan commands. There is no service to run, no queue, no external dependency — a first attribute is roughly a 20-minute task, and a production rollout on one model is a day.

1. Install, pinned to the right major. v3.x targets Laravel 10/11/12/13 (3.3.0 added Laravel 13). If you are still on Laravel 9, you need the 2.2.x line. Confirm the support matrix on Packagist rather than trusting changelogs, and pin with "korridor/laravel-computed-attributes": "^3.3" — this package's release cadence is one-to-one with Laravel majors, so it gates your upgrade calendar.

2. Add the column. It is a real column, so write a normal migration. Match the column type to what the compute method returns before you write the method:

Schema::table('orders', function (Blueprint $table) {
    $table->bigInteger('total_cents')->nullable()->after('status');
    $table->index('total_cents'); // the whole reason you're here
});

3. Declare the attribute on the model. The mapping is column name => method name on the $computedAttributes property; the trait recalculates and writes those columns on every save().

use Korridor\LaravelComputedAttributes\ComputedAttributes;

class Order extends Model
{
    use ComputedAttributes;

    protected $fillable = ['customer_id', 'status'];

    /** @var array<string, string> */
    protected $computedAttributes = [
        'total_cents' => 'computeTotalCents',
    ];

    protected $casts = ['total_cents' => 'integer'];

    public function computeTotalCents(): int
    {
        return (int) $this->items->sum(
            fn ($item) => $item->unit_price_cents * $item->quantity
        );
    }
}

4. Register the model in the published config (config/computed-attributes.php, models array). Both artisan commands iterate this list; a model that is missing from it silently never gets backfilled or validated. Publish with php artisan vendor:publish --tag=computed-attributes (check the README for the exact tag on your version).

5. Backfill, then verify.

php artisan computed-attributes:generate --attributes=total_cents
php artisan computed-attributes:validate --attributes=total_cents

First use case to pick: a denormalized search or sort key — users.full_name for LIKE search, or orders.total_cents for sorting. That is where the value is unambiguously worth a column. Do not start with a value that only appears in one Blade template; a plain accessor plus $appends is free and carries no consistency burden.

Where to look first in the source: the trait's save() path (to know exactly when recomputation fires) and the two command classes (to know the exact option names, chunking behavior, and exit codes on your pinned version). Secondary write-ups of this package do not reliably match the code.

Implementation Patterns

The four-step change is the unit of work. Column migration → $computedAttributes entry → computeX() method → config registration. Treat that as a checklist and put it in the PR template; the most common failure is a model that recomputes on save but was never added to the config, so generate/validate never touch it.

Keep computations pure and row-local. The formula should read the model's own columns or an already-loaded relation. No HTTP calls, no now()-dependent logic that changes between the write and the validate run, no dependence on the current user or locale — otherwise validate becomes a permanent false-alarm generator.

Define the command scopes for eager loading. Both commands honor scopes so backfills don't N+1 across millions of rows:

public function scopeComputedAttributesGenerate($query)
{
    return $query->with('items');
}

public function scopeComputedAttributesValidate($query)
{
    return $query->with('items');
}

This is the credible answer to "how do we backfill 2M rows without melting the DB," and it is the thing to cite when sizing the work.

Operationalize drift detection — this is the real day-to-day value. Schedule computed-attributes:validate (nightly or weekly) and run it in CI against a seeded database. Because generate skips the write when isDirty() is false (since 2.2.0), you can also schedule generate cheaply as a self-healing sweep. Add an alert on non-zero validate output; that turns "denormalized data silently drifts" into a monitorable metric instead of a theoretical objection.

Respect write-order for aggregates. The value is computed from in-memory state at save time. If an order's total_cents depends on items, saving an item does not update the order. Either recompute on the parent after children change ($order->save() / $order->touch() in the item's saved observer), or accept staleness and let the nightly generate sweep close the gap. Decide explicitly, per attribute, which one you have.

Query it like a column, because it is one. The payoff:

Order::whereBetween('total_cents', [$min, $max])
    ->orderByDesc('total_cents')
    ->paginate();

Index the column in the same migration that adds it, in the same PR that adds the attribute — not in a later "performance" ticket.

Test it in two layers. Unit-test computeTotalCents() directly with an in-memory model; feature-test that $order->save() produces the expected column value, and that the JSON response contains it (real columns serialize automatically). Add one test that validate passes on a freshly generated factory fixture — that catches cast/rounding mismatches before production does.

Denormalize deliberately, and document it. Add a comment in the migration (// Derived. Authoritative source: order_items. Recompute via computed-attributes:generate.) and note it in the model docblock. Once BI or an API client filters on the column, it is part of the persisted schema contract, and the next developer needs to know it is not authoritative.

Gotchas and Tips

  • Every non-Eloquent write bypasses recomputation. query()->update(), Model::insert(), upsert(), increment()/decrement(), raw SQL, ETL imports, database triggers, other services. The value drifts silently. Audit your codebase for these on any table that gains a computed attribute, and route them through Eloquent (or schedule generate).
  • Never chain computed attributes. Attribute B computing from attribute A depends on save ordering and on A already being current; both assumptions break under bulk operations and make validate uninterpretable. Compute both from the underlying source columns.
  • Float/decimal precision causes phantom dirty writes. isDirty() compares against the cast value; 0.1 + 0.2 recomputed as 0.30000000000000004 will look dirty every run and rewrite the row forever. Store integers (cents, basis points) or cast to decimal:2 and round explicitly inside the compute method.
  • Keep computed columns out of $fillable. A mass assignment from a request payload should never overwrite a derived value. Guard them, and remember that once they are columns they serialize into toArray()/JSON automatically — you do not need $appends, and adding it is harmless but misleading.
  • save() still fires model events. The generate command saves models, so observers run. The isDirty() guard means unchanged rows don't write and updated_at doesn't move, but a heavy updated/saved observer will still execute per row during a backfill. Keep observers cheap or scope the backfill down.
  • N+1 inside the compute method. If computeX() touches $this->items and the relation isn't loaded, every row during generate fires a query. The scope fixes it; forgetting the scope on validate is the more common miss because it runs less often and slower.
  • Column type must match the return type, in the same release. Shipping the column in one deploy and the attribute in the next leaves a window where the column is null and queries return wrong results. Ship both together, and make the column nullable if the table is large so the migration doesn't lock it.
  • Bulk-updating an existing table needs a plan. Chunk the backfill (either via the command's chunking options on your version or by narrowing the scope), run it off-peak, and verify with validate before you start filtering on the column in application code.
  • Upgrade risk is the real cost. Small project, effectively one maintainer, negligible adoption. Mitigate by keeping usage narrow — a trait, a property, a config entry, two commands — so fork-and-vendor stays a morning's work rather than a project. Read the actual source before committing an estimate; the package's own description and the behavior you observe have been known to disagree on exactly what gets persisted and when.
  • Rollback is cheap. Drop the column, remove the map entry, remove the config line. That is worth saying out loud in the design review — it is what makes this a low-stakes bet rather than an architectural commitment.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai