jsdecena/baserepo
A Laravel repository-pattern package that provides reusable base repository and contract classes for cleaner CRUD, query filtering, pagination, and model interaction.
Install
composer require jsdecena/baserepo
The package has no service provider or config to publish — it is a thin abstraction layer, so installing it is the whole setup. Check the Jsdecena\Baserepo\BaseRepository source first; it is small and reading it (~100 lines) answers most questions faster than the README.
First use case — a CRUD repository for one model
namespace App\Repositories;
use App\Models\Post;
use Jsdecena\Baserepo\BaseRepository;
class PostRepository extends BaseRepository
{
public function __construct(Post $post)
{
parent::__construct($post);
$this->model = $post;
}
}
Then inject it anywhere Laravel resolves dependencies:
class PostController extends Controller
{
public function __construct(PostRepository $posts)
{
$this->posts = $posts;
}
public function show($id)
{
return $this->posts->find($id);
}
}
Where to look first
src/BaseRepository.php — the actual CRUD surface (all, find, findBy, create, update, delete, getModel).src/Traits/Searchable.php — search() implementation and the $searchable property.src/Traits/Cacheable.php — cache-wrapped reads and how keys are built.If the code you inherit still works today, you are almost certainly on Laravel ≤ 6/7 conventions and PHP ≤ 7.x; plan a review before dropping it into a Laravel 10/11 app (see Gotchas).
Standard CRUD repository per aggregate root
One repository per Eloquent model, injected into a controller, job, or command. Keep the repository thin — the package does not add query builders or scopes, so anything sophisticated belongs in the model or a dedicated query object.
$post = $this->posts->create($request->validated());
$post = $this->posts->find($id);
$this->posts->update($request->validated());
$this->posts->delete();
Searchable trait for admin/list endpoints
use Jsdecena\Baserepo\BaseRepository;
use Jsdecena\Baserepo\Traits\Searchable;
class PostRepository extends BaseRepository
{
use Searchable;
protected $searchable = ['title', 'body'];
}
search($term) produces a WHERE ... OR ... LIKE query across $searchable. This is fine for small admin tables and small tables; it is not a substitute for Laravel Scout on large datasets.
Cacheable trait for hot reads
use Jsdecena\Baserepo\Traits\Cacheable;
class PostRepository extends BaseRepository
{
use Cacheable;
}
Wrap read-only listing/detail methods with cache and invalidate in create/update/delete. Because the trait overrides methods like all() and find() from BaseRepository, cache invalidation has to be wired explicitly in your write paths (see Gotchas).
Interface + service provider binding
Define an interface (PostRepositoryInterface) and bind the concrete class in a dedicated RepositoryServiceProvider. This is the pattern the package is designed around and gives you test doubles without touching Eloquent.
$this->app->bind(PostRepositoryInterface::class, PostRepository::class);
Compose repositories for cross-model workflows
Because BaseRepository only wraps one model, keep multi-model orchestration in a service class. Example: an OnboardUserService that calls UserRepository::create and ProfileRepository::create inside a transaction — do not push cross-model logic into a single repository.
Return models, not query builders
findBy($field, $value) returns the first match; chain further Eloquent calls on the returned model, not on the repository. If you find yourself wanting a fluent interface, add explicit methods to your repository instead of leaking $this->model out.
It is effectively unmaintained. Last release was September 2019. There is no Laravel 8/9/10/11 tag and no PHP 8 type-hint update. It will usually still run because it delegates to Eloquent, but expect to fork or vendor-edit for newer PHP if you hit #[ReturnTypeWillChange] or strict typing issues. Budget a review for any project past Laravel 7.
find() mutates $this->model. Many of the base methods reassign $this->model to the fetched entity, so the repository is stateful after a call. Chaining two find() calls or reusing an injected repository within a long-running request/loop can produce surprising results. Safest pattern: inject per-request, call one method, and don't reuse the resolver across iterations in queued jobs.
Trait methods override base methods. Cacheable::all() / Cacheable::find() shadow BaseRepository::all() / find(). If you enable caching on a repository and call all() expecting fresh data (e.g. right after a write in the same request), you will get the cached version unless you invalidate. Put cache-busting in your update/delete wrappers, not in the base class.
Trait order matters for Searchable + Cacheable together. Both traits may define overlapping helpers. Declare them deliberately (use Cacheable, Searchable;) and confirm in a test which one wins for any shared method name before relying on it.
$searchable must be declared on the repository, not the model. The trait reads the repository property. Forgetting it silently produces an empty search. Add a feature test with a known term to catch this.
No built-in validation or exception handling. find() returns null if not found (Eloquent's find), and create() will throw whatever Eloquent throws. If you need "not found" exceptions or form-request validation, add them in your repository method overrides:
public function findOrFail($id)
{
return $this->find($id) ?? throw new ModelNotFoundException;
}
No soft-delete awareness out of the box. all() uses get(), so soft-deleted rows are excluded as usual — but findBy and custom methods need to know whether you want withTrashed(). Wrap explicitly.
Better to extend than edit the vendor directory. If you need behavior changes (typed properties, PHP 8 compatibility, cache tags), create an App\Repositories\BaseRepository that extends the package class or copy the base class in. Composer will overwrite vendor/jsdecena/baserepo/ on composer update.
Debugging tip: when a call behaves differently than Eloquent would, dump $this->model->toSql() inside the method (or getModel() from outside) — the package adds no query manipulation except in the traits, so the query is always the underlying Eloquent state.
How can I help you explore Laravel packages today?