nanvaie/database-repository
A Laravel package that provides a database-backed repository layer for cleaner data access. It helps organize queries behind reusable repositories, keeping Eloquent models and controllers lean while supporting maintainable CRUD workflows.
Install the package
composer require nanvaie/database-repository
On Laravel 5.5+ the service provider is auto-discovered. On Lumen you must register it manually in bootstrap/app.php:
$app->register(Nanvaie\DatabaseRepository\Providers\RepositoryServiceProvider::class);
Check composer.json / vendor/nanvaie/database-repository for a config/ or src/ folder first — the package name suggests a thin base class rather than a full scaffolding tool, so there is usually little to publish.
Inspect the base class before writing anything. Open the package's src/ and read the abstract repository (likely BaseRepository or similar). You need to know:
$model property, a constructor argument, or a model() method.all, find, findOrFail, create, update, delete, paginate, with, where wrappers).This determines how much you override versus inherit.
Create your first repository
namespace App\Repositories;
use App\Models\Post;
use Nanvaie\DatabaseRepository\BaseRepository; // verify exact namespace
class PostRepository extends BaseRepository
{
public function __construct(Post $model)
{
parent::__construct($model);
}
}
The constructor-injected model is the most common contract in this family of packages; if the base class instead reads protected $model = Post::class;, adapt accordingly.
Wire it into a controller and use it for one read path (listing) plus one write path (create). That is the smallest useful slice and confirms the base class method signatures match your expectations before you refactor everything.
Where to look next: the package's README.md and its tests are the authoritative docs. With ~19 stars the README is often the only documentation, and the tests reveal the real method contracts faster than the source.
One repository per aggregate root. Keep repositories at the model level (PostRepository, OrderRepository), not per-page or per-feature. Feature logic belongs in services/actions that compose repositories.
Interface + binding for testability. The main payoff of a repository layer is swapping implementations:
// app/Providers/RepositoryServiceProvider.php
public function register(): void
{
$this->app->bind(PostRepositoryInterface::class, PostRepository::class);
}
Type-hint the interface in controllers/services and bind a fake in tests. Binding to the concrete class only buys you nothing over injecting the model directly.
Keep custom query methods on the repository, not the controller.
class PostRepository extends BaseRepository
{
public function publishedWithAuthor(int $perPage = 15)
{
return $this->model
->with('author:id,name')
->where('published_at', '<=', now())
->latest('published_at')
->paginate($perPage);
}
}
This keeps eager-loading and filtering decisions in one place and makes them reusable across controllers, jobs, and console commands.
Wrap multi-step writes in transactions at the service layer, not the repository. Repositories should stay single-operation. Compose them:
DB::transaction(function () use ($data) {
$order = $this->orders->create($data);
$this->invoices->createForOrder($order);
});
Use repositories as the seam for caching and events. A CachedPostRepository or a decorator that dispatches domain events after create()/update() is far cleaner than scattering Cache::forget calls across controllers — and it is the reason to keep the interface binding in place even when you only have one implementation.
Consistent return types across repositories. Decide up front: paginators for lists, models (or null/ModelNotFound) for single fetches, booleans for deletes. If the base class's all() returns a Collection but your custom getActive() returns an array, callers will break silently.
Lumen integration: without auto-discovery, remember the service provider registration and that Lumen's Eloquent is opt-in ($app->withEloquent()). A repository package will fail confusingly if Eloquent is not bootstrapped.
Verify the namespace and method contracts for this exact version. The package is small and low-traffic; class names, constructor signatures, and return types have a habit of shifting between releases. Pin the version in composer.json and read src/ after every minor bump. Last release is 2025-03-05, so treat anything written against an older tag as suspect.
The base class may not return what you expect. Some repository bases return raw arrays or standard objects rather than Eloquent models, which breaks $post->author, route-model binding, and Blade accessors. Assert the return type in a quick test ($this->assertInstanceOf(Post::class, $repo->find(1))) before building on it.
Do not lose query state. If the base class exposes a shared builder property, chained calls mutate it and leak between calls. Prefer methods that start from a fresh $this->model->newQuery() each time. If you see weird "filters from the previous request are still applied" bugs, this is almost always the cause.
Method name collisions. A base update($id, $data) versus a custom update(Post $post, $data) will fatal or behave unexpectedly. Check the parent signature before adding methods.
N+1 hides easily behind repositories. $repo->all() returns bare models; the controller then loops and touches relations. Add with() in repository list methods, and consider a global Model::preventLazyLoading() in non-production.
Mass assignment errors come from the model, not the repository. create($data) flows straight into Model::create, so $fillable/$guarded still governs everything. Guard against passing whole $request->all().
Mass assignment errors come from the model, not the repository. create($data) flows straight into Model::create, so $fillable/$guarded still governs everything. Guard against passing whole $request->all().
Soft deletes are a common surprise. A base all() on a soft-deleting model excludes trashed rows; if you need them, you must call withTrashed() explicitly on a custom method — the base class will not offer it unless documented.
Testing: because the abstraction is thin, integration tests against SQLite in-memory catch far more than mocking the repository. Mock the interface only in unit tests of services; do not mock Eloquent builders — they are painful and brittle.
Extension points: the intended customization surface is almost certainly overriding base methods in your subclass and passing a pre-configured model. If you find yourself editing the vendor code, stop and open an issue — with 19 stars, a fork is cheap and a patch PR is likely welcome.
Debugging tip: if a repository method returns empty unexpectedly, dump the underlying builder with ->toSql() and ->getBindings() from a custom method rather than guessing. Most "repository bug" reports turn out to be scope, soft-delete, or global-scope interactions on the model.
How can I help you explore Laravel packages today?