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

Laravel Repository Laravel Package

andydefer/laravel-repository

A repository pattern implementation for Laravel that abstracts database queries behind clean, testable interfaces. Ships base repository classes, contracts, and helpers for common CRUD and query operations, keeping controllers slim and letting you swap data sources without touching app logic.

View on GitHub
Deep Wiki
Context7

Getting Started

Because this is a zero-star, single-maintainer package, treat the first ten minutes as a source-code review, not just an install.

1. Read the package before installing it. Open the repo and scan, in this order:

  • composer.json → required PHP/Laravel versions and autoload namespace
  • src/ → the base repository class and any contracts/interfaces
  • README.md → the canonical example (usually a UserRepository)

That five-minute pass tells you whether the package gives you an abstract base class, an interface you implement, or both. Everything below assumes the common shape: a base class you extend plus a contract you bind in a service provider. Adjust names to whatever src/ actually exports.

2. Install and bind.

composer require andydefer/laravel-repository
// app/Providers/RepositoryServiceProvider.php
public function register(): void
{
    $this->app->bind(
        \App\Contracts\UserRepository::class,
        \App\Repositories\EloquentUserRepository::class
    );
}

Register it in bootstrap/providers.php (Laravel 11+) or config/app.php.

3. First use case: extract one messy controller method. Do not refactor everything. Pick a controller method that has three or more chained Eloquent calls plus business logic, and move the query half into a repository:

// app/Contracts/UserRepository.php
interface UserRepository
{
    public function findActiveByEmail(string $email): ?User;
}

// app/Repositories/EloquentUserRepository.php
class EloquentUserRepository extends BaseRepository implements UserRepository
{
    public function __construct(User $model)
    {
        parent::__construct($model);
    }

    public function findActiveByEmail(string $email): ?User
    {
        return $this->model->newQuery()
            ->where('email', $email)
            ->where('is_active', true)
            ->first();
    }
}

4. Look first at the base class's constructor (how the model is injected), its protected $model property, and any built-in methods for all, find, create, update, delete, paginate. Those are your freebies; everything else you write yourself.

If the package has no src/ beyond one small file, that's fine — the value is the convention, not the code. Consider vendoring the class into app/Repositories/BaseRepository.php so you're not dependent on an unmaintained dependency for a 60-line abstraction.

Implementation Patterns

One repository per aggregate root, not per table. OrderRepository owns orders, order_items, and anything loaded with it. A OrderItemRepository that only ever gets called from OrderRepository is dead weight.

Interfaces at the boundary, Eloquent behind it. The contract is what your controllers, jobs, and Livewire components type-hint. The implementation is the only file that knows Illuminate\Database\Eloquent\Builder exists. This is the single pattern that makes the abstraction worth its cost — it's what lets you swap in an API-backed or cached implementation later.

// app/Http/Controllers/CheckoutController.php
public function __invoke(CheckoutRequest $request, UserRepository $users)
{
    $user = $users->findActiveByEmail($request->email)
        ?? throw new \Illuminate\Database\Eloquent\ModelNotFoundException();
    // ...
}

Keep domain-specific methods, not generic ones. findActiveByEmail() is a repository method. whereEmailAndIsActive() is a leaked query builder. If a method signature contains an operator ($column, $operator, $value), it belongs in the caller or a dedicated query object.

Split reads from writes when the class grows past ~150 lines. Either two interfaces (UserReader / UserWriter) or separate repositories. Read repositories are cheap to mock in tests; write repositories carry transaction concerns.

Use the repository as the transaction boundary.

public function placeOrder(Cart $cart, User $user): Order
{
    return DB::transaction(function () use ($cart, $user) {
        $order = $this->create([...]);
        $order->items()->createMany($cart->lines());
        return $order;
    });
}

If you find yourself calling DB::transaction() in a controller, the transaction logic belongs in the repository (or better, a dedicated action/service that composes repositories).

Decorator pattern for caching — the highest-leverage extension point.

class CachedUserRepository implements UserRepository
{
    public function __construct(
        private UserRepository $inner,
        private Cache $cache,
    ) {}

    public function findActiveByEmail(string $email): ?User
    {
        return $this->cache->remember(
            "user:active:{$email}", 300,
            fn () => $this->inner->findActiveByEmail($email)
        );
    }
}

