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 to your Eloquent model:
use Korridor\ComputedAttributes\HasComputedAttributes;
class Product extends Model
{
use HasComputedAttributes;
protected $computedAttributes = [
'formatted_price' => function () {
return '$' . number_format($this->price, 2);
},
];
}
First Use Case
$product = Product::find(1);
$product->formatted_price; // Computes and caches (if enabled)
$product->save(); // Persists to DB
protected $computedAttributes = [
'discounted_price' => function () {
return $this->price * (1 - $this->discount / 100);
},
];
'status_badge' => function () {
return $this->is_active ? 'badge-success' : 'badge-danger';
},
'tax_amount' => function () {
return $this->price * $this->getTaxRate();
},
'volatile_data' => [
'computation' => fn() => $this->fetchExternalData(),
'cache' => false, // Disable caching
],
Trigger recomputation on updates:
protected static function booted()
{
static::updating(function ($model) {
$model->recomputeComputedAttributes();
});
}
Auto-include in JSON:
protected $appends = ['formatted_price'];
Allow computed attributes in fillable:
protected $fillable = ['*']; // Or explicitly list
Use virtual columns (MySQL 5.7+) alongside PHP computations:
// Migration
$table->virtualAs('CONCAT(name, " (", category, ")")')->storedAs('full_name');
Use traits for reusable logic:
trait ComputesShippingCost
{
public function getShippingCostAttribute()
{
return $this->weight * 0.50;
}
}
Cache API responses:
'weather' => [
'computation' => fn() => Cache::remember("weather_{$this->id}", 3600, fn() =>
Http::get("api.weather/{$this->location}")->json()
),
'cache' => true,
],
Recompute on restore:
protected static function booted()
{
static::restored(function ($model) {
$model->recomputeComputedAttributes();
});
}
$model->afterSave(function ($model) {
ComputeHeavyAttribute::dispatch($model, 'complex_value');
});
a depends on b, which depends on a.cache: false or restructure logic.text) or sanitize values:
'truncated_name' => fn() => Str::limit($this->full_name, 50),
$appends:
protected $appends = ['formatted_price'];
'debug_value' => [
'computation' => fn() => Log::debug('Computing debug_value'),
'cache' => false,
],
$model->isComputedAttributeCached('full_name'); // true/false
$model->recomputeComputedAttribute('formatted_price');
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 Product extends Model
{
use HasComputedAttributes, StoresComputedAttributesInJson;
}
ComputedAttributes::extend('custom', function ($model, $attribute) {
return strtoupper($model->{$attribute});
});
Usage:
'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);
}
}
Mock computations:
$model->shouldReceive('computeFormattedPrice')->once()->andReturn('$10.00');
Test persistence:
$model->formatted_price = '$5.00';
$model->save();
$model->refresh();
$this->assertEquals('$5.00', $model->formatted_price);
null values.Check computed attribute consistency:
php artisan computed-attributes:validate --model=Product
Optimize queries with ->with():
php artisan computed-attributes:validate --model=Product --scope="with(relationship)"
Combine with Laravel 13’s macroable traits:
Product::macro('recomputeAll', function () {
return $this->each->recomputeComputedAttributes();
});
Use Laravel 13’s improved observer syntax:
Product::observe(ProductObserver::class);
Leverage PHP 8.2+ return types:
public function getFormattedPriceAttribute(): string
{
return '$' . number_format($this->price, 2);
}
it('persists computed attributes', function () {
$product = Product::factory()->create(['price' => 100]);
$product->save();
$this->assertDatabaseHas('products', [
'id' => $product->id,
'
How can I help you explore Laravel packages today?