DEV Community

Eduardo Lázaro
Eduardo Lázaro

Posted on

Model field keepers in Laravel with Larakeep

Most Laravel models have a few fields nobody types in. A total summed from line items, a slug, a search_text blob, a cached count. Something has to compute them, and that something usually ends up wedged into an observer or spread across the model until both are hard to read.

Larakeep gives that computation a home. A Keeper is a small class that knows how to produce one or more of a model's fields, and the model runs it with a single call. This is the whole package, feature by feature.

How to install

One Composer package, and the service provider auto-registers.

composer require edulazaro/larakeep
Enter fullscreen mode Exit fullscreen mode

Nothing to publish. A keeper is a plain class, and the wiring is a trait plus an attribute.

What a keeper is

A keeper holds the formulas for a model's derived fields, one method per field, and nothing else. It sits deliberately close to an action but aims at a different job: an action performs an operation, a keeper fills a field. Where an action might send an email, a keeper computes the total that gets stored on the invoice.

Writing a keeper

Generate one with the artisan command. The keeper takes the model in its constructor, and by convention it is created for the model whose name it carries, so InvoiceKeeper targets Invoice.

php artisan make:keeper InvoiceKeeper
Enter fullscreen mode Exit fullscreen mode

Each field maps to a method named get followed by the PascalCase of the column, so a total column is filled by getTotal() and amount_due by getAmountDue().

namespace App\Keepers;

use App\Models\Invoice;

class InvoiceKeeper
{
    public function __construct(private Invoice $invoice) {}

    public function getTotal(): int
    {
        return $this->invoice->items->sum('amount');
    }

    public function getAmountDue(): int
    {
        return $this->getTotal() - $this->invoice->paid;
    }
}
Enter fullscreen mode Exit fullscreen mode

A method just returns the value. It does not assign it and it never touches the database; it is only the formula.

Binding a keeper to the model

Add the HasKeepers trait to the model and attach the keeper with the #[KeptBy] attribute.

use EduLazaro\Larakeep\Concerns\HasKeepers;
use EduLazaro\Larakeep\Attributes\KeptBy;
use App\Keepers\InvoiceKeeper;

#[KeptBy(InvoiceKeeper::class)]
class Invoice extends Model
{
    use HasKeepers;
}
Enter fullscreen mode Exit fullscreen mode

The attribute is repeatable, so a model can carry several keepers, each owning different fields. If you prefer to keep the model clean, register the same binding in a service provider's boot() instead.

Invoice::keep(InvoiceKeeper::class);
Enter fullscreen mode Exit fullscreen mode

Filling the fields

Call process() with a field name and the keeper's matching method runs, assigning its return value to the model attribute. Pass an array to fill several at once, and because process() returns the model, you chain the save.

$invoice->process('total');
$invoice->process(['total', 'amount_due'])->save();
Enter fullscreen mode Exit fullscreen mode

The one thing to keep in mind is that closing save(). process() sets the attributes in memory and hands the model back, but it does not persist anything on its own. The natural place to call it is the model's saving observer, so the derived fields are always current right before a write.

Fields that take arguments

A method can take arguments. Suffix it with With and pass the arguments as an array to processWith().

public function getTotalWith(string $currency): int
{
    // ...
}

$invoice->processWith('total', ['EUR']);
Enter fullscreen mode Exit fullscreen mode

The arguments go in as an array, one element per parameter.

Other verbs, not just get

get is only the default prefix. Name a method with any verb, say refreshTotal(), and run it through processTask() with that verb.

public function refreshTotal(): int
{
    // ...
}

$invoice->processTask('refresh', 'total');
$invoice->processTask('refresh', ['total', 'amount_due']);
Enter fullscreen mode Exit fullscreen mode

Its argument-taking cousin is processTaskWith('refresh', 'total', ['EUR']), the same as processWith() with an explicit verb. This is handy when one keeper computes the same field in more than one way.

Wrapping up

That is Larakeep end to end: a keeper class holding the formulas, #[KeptBy] to bind it, and process() to run those formulas into the model's attributes before you save.

The derived-field logic lives in one place per model instead of living on random unwanted places through an observer.

👉 Package on Packagist: https://packagist.org/packages/edulazaro/larakeep
👉 Source on GitHub: https://github.com/edulazaro/larakeep

Top comments (0)