ozankurt/repoist
Repoist is a Laravel package that simplifies the repository pattern. It provides Artisan commands to generate repository classes and interfaces for Eloquent models, helping organize database queries, keep controllers lean, and make data access easier to test and maintain.
Repoist is a scaffolding tool, not a runtime library — it generates repository interfaces + Eloquent implementations so you don't hand-write the boilerplate. Treat it as a dev dependency.
composer require --dev ozankurt/repoist
On Laravel 5.5+ the service provider is auto-discovered. On older 5.x, register OzanKurt\Repoist\RepoistServiceProvider::class in config/app.php.
Where to look first, in this order:
php artisan list — confirm the actual command signature and namespace the package registered (generator command names in 2019-era packages vary; don't assume from the README).config/repoist.php after php artisan vendor:publish — this controls the repository namespace, contracts namespace, and output paths. Get this right before generating anything, because the paths are baked into every generated file.stubs/ directory inside the package — this tells you exactly what the generated class looks like, which is faster than generating and reading.First use case — scaffold a repository for one model and wire it end to end:
php artisan make:repository User # or whatever `artisan list` shows
You should end up with something shaped like:
app/Repositories/Eloquent/UserRepository.php
app/Repositories/Contracts/UserRepositoryInterface.php
Then bind it once, inject it, and stop. The value of the first hour is proving the container resolution works:
// app/Providers/RepositoryServiceProvider.php
public function register()
{
$this->app->bind(
\App\Repositories\Contracts\UserRepositoryInterface::class,
\App\Repositories\Eloquent\UserRepository::class
);
}
class UserController extends Controller
{
public function __construct(private UserRepositoryInterface $users) {}
public function index()
{
return view('users.index', ['users' => $this->users->all()]);
}
}
If that renders, the tool has done its job. Everything after is discipline, not tooling.
Generate per aggregate root, not per table. Repositories should map to the thing your domain talks about (OrderRepository, not OrderLineItemRepository). If you find yourself generating five repositories for one bounded context, you're wrapping tables instead of modelling.
The generated class is a starting point, so edit it immediately. The stub typically gives you all(), find(), create(), update(), delete(). Delete the ones you don't need. An interface with all() on it that no caller uses is noise that future readers have to reason about.
Let the model come in through the constructor. This is the single biggest upgrade over the raw stub, because it makes the repository testable without a container:
class EloquentUserRepository implements UserRepositoryInterface
{
public function __construct(protected User $model) {}
}
Push real query logic down, keep controllers thin. The pattern earns its keep when a query is genuinely complicated and reused:
public function activeSubscribersWithRecentActivity(Carbon $since): Collection
{
return $this->model
->where('status', 'active')
->whereHas('subscription', fn ($q) => $q->where('renews_at', '>', now()))
->with(['subscription', 'lastActivity'])
->whereHas('lastActivity', fn ($q) => $q->where('created_at', '>=', $since))
->get();
}
That's a nameable, testable concept. $user->where(...)->get() sprinkled across three controllers is not.
Integration with the rest of the stack:
$request->all(). Pass explicit arrays or DTOs.Order and Invoice belongs in a service that calls two repositories.UserResource decides what a user looks like on the wire. Don't return arrays from the repository — you'll lose lazy-loading control and route-model-binding.Testing pattern — this is where the interface pays for itself:
$this->app->instance(UserRepositoryInterface::class, new InMemoryUserRepository());
Feature tests get a fake with no database. Note that the interface must be honest for this to work: if your implementation returns an EloquentCollection and your fake returns a SupportCollection, the fake is lying to you. Return Collection from the contract or type against the interface.
When you outgrow it: once you have ~10 repositories, consider replacing the per-class binding with an auto-binding convention in the service provider (loop over app/Repositories/Eloquent/*.php, derive the contract name, bind). Hand-maintained bind lists rot fast.
This package is frozen at Laravel 5 / 2019. That is the dominant fact about it. Expect friction on PHP 8 (implicit nullable params, ArrayAccess return types in the container, str_*/array_* signature changes) and outright failure on Laravel 9+. If you're on a modern Laravel, do not try to force it — copy the stubs into your own stubs/ directory and write a 40-line artisan command. You get the same output with zero dependency risk. Check composer why-not and the package's own composer.json constraints before spending an afternoon debugging a generator.
Re-running the generator overwrites silently. Before you regenerate, commit. If the command has a --force flag, it exists because the default path refuses to clobber — respect it. There is no merge; you will lose your hand-written methods.
Target [X] is not instantiable means your binding didn't register. Almost always one of: provider not in config/app.php, php artisan config:cache run before you added the binding, or a namespace typo in the bind. Run php artisan config:clear first, then php artisan tinker and app(UserRepositoryInterface::class) to isolate.
Changing the app namespace breaks generated files. If your root namespace isn't App\, set it in config/repoist.php before the first generation. Fixing it afterwards is a grep -r across every generated file, and it's easy to miss one on a Friday.
Don't let the repository become a god object. The generator makes it trivially easy to keep adding methods, and six months later you have a 900-line UserRepository with 40 methods, three of which are used. Split into focused query classes (ActiveUsersQuery, UserSignupStats) and keep the repository for persistence.
Keep the interface in sync manually. It's plain PHP — nothing enforces that the contract matches the implementation. A stale method signature in the interface becomes a fatal error only when that code path runs in production. Consider a one-line test that reflects over the interface and asserts each method exists on the implementation with matching parameters.
Beware leaky abstractions. The moment a method signature takes Request $request or returns a paginator built from request('page'), the repository knows about HTTP and your "swap the implementation" story is fiction. Take scalars (int $perPage) and pass them from the controller.
Extension point that's actually worth using: the stubs. Publish them (php artisan vendor:publish) and edit to match your house style — strict types, final classes, a readonly model property, your own base class. Every future repository then starts correct, which is a much better return than any runtime feature the package offers.
If the package is unmaintained for your needs, the exit ramp is cheap: the generated code has no runtime dependency on Repoist. Delete the --dev requirement, keep the files, done. Plan for that from day one and the package never becomes load-bearing.
How can I help you explore Laravel packages today?