Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

L5 Repository Laravel Package

plum/l5-repository

Laravel package that adds repositories, criteria, presenters, and transformers to your applications for cleaner, testable data access. Supports caching, request criteria, and easy model swapping—a practical way to organize query logic and keep controllers and models lean.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. plum/l5-repository is a fork of what most people know as prettus/l5-repository (formerly andersao/l5-repository) — the 4k stars belong to the upstream lineage. Before you write code, confirm two things in the installed source: the root namespace (upstream uses Prettus\Repository\*) and the service-provider class name. Fork docs drift; the source never lies.

composer require plum/l5-repository
php artisan vendor:publish --provider="Prettus\Repository\Providers\RepositoryServiceProvider"

That publishes config/repository.php — the two blocks that matter are generator (paths/namespaces for make:* commands) and criteria.params (which query-string keys RequestCriteria is allowed to honour).

Where to look first, in this order:

  1. config/repository.php — sets where generated repositories live (app/Repositories, namespace App\Repositories).
  2. vendor/plum/l5-repository/src/Eloquent/BaseRepository.php — every method you'll call (all, paginate, find, create, update, deleteWhere, scopeQuery, pushCriteria, skipPresenter) lives here. Skim the signatures; they return Eloquent collections until you add a presenter.
  3. src/Criteria/RequestCriteria.php — the single most leveraged and most dangerous class in the package.
  4. src/Contracts/ — the interfaces you'll bind against in a service provider.

First use case — generate and bind one repository:

php artisan make:repository Users\UserRepository --model=User
// app/Repositories/Users/UserRepositoryEloquent.php
namespace App\Repositories\Users;

use App\Models\User;
use Prettus\Repository\Eloquent\BaseRepository;

class UserRepositoryEloquent extends BaseRepository implements UserRepository
{
    public function model(): string
    {
        return User::class;
    }

    public function getFieldsSearchable(): array
    {
        return ['name', 'email']; // required, or RequestCriteria search throws
    }
}
// app/Providers/RepositoryServiceProvider.php  (register in config/app.php providers)
public function register(): void
{
    $this->app->bind(UserRepository::class, UserRepositoryEloquent::class);
}

Then inject the interface, never the Eloquent class:

public function index(UserRepository $users)
{
    return $users->with('roles')->orderBy('name')->paginate(20);
}

Implementation Patterns

Bind bind, not singleton. Repositories accumulate state (criteria, orderBy, with, scopeQuery) on the instance. $this->app->bind() gives each resolution a clean instance; a singleton will leak a where('active', true) from one controller action into the next request in the same worker (Octane makes this catastrophic).

Push RequestCriteria per-action, not in the constructor. The tempting pattern is $this->pushCriteria(app(RequestCriteria::class)) inside the repository — that makes every call in the app magically driven by whatever query string the user sent. Prefer a base controller/middleware or explicit opt-in:

public function index(UserRepository $users)
{
    $users->pushCriteria(app(RequestCriteria::class));

    return $users->paginate(20);
}

This gives you ?search=foo&searchFields=name:like, ?filter=status:active;role:admin, ?orderBy=created_at&sortedBy=desc for free — but only where you asked for it. Always pair it with a non-empty getFieldsSearchable().

Criteria are the right extension point for reusable where-clauses. Don't subclass the repository per use case:

class ActiveOnly implements CriteriaInterface
{
    public function apply($model, RepositoryInterface $repository)
    {
        return $model->where('active', true);
    }
}

$users->pushCriteria(new ActiveOnly)->all();
$users->popCriteria(ActiveOnly::class);

php artisan make:criteria ActiveOnly scaffolds it. Criteria receive the Eloquent builder, so they can do joins, whereHas, ordering — anything.

Escape hatch: scopeQuery and model(). For one-offs that aren't worth a criteria class:

$users->scopeQuery(fn ($q) => $q->whereHas('orders', fn ($o) => $o->where('total', '>', 500)))
      ->paginate(15);

$query = $users->model()->newQuery(); // raw Eloquent when you outgrow the API

model() is also how you reach withTrashed(), lockForUpdate(), and relationship helpers the repository doesn't proxy.

Presenters for API shape, not for domain logic. Implement FractalPresenter (requires league/fractal, an optional dependency you must install yourself) or a custom PresenterInterface and call $repo->setPresenter(new UserPresenter) in a route-scoped binding or service provider. Remember to skipPresenter() inside jobs, console commands, and tests where you need real models back.

Caching is opt-in and coarse. Implement CacheableInterface and use CacheableRepository:

class UserRepositoryEloquent extends BaseRepository implements UserRepository, CacheableInterface
{
    use CacheableRepository;

    public function getCacheKey(): string
    {
        return 'users.'.md5(serialize([$this->getCriteria(), request()->query()]));
    }

    public function getCacheMinutes(): int
    {
        return 60;
    }
}

The default key does not account for criteria — if you leave it alone, pushCriteria() will silently serve another criteria set's cached results. Always override getCacheKey().

Testing. Swap the binding for a fake in TestCase::setUp(), and call $this->repo->skipPresenter() in setUp of any test touching a presenter-backed repository. For integration tests, RefreshDatabase + the real repository is cheaper than mocking the fluent API.

Gotchas and Tips

  • find() throws, first() returns null. find($id) raises Prettus\Repository\Exceptions\RepositoryException when nothing matches — map it to a 404 in Handler::render() or use firstWhere('id', $id) where absence is normal.
  • Mass assignment still applies. create()/update() go through fill(); a field missing from $fillable is silently dropped and you'll blame the repository. Same for update() — it loads the model then saves, so it fires saved/updated events but can't do bulk where updates. Use updateWhere() or raw Eloquent for those.
  • Return types shift under presenters. paginate() returns LengthAwarePaginator normally, but a presenter-wrapped collection when one is set. Type-hinting Illuminate\Support\Collection on a repository return value breaks the moment someone enables a presenter. Type-hint the interface, or accept iterable.
  • Criteria and scope are sticky. pushCriteria, orderBy, with, and scopeQuery persist on the instance until popCriteria/resetCriteria/resetScope. Any queue job or long-running process reusing one instance will accumulate clauses. bind() + fresh resolution is the defense.
  • getFieldsSearchable() returning [] is a runtime bomb — not a boot error. The first request carrying ?search= throws a RepositoryException in production. Make the generator template include it by default.
  • with() called twice duplicates eager loads (roles, roles.roles). Reset between calls or build the relation list conditionally.
  • Search validation package is separate. The --validator flag on make:repository and the generated validator classes expect prettus/laravel-validation. It is not bundled. If you don't install it, delete the generated validator or you'll get class-not-found on boot of that repository.
  • Cache invalidation only covers writes through the repository. Seeders, direct model saves, other services, and DB::table() writes leave stale entries. Wire model observers to call $repo->flushCache() if the data is written from more than one place.
  • The model property must resolve to a Model. BaseRepository's constructor validates $this->model(). Wrong namespace, a typo'd FQCN, or returning an instance instead of a class string all produce confusing exceptions at resolution time.
  • RequestCriteria is a blunt instrument. It parses a fixed grammar from config('repository.criteria.params'); it does not understand relations, or OR groups. Anything beyond flat filters and LIKE belongs in a criteria class. Never let it touch columns that aren't indexed or user-scoped — ?filter=tenant_id:X is a data leak waiting to happen; whitelist by overriding the criteria or removing filter from params.
  • Fork drift is the real risk at 0 dependents. Read CHANGELOG and diff against prettus/l5-repository before upgrading across majors; pin the exact version and record the namespace/provider in your README so the next dev doesn't copy upstream docs verbatim.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core