Bind the decorator in the provider and the rest of the app is unaware. This is where repositories earn their keep in a Laravel app — not in testability (Eloquent models are already mockable-ish), but in composability.

Eager loading is the repository's job. Callers should never have to write ->with('items.product'). Bake it into the method:

public function findWithItems(int $id): ?Order
{
    return $this->model->newQuery()->with('items.product')->find($id);
}

Testing. Because you type-hinted interfaces, tests become:

$this->mock(UserRepository::class, fn ($m) => $m
    ->shouldReceive('findActiveByEmail')->with('[email protected]')->andReturn($user));

For the Eloquent implementation itself, use RefreshDatabase against a real SQLite/MySQL connection — do not mock the model. Testing repository implementations against a mocked builder verifies nothing.

Pagination and filters: return DTOs or paginators, not arrays. LengthAwarePaginator is fine and Laravel-native; if you need framework independence, wrap it. Avoid ->toArray() in repositories — you lose the model and its relations.

Gotchas and Tips

Zero stars means zero safety net. Pin the version in composer.json ("andydefer/laravel-repository": "1.0.*" or an exact tag), commit composer.lock, and read the diff before every composer update. There will be no CVE advisories and no community answers. If it's under ~200 lines, fork it into app/ and move on.

The repository pattern is a known Laravel anti-pattern when applied dogmatically. The framework's own position — and the prevailing community one — is that Eloquent models are the abstraction and wrapping them in UserRepository::all() adds indirection without removing any coupling. Adopt this package only where it buys you something specific: caching decorators, multi-source data (DB + API), or a large team that needs a hard boundary. If you can't name the concrete win, skip it.

Check the return types before you trust "type-safe." Look at the base class signatures. A common failure mode is find(int $id): ?Model on the base but find(int $id): User in the subclass — a PHP fatal error or a silent TypeError at runtime. Verify covariance works for your overrides, and run php artisan about plus a smoke test after wiring the provider.

Static analysis will need generics annotations. PHPStan/Psalm will flag BaseRepository::find() returning Model when you expect User. Add @template TModel of \Illuminate\Database\Eloquent\Model in a local stub, or override with a docblock:

/** @return User|null */
public function find(int $id): ?User

Without this, larastan at level 6+ will generate noise that trains your team to ignore the analyser.

Don't let models leak into the contract if you actually plan to swap implementations. interface UserRepository { public function find(int $id): ?User; } is not swappable with an API client. Return DTOs or value objects if framework independence is a real goal. If it isn't a real goal — and for most apps it isn't — admit that and stop paying the abstraction tax.

N+1 is now hiding one layer deeper. Repositories make it easy to lose track of what's loaded. Two guards: bake eager loads into method names (findWithItems), and enable Model::preventLazyLoading() in AppServiceProvider::boot() outside production. That single line catches most repository-introduced N+1s before they ship.

Watch for accidental query-builder leakage. If the base class exposes query() or model() publicly, someone will call $repo->model()->whereRaw(...) in a controller within a month. Override those as protected, or accept that the boundary is advisory and review for it.

Debugging slow queries routed through a repository. The stack trace now points at EloquentUserRepository.php instead of your controller — actually helpful. Pair with DB::listen() in a local service provider or Laravel Telescope to see the SQL. If the package wraps Cache, log cache hits/misses in the decorator; a caching repository without hit-rate visibility is a liability.

Transactions and events. DB::transaction() inside a repository means model events (created, saved) fire before commit. If a listener sends mail or dispatches a job, you can notify a user about an order that later rolls back. Use DB::afterCommit() on the listener/job, or dispatch domain events explicitly after the transaction closes.

Extension points to look for (and add if missing): a resetQuery()/newQuery() protected helper to stop builder state bleeding between calls, a applyFilters() hook, and a $with property for default eager loads. If the base class mutates shared builder state on the instance, you have a stateful-singleton bug waiting — verify the class is bound as a non-singleton (bind, not singleton) in your provider, or that it constructs a fresh builder per call.

Migration path if it dies. Keep every repository behind its interface. Then, when the package goes unmaintained, replace BaseRepository with your own 50-line class — a one-file change touching nothing in your domain code. That is the actual deliverable of adopting this package; make sure you get it.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource