codebykyle/calculated-field
A Laravel package for defining calculated fields on Eloquent models. Declare attributes that derive their values from other model properties, and the package keeps them updated automatically when the model is saved—no manual recalculation or custom observers required.
This package adds a Nova field whose value is computed on the server from the underlying model rather than read from a database column. It is a thin base class: you subclass it, implement calculate(), and drop it into a resource's fields().
1. Install (mind the version constraint). The package has been archived since 2019 and targets Nova 2. If you're on a newer Nova, you'll need to fork or accept that it likely won't resolve:
composer require codebykyle/calculated-field
2. Look here first. Open src/CalculatedField.php in the vendor directory. The whole package is one or two files — the base field overrides Nova's resolve() to call your calculate() method instead of reading $model->{$attribute}. Understanding that one override is the entire mental model.
3. Create a field class. The contract is:
namespace App\Nova\Fields;
use Codebykyle\CalculatedField\CalculatedField;
use Laravel\Nova\Http\Requests\NovaRequest;
class FullName extends CalculatedField
{
public function calculate(NovaRequest $request, $model)
{
return trim("{$model->first_name} {$model->last_name}");
}
}
4. Register it. The second argument to make() is only a label anchor — there is no real column, so pass anything meaningful for the UI:
use App\Nova\Fields\FullName;
public function fields(NovaRequest $request)
{
return [
ID::make()->sortable(),
Text::make('First name'),
Text::make('Last name'),
FullName::make('Full Name')->onlyOnIndex()->onlyOnDetail(),
];
}
First use case: a full-name column derived from first_name + last_name. It's the lowest-risk way to validate the install end-to-end — no relations, no formatting, no auth — before you start using it for aggregates or status badges.
Display-only derivation (the common case). Concatenation, formatting, arithmetic, and status derivation all belong here. Prefer returning primitives and letting Nova's display layer format them:
class OrderTotal extends CalculatedField
{
public function calculate(NovaRequest $request, $model)
{
return number_format($model->lineItems->sum('total'), 2);
}
}
Always constrain placement. Because there is no backing column, a CalculatedField on a create/update form is a lie — Nova has nothing to fill and nothing to persist. Use ->onlyOnIndex() and/or ->onlyOnDetail() consistently. Treat writing to it as a bug, not a feature.
Badges and conditional HTML. Return an HTML string and opt into rendering, which is often cleaner than composing a Badge field:
public function calculate(NovaRequest $request, $model)
{
return $model->isOverdue()
? '<span class="text-red-500 font-bold">Overdue</span>'
: '<span class="text-green-500">On time</span>';
}
OverdueStatus::make('Status')->asHtml()->onlyOnIndex(),
Reusable fields with parameters. Push variation into constructor args or fluent setters so one class serves many resources:
class RelationSum extends CalculatedField
{
protected $relation;
protected $column;
public function relation(string $relation, string $column = 'amount'): static
{
$this->relation = $relation;
$this->column = $column;
return $this;
}
public function calculate(NovaRequest $request, $model)
{
return $model->{$this->relation}->sum($this->column);
}
}
Then RelationSum::make('Revenue')->relation('orders', 'total').
Request-aware visibility. Use the injected NovaRequest to gate values by role rather than hiding the whole field:
public function calculate(NovaRequest $request, $model)
{
return $request->user()->isAdmin() ? $model->margin() : '—';
}
Testing. You don't need an HTTP round-trip. Instantiate the field, build a model (unsaved is usually fine), and call calculate() with a mocked NovaRequest. Anything you can't test that way is a sign the logic belongs on the model, not in the field.
Integration tip: keep the calculation itself on the model ($model->margin(), $model->fullName()) and let the field be a two-line adapter. Nova's own CSV exports and API responses can then reuse the same logic, and you get testability for free.
resolve/resolveUsing ordering, request types). Before adopting, copy the base class into app/Nova/Fields/ and maintain it yourself — it's a handful of lines and removes a dead dependency.->sortable() and ->filterable() will attempt to reference a nonexistent column and fail at the SQL layer. If users need to sort by the computed value, store it (accessor + generated column) and use a normal Text/Number field.calculate() runs once per row on index views. Any $model->relation access lazy-loads a query per row. Either eager-load in the resource's indexQuery(), or do a single aggregate query inside calculate() using $model->relation()->sum(...) — still one query per row, but cheaper than hydrating models.resolveUsing() is shadowed. The base class takes over resolve(), so chaining ->resolveUsing(...) on the field may be ignored or double-applied depending on Nova version. Put all logic in calculate().Number, Currency, and DateTime fields apply presentation; a CalculatedField returns whatever you hand it. Carbon instances often render as 2024-01-01T00:00:00.000000Z unless you format. Return strings for anything non-trivial.calculate() surfaces as a blank cell with little signal. Temporarily dd() or logger() the return value; on index pages, check storage/logs/laravel.log rather than the browser.onlyOnIndex/onlyOnDetail.$model->setAttribute('_cf_total', $v)) or guard with a static/instance cache so repeated resolution of the same field within a request doesn't recompute.Text::make('Full Name', fn () => $this->first_name.' '.$this->last_name)) or ->resolveUsing() on a plain Text field gives the same behavior with a supported dependency. Reserve the package for legacy Nova 2 codebases where rewriting isn't warranted.How can I help you explore Laravel packages today?