bfg/repository
bfg/repository is a Laravel package that adds a clean repository layer between your application and Eloquent models, keeping queries organized and controllers lean. It supports reusable query logic and a testable data access structure.
This package is effectively undocumented at scale (1 star, 0 dependents), so treat src/ as the specification and the README as a hint, not a contract. Budget 15 minutes to read the source before wiring anything into a real project.
composer.json in the repo and check the require block for illuminate/support and php constraints. A low-traffic package like this is often pinned to an older Laravel major; if the constraint is ^8.0 you may need --ignore-platform-reqs or a fork for Laravel 10/11. Also check autoload.psr-4 to learn the real namespace — do not guess it from the package name.composer require bfg/repository
ls vendor/bfg/repository/src
Look for: a base repository class (abstract, wraps Model), an optional Contracts/Interfaces directory, a Criteria or Filters directory, and any Console commands (a make:repository generator is common in this family of packages).composer.json does not list a provider under extra.laravel.providers, you must register it manually in config/app.php (Laravel ≤10) or bootstrap/providers.php (Laravel 11+) — otherwise any container bindings or generator commands silently won't exist.Post. Write the smallest possible repository against it — extend the package's base class, set the model, and call a single read method — then call it from php artisan tinker. Do not convert a whole domain on day one; you want to discover the return types (Eloquent Collection vs LengthAwarePaginator vs plain array) on a throwaway class.If step 1 or 4 fails, stop. For a package this small the cost of writing a 60-line base repository yourself is lower than the cost of debugging someone else's.
One repository per aggregate root, not per table. The value of the pattern is that PostRepository can own the join to authors and the eager-loads; a repository per table just adds a passthrough layer over Eloquent.
Interface binding in a dedicated provider. This is where the package earns its keep — controllers depend on the interface, tests swap in a fake:
// app/Providers/RepositoryServiceProvider.php
public function register(): void
{
$this->app->bind(
\App\Contracts\PostRepository::class,
\App\Repositories\EloquentPostRepository::class
);
}
Extend, don't wrap. Subclass the package's base repository and add domain-specific methods (publishedForHomepage(), archiveOlderThan()). Keep the inherited generic CRUD (all, find, create, update, delete) for the boring cases and write explicit methods for anything with a where clause that matters. Generic all($columns, $orderBy) signatures are where repository packages usually leak.
Criteria/filter objects for cross-cutting query fragments. If the package ships a criteria mechanism, use it for reusable filters (tenant scope, published scope) rather than for one-off queries — criteria that are used once are just indirection with extra files. If there is no criteria system, closures or Eloquent local scopes are a fine substitute; don't build one.
Keep transactions in the service layer, not the repository. Repositories that call DB::transaction() internally compose badly when two of them need to share a transaction. The controller/service calls DB::transaction(fn () => ...) and the repositories stay transaction-agnostic.
Incremental migration path. Introduce the repository alongside existing Model::query() calls, route new endpoints through it, and backfill old call sites only when you touch them. A big-bang conversion creates a huge diff for zero user-visible benefit and is the usual reason repository refactors get reverted.
Testing. Bind the interface to a real implementation and run against SQLite in-memory for query correctness; bind to a mock only for controller tests where you're asserting HTTP behaviour, not SQL. Mocked repositories that return canned collections hide broken where clauses very effectively.
whereHas, withCount, cursor, chunkById, lockForUpdate) either needs a method on your subclass or it defeats the point. Audit the base class's method list early and decide which gaps you'll fill.paginate() returning a paginator but find() returning ?Model and all() returning a Collection is fine — until a refactor makes one of them ->toArray(). Document the contract on your interface with return types and stick to it.all() with no $with support, you will ship N+1s. Set protected $with = [...] on the model itself, or override the read methods to apply with().update() built on ->where(...)->update([...]) bypasses saving/saved observers, so slugs, UUIDs, and audit columns silently don't fire. Know which of the two code paths each method uses.forget must live in the same class's write methods — otherwise the next developer adds a write elsewhere and the cache goes stale.register(), not boot(), for bindings.$model->getTable() or default connection; multi-tenant or multi-database setups need those overridden.composer update six months from now should not be able to change your data layer.How can I help you explore Laravel packages today?