Open source October 2023
Larakeep

Larakeep

Larakeep

Larakeep moves the logic that fills derived model fields (an excerpt, a reading estimate, a denormalized search column, a cached count) out of observers and into small classes called Keepers.

If you have used Laractions, the distinction is one line: an action performs an operation, a keeper fills a field.

Installation

composer require edulazaro/larakeep

The service provider auto-registers and there is nothing to publish. Keepers are plain classes; the only wiring is a trait and an attribute.

Writing a keeper

A keeper receives the model in its constructor and exposes one method per field it computes. The convention is get followed by the PascalCase of the field, so excerpt maps to getExcerpt() and reading_minutes to getReadingMinutes().

php artisan make:keeper ArticleKeeper
namespace App\Keepers;

use App\Models\Article;
use Illuminate\Support\Str;

class ArticleKeeper
{
    public function __construct(private Article $article) {}

    public function getExcerpt(): string
    {
        return Str::limit(strip_tags($this->article->body), 160);
    }

    public function getReadingMinutes(): int
    {
        return max(1, (int) ceil(str_word_count(strip_tags($this->article->body)) / 200));
    }
}

Each method returns the value; it does not assign it. Nothing in a keeper touches the database.

Binding it to the model

Add HasKeepers and bind the keeper with the #[KeptBy] attribute, which is repeatable so a model can carry several.

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

#[KeptBy(ArticleKeeper::class)]
class Article extends Model
{
    use HasKeepers;
}

If you would rather not annotate the model, register the same binding in a service provider's boot() with Article::keep(ArticleKeeper::class).

Running it

process() runs the matching method and assigns its return value to the model attribute. Pass an array to fill several at once.

$article->process('excerpt');
$article->process(['excerpt', 'reading_minutes']);

process() sets the attribute in memory and returns the model, but does not persist. Save as usual, chaining if you like:

$article->process(['excerpt', 'reading_minutes'])->save();

Inside a saving observer there is no second save, because the attribute is set before the write:

class ArticleObserver
{
    public function saving(Article $article): void
    {
        $article->process('excerpt');
    }
}

Backfilling existing rows

The same keeper replays over a whole table when the formula changes:

Article::query()->chunkById(500, function ($articles) {
    foreach ($articles as $article) {
        $article->process(['excerpt'])->save();
    }
});

Fields that take arguments

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

public function getExcerptWith(int $length): string
{
    return Str::limit(strip_tags($this->article->body), $length);
}

$article->processWith('excerpt', [280]);

Other verbs

get is only the default prefix. Name a method configureExcerpt() and run it through processTask('configure', 'excerpt'), or processTaskWith() when it takes arguments. Useful when one keeper computes a field in more than one way.

built and maintained by Edu Lazaro · MIT license