dannyweeks/laravel-base-repository
A base repository package for Laravel that reduces boilerplate when working with Eloquent models. It provides reusable CRUD methods, query helpers, eager loading, and caching so you can keep controllers and models lean and your data layer consistent.
The package is essentially one abstract class, so "getting started" is genuinely a five-minute job.
Install and pin it. Last release was March 2016, so install the exact tag that resolves rather than floating a constraint:
composer require dannyweeks/laravel-base-repository
There is no service provider to register and no config file to publish — the package ships only Dannyweeks\Repositories\BaseRepository and an exception class.
Look at the source first. Open vendor/dannyweeks/laravel-base-repository/src/Dannyweeks/Repositories/BaseRepository.php. It is short enough to read end-to-end in a few minutes, and that is the fastest way to learn the real method surface (find, findBy, findAll, findAllBy, paginate, create, with, scopeQuery, resetScope, getModel, newModel). Everything else in this assessment is commentary on that file.
Create one repository. The only required configuration is the $model property:
namespace App\Repositories;
use App\User;
use Dannyweeks\Repositories\BaseRepository;
class UserRepository extends BaseRepository
{
protected $model = User::class;
}
Use it once, for real. Do not start by migrating every controller. Pick the read path with the most duplicated query logic and replace it:
public function show(UserRepository $users, $id)
{
$user = $users->find($id, ['id', 'name', 'email']);
abort_if(is_null($user), 404);
return view('users.show', compact('user'));
}
Because the constructor takes no arguments, the container resolves the repository automatically when it is type-hinted — no binding needed.
First use case: treat BaseRepository as a home for query logic that would otherwise be copy-pasted across controllers. If a controller only ever calls User::find($id), the repository earns its keep the moment you add a second method like activeWithPosts(). Start there.
Class-string $model, not an instance. Set protected $model = User::class;. The base class instantiates internally (newModel()), which keeps the underlying model fresh instead of sharing one stateful instance across every call on the repository.
Custom domain methods are the actual product. The inherited CRUD is boilerplate; the value is in named intent:
public function activeWithRecentPosts(int $perPage = 15)
{
return $this->with('posts')
->scopeQuery(function ($query) {
return $query->where('active', true)->latest();
})
->paginate($perPage);
}
public function findByEmail(string $email)
{
return $this->findBy('email', $email);
}
Controllers then read as business operations rather than query-builder archaeology.
Chaining with() for eager loading. with() mutates the query context and returns $this, so chain it before the terminal call (find, findAll, paginate). Do not call it after — the query has already run.
scopeQuery() for one-off constraints. Use it for ad-hoc filters in a controller or a one-off command, and reserve dedicated methods for anything used more than once. It accepts a closure receiving the Eloquent builder, so any builder API is available inside it.
Interface binding for testability. If you want to fake the repository in tests, define UserRepositoryInterface, bind implementation to interface in a service provider, and type-hint the interface. This is the only reason to add container configuration — do not add it otherwise.
Composition over inheritance depth. If two repositories share custom behaviour (e.g. a searchable trait), use traits on top of BaseRepository. Avoid multi-level inheritance chains; the base class is deliberately thin and there is no reason to build a hierarchy on it.
Register repositories as singletons only with care (see gotchas). The default — fresh instance per resolution — is the safe option.
findOrFail, updateOrCreate, firstOrCreate, soft-delete helpers, or transaction wrapper. Plan to write those yourself in the concrete repository; do not assume parity with modern Eloquent conveniences.with() and scopeQuery() store state on the repository. In a standard per-request lifecycle this is invisible. In queue workers, Octane, or a repository registered as a singleton, the constraints from the previous job will silently apply to the next query. Call resetScope() after a scoped query, or bind the repository as a non-singleton (the default).$model must be set or you get an exception. The abstract class throws if the property is missing or does not resolve to an Eloquent model. If you see that exception, you forgot the property in a subclass, not the parent.$fillable.dd($repo->getModel()->toSql()), or wire DB::listen() in a service provider. Because the builder is held internally, dd() on the repository itself is rarely useful.newModel() in a subclass when you need to pre-configure the query (e.g. ->newQuery()->withTrashed() or a specific connection). This is the cleanest hook for cross-cutting model setup and avoids repeating it in every custom method.How can I help you explore Laravel packages today?