n7olkachev/laravel-computed-properties
Add computed properties to your Laravel Eloquent models, calculated on the fly like accessors but excluded from the database. Keep models lean, reuse derived values across queries and serialization, and write less boilerplate.
Install the package and verify it is compatible with your Laravel/PHP version before wiring it into a model:
composer require n7olkachev/laravel-computed-properties
The package is small and has no meaningful configuration surface. The first place to look is the package README and src/ — specifically the HasComputedProperties trait, since that is where the getAttribute interception and per-instance caching live. Confirm the exact resolver convention for the version you install; this package predates Laravel 9’s native Attribute API.
Start with one model and one genuinely derived value. A good first use case is an order or invoice total built from already-loaded relations:
use N7olkachev\ComputedProperties\HasComputedProperties;
class Order extends Model
{
use HasComputedProperties;
protected $computed = ['subtotal', 'tax', 'total'];
public function getSubtotalAttribute()
{
return $this->items->sum('price');
}
public function getTaxAttribute()
{
return $this->subtotal * 0.2;
}
public function getTotalAttribute()
{
return $this->subtotal + $this->tax;
}
}
Then use it like a normal attribute:
$order = Order::with('items')->find($id);
$order->total; // computed once, reused within the instance
The value proposition is not “new Eloquent features”; it is per-instance memoization plus the ability for computed accessors to depend on other computed accessors without recomputing the whole chain every time.
Chain derived values instead of duplicating logic. Define small computed properties that reference each other:
public function getDiscountAttribute()
{
return $this->subtotal > 1000 ? $this->subtotal * 0.1 : 0;
}
public function getGrandTotalAttribute()
{
return $this->subtotal + $this->tax - $this->discount;
}
This is where the package earns its keep: in a Blade loop, collection sort, or API resource, grand_total can be read repeatedly without re-walking items and recalculating tax each time.
Eager load everything a computed property touches. Computed properties are ordinary PHP methods under the hood. If getTotalAttribute() reads $this->items, every model without items loaded will trigger a query. Use with('items') deliberately, and enable Model::preventLazyLoading() in local/testing environments to catch accidental N+1s.
Append computed values for JSON/API output. If the property should appear in toArray()/toJson(), add it to $appends:
protected $appends = ['total', 'grand_total'];
Use this selectively. Appending expensive computed properties to a large collection means every serialized model pays the computation cost, even if the client never reads the field.
Keep computed resolvers pure and read-only. They should derive from loaded attributes and relations, not write to the database, dispatch events, or perform unbounded queries. Pure resolvers are safe to cache, safe to call from resources, and predictable in tests.
Use them as a presentation/domain convenience layer. Good candidates: full_name, display_status, formatted_total, is_overdue, shipping_address_line. Poor candidates: anything you need in where(), orderBy(), or aggregates — those belong in SQL, scopes, or denormalized columns.
Test both value and query count. Assert the computed value, but also assert the relationship was eager loaded:
DB::enableQueryLog();
$orders = Order::with('items')->get();
$orders->each->total;
$this->assertCount(2, DB::getQueryLog()); // orders + items, not 1 + N
Computed properties are not queryable. You cannot use them in where, orderBy, pluck, or database aggregates. If a derived value becomes a filtering/sorting requirement, promote it to a real column, a generated column, or a query-level subselect.
Caching can go stale on a mutated instance. If the package memoizes on the model instance, reading $order->total, then changing items or tax_rate, may leave the old value in place. Set all inputs before reading, create a fresh model instance, or check whether the trait exposes an invalidation hook. Treat computed values as snapshots for the current instance state.
Watch for name collisions. A computed property that shadows a real database column, relation, or existing accessor will make debugging confusing. Avoid names like status or total if those are also columns; prefer explicit names like calculated_total or display_total.
$appends can silently make serialization expensive. Large API responses with appended computed properties can compute relations and chains for every model. Use API Resources to expose fields explicitly instead of appending everything globally.
Long-lived workers and Octane need extra scrutiny. This package was released in 2018, before modern Laravel lifecycle concerns. Verify that any internal cache is instance-scoped, not static. If you run Octane, test computed values after model rehydration and across requests.
Queues rehydrate models. Computed properties are not persisted. A queued job that receives a serialized model will recompute them after rehydration, and any relation-dependent computed property will need the relation loaded again inside the job.
Laravel 9+ compatibility is the main adoption risk. The package’s last release is from 2018. Modern Laravel changed accessor internals significantly (Attribute objects, getAttribute behavior, PHP 8 types). Composer constraints may block installation, or the trait may not see native Attribute-based accessors. Before adopting, test a throwaway model on your exact Laravel version. If it does not fit, the modern replacement is native accessors plus a small private memoization helper on the model.
Add docblocks for static analysis and IDE support. Computed attributes are invisible to PHP’s type system:
/**
* @property-read float $subtotal
* @property-read float $tax
* @property-read float $total
*/
class Order extends Model
{
}
Debugging tip: $model->getAttributes() will not show computed values because they are not stored attributes. Inspect the trait’s cache property if exposed, or use $model->toArray() when the property is in $appends. If a computed property returns null, check the spelling in $computed, the resolver method name, and whether the trait’s getAttribute interception is actually being reached before Eloquent’s normal attribute lookup.
How can I help you explore Laravel packages today?