korridor/laravel-computed-attributes
Adds “computed attributes” to Laravel models, letting you define dynamic/derived properties that behave like normal attributes (including access/casting/serialization) without storing them in the database. Useful for clean model APIs and reusable calculations.
## Getting Started
### **First Steps**
1. **Installation**
```bash
composer require korridor/laravel-computed-attributes
php artisan vendor:publish --provider="Korridor\ComputedAttributes\ComputedAttributesServiceProvider" --tag="config"
config/computed-attributes.php exists (default settings work for most cases).Define a Computed Attribute Add the trait to your Eloquent model and declare computed attributes:
use Korridor\ComputedAttributes\HasComputedAttributes;
class Order extends Model
{
use HasComputedAttributes;
protected $computedAttributes = [
'total_amount' => function () {
return $this->items->sum('price');
},
];
}
total_amount to the database when the model is saved.First Test
$order = Order::find(1);
$order->total_amount; // Computes and caches (if enabled)
$order->save(); // Persists to DB
protected $computedAttributes = [
'formatted_date' => function () {
return Carbon::parse($this->created_at)->format('Y-m-d');
},
];
'status_badge' => function () {
return $this->is_active ? 'success' : 'danger';
},
'tax_included_price' => function () {
return $this->price * (1 + $this->tax_rate / 100);
},
'weather' => function () {
return Cache::remember("weather_{$this->id}", 3600, fn() =>
Http::get("https://api.weather.com/{$this->location}")->json()
);
},
'timestamp' => [
'computation' => fn() => now()->timestamp,
'cache' => false,
],
'last_updated' => [
'computation' => fn() => now(),
'cache_ttl' => 60, // 1 minute
],
Order::where('updated_at', '<', now()->subDays(1))->each(function ($order) {
$order->recomputeComputedAttributes();
$order->save();
});
protected $appends = ['full_name', 'total_amount'];
class OrderObserver
{
public function saved(Order $order)
{
if ($order->isDirty('items')) {
$order->recomputeComputedAttribute('total_amount');
}
}
}
Schema::table('orders', function (Blueprint $table) {
$table->decimal('total_amount', 10, 2);
});
$table->virtualAs('total_amount', 'SUM(price)')->stored;
Schema::table('orders', function (Blueprint $table) {
$table->index('total_amount');
});
public function test_computed_attribute()
{
$order = new Order(['items' => [['price' => 100]]]);
$this->assertEquals(100, $order->total_amount);
}
public function test_persistence()
{
$order = Order::factory()->create(['items' => [['price' => 50]]]);
$order->refresh();
$this->assertEquals(50, $order->total_amount);
}
'discount' => function () {
return $this->discount_percentage ?? 0;
},
'a' => fn() => $this->b,
'b' => fn() => $this->a,
cache: false or restructure logic.save().$model->afterSave(function ($model) {
ComputeHeavyAttribute::dispatch($model, 'complex_value');
});
'volatile' => ['computation' => fn() => heavyLogic(), 'cache' => false],
text instead of string) or sanitize:
'truncated_name' => fn() => Str::limit($this->full_name, 50),
$appends:
protected $appends = ['full_name'];
protected $computedAttributes = [
'debug_value' => [
'computation' => fn() => Log::debug('Computing debug_value'),
'cache' => false,
],
];
$model->isComputedAttributeCached('full_name'); // true/false
$model->recomputeComputedAttribute('full_name');
php artisan computed-attributes:validate User --attribute=full_name
Set in config/computed-attributes.php:
'default_cache_ttl' => 3600, // 1 hour
'skip_on_create' => [
'computation' => fn() => 'value',
'skip_on_create' => true,
],
Override storage (e.g., JSON column):
use Korridor\ComputedAttributes\StoresComputedAttributesInJson;
class User extends Model
{
use HasComputedAttributes, StoresComputedAttributesInJson;
}
ComputedAttributes::extend('custom', function ($model, $attribute) {
return strtoupper($model->{$attribute});
});
Usage:
protected $computedAttributes = [
'uppercase_name' => ['type' => 'custom'],
];
ComputedAttributes::computing(function ($model, $attribute) {
Log::info("Computing {$attribute} for {$model->id}");
});
Extend for PostgreSQL/SQLite:
// app/Providers/ComputedAttributesServiceProvider.php
public function register()
{
if (config('database.default') === 'pgsql') {
$this->app->bind('computed-attributes.driver', \App\Services\PostgresDriver::class);
}
}
2.2.* for older versions.strict_types=1.$model->isDirty() before saving (since `2.How can I help you explore Laravel packages today?