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

Eloquent Repository Laravel Package

nilportugues/eloquent-repository

A Laravel package that adds a repository layer for Eloquent models, with reusable query criteria, caching support, and cleaner separation between persistence and application logic.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes)

composer require phprepository/php-eloquent-repository

Generate your first repository with the package's artisan command:

php artisan make:repository UserRepository

The generated class extends PHPRepository\Repository and requires a single model() method returning the Eloquent model FQCN:

namespace App\Repositories;

use App\Models\User;
use PHPRepository\Repository;

class UserRepository extends Repository
{
    public function model(): string
    {
        return User::class;
    }
}

Bind it in a RepositoryServiceProvider (or AppServiceProvider) so you can inject an interface:

$this->app->bind(UserRepositoryInterface::class, UserRepository::class);

Where to look first

  1. PHPRepository\Repository — the base class; read every public method before writing a custom one. Most "custom" methods you think you need already exist (find, findBy, findWhere, paginate, pluck, with, orderBy, scopeQuery).
  2. PHPRepository\Contracts\RepositoryInterface / CriteriaInterface — the contract surface; type-hint against this in your services.
  3. PHPRepository\ReadRepository / PHPRepository\WriteRepository — the read/write split; use these instead of the monolithic base when a consumer should only read.
  4. app/Repositories/ — the convention the generator writes to; keep it consistent, one repository per aggregate root.

First real use case: take a fat controller action — User::where('active', 1)->with('team')->orderBy('name')->paginate(25) — and move it behind $this->users->paginateActive(). That single move gets you testability, a place for criteria, and a boundary you can enforce in code review.

Implementation Patterns

Interface-first binding. Always bind an interface, never the concrete class, in controllers and jobs. It is what makes $this->mock(UserRepositoryInterface::class) trivial and stops consumers from calling newQuery() and leaking Eloquent.

interface UserRepositoryInterface extends RepositoryInterface
{
    public function paginateActive(int $perPage = 25): LengthAwarePaginator;
}

Custom methods should reuse the pipeline. Anything you add must go through newQuery() + applyCriteria() so pushed criteria still apply:

public function paginateActive(int $perPage = 25): LengthAwarePaginator
{
    return $this->applyCriteria($this->newQuery())
        ->where('active', true)
        ->orderBy('name')
        ->paginate($perPage);
}

Criteria for composable filters. This is the package's main value-add — admin screens, search endpoints, and API filters become reusable, individually testable classes:

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

$users = $repository->pushCriteria(new ActiveCriteria())
                    ->pushCriteria(new TeamCriteria($teamId))
                    ->paginate();

Criteria are also the right place for authorization scoping (TenantCriteria, OwnedByCriteria) — push them from middleware or a base service so no caller can forget them.

Read/write split. For CQRS-lite, or when a service should never create(), extend ReadRepository:

class UserReadRepository extends ReadRepository
{
    public function model(): string { return User::class; }
}

As of 2.2.2, ReadRepository implements count() and exists() directly. That means $read->count() and $read->exists() are first-class contract methods, not magic __call passthroughs — you can now type-hint and fake them reliably, and read-only repositories no longer need a WriteRepository dependency just to answer "does this exist?"

Eager loading and pagination stay in the repository. Accept array $with = [] and apply it in the method; do not let controllers chain ->with('...') onto a returned builder. Returning LengthAwarePaginator (not a builder) from paginated methods keeps the boundary real.

Transactions belong one layer up. Repositories wrap single-model operations; a service or action that spans two repositories owns the DB::transaction(). Wrapping in the repository makes composition impossible.

Gotchas and Tips

Upgrade note for 2.2.2 — division by zero. The release fixes a DivisionByZeroError thrown when pagination math hits a zero divisor. In practice this surfaced when perPage was 0 or -1 — usually because someone passed a raw ?per_page= request value straight through, or a config default was misread. The fix removes the crash, but it does not make 0 a sane page size; keep validating:

$perPage = max(1, min((int) $request->input('per_page', 25), 100));

If you had a defensive $perPage = $perPage ?: 25 guard in your own code purely to dodge this crash, you can drop it — but only after confirming you are pinned to ^2.2.2.

Upgrade note for 2.2.2 — count()/exists() on ReadRepository. If you previously wrote your own countUser()/hasUser() wrappers on read repositories, delete them; they now shadow real contract methods and confuse readers. Also check any __call-based or mocking-based tests — assertions that previously exercised magic pass-through will now hit concrete methods.

Singleton repositories leak criteria. If you register a repository as a singleton, or you run under Octane, pushCriteria() mutates the instance and the next request inherits the previous one's TenantCriteria. Bind normally (bind) or use scoped:

$this->app->scoped(UserRepository::class);

Symptom of getting this wrong: filters "stick" between requests, or the first request after deploy behaves differently.

skipCriteria() is stateful, not a pipeline flag. Chain it on a fresh instance, or you will silently disable criteria for every later call on that object. Same for resetCriteria().

Criteria run on every query, including writes you route through newQuery(). A SoftDeleteCriteria-style filter that assumes it is only applied to SELECTs can corrupt an update. Scope criteria to read methods, or guard on the builder type.

model() returns a class string, never an instance. Returning new User is the single most common misread of the base class, and the failure is confusing (it works for reads, then breaks on create()).

Do not return query builders. Every time a repository method returns Builder, a controller chains ->where() on it and the abstraction is gone. Return arrays, models, paginators, or collections.

Testing. Unit-test criteria against a builder with RefreshDatabase + sqlite in-memory (they are queries, not mocks); integration-test repositories against a real schema; mock the interface in controllers and services. Resist mocking Builder — it is a losing battle.

Extension point. If you need behaviour across every repository (e.g. audit logging on writes, cache busting), override the base methods in an App\Repositories\BaseRepository that all your repositories extend — do not patch the vendor class, and do not reach for a trait that duplicates Repository's method names, or you will get confusing precedence with the base class.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle