hocvt/l5-repository
Laravel repository package that abstracts the database layer behind reusable repositories. Add criteria, presenters, validators, Fractal transformers and caching without repeating query code, keeping controllers and models clean and testable.
hocvt/l5-repository is a fork of prettus/l5-repository — the package still uses the Prettus\ namespace, so all configuration, contracts, and generators live under Prettus\Repository\* regardless of the Composer name. Confirm the installed namespace before copy-pasting docs from either repo.
Minimal setup:
composer require hocvt/l5-repository
php artisan vendor:publish --provider="Prettus\Repository\Providers\RepositoryServiceProvider"
This creates config/repository.php (generator paths, cache settings, Fractal serializer, etc.). Laravel 5.5+ auto-discovery registers the service provider.
Generate a repository + interface:
php artisan make:repository UserRepository
This drops app/Repositories/UserRepository.php and UserRepositoryInterface.php. Wire them in a service provider:
// app/Providers/RepositoryServiceProvider.php
public function register(): void
{
$this->app->bind(
\App\Repositories\UserRepositoryInterface::class,
\App\Repositories\UserRepository::class
);
}
Implement it:
namespace App\Repositories;
use App\Models\User;
use Prettus\Repository\Eloquent\BaseRepository;
class UserRepository extends BaseRepository
{
public function model(): string
{
return User::class;
}
}
First use case: inject the interface into a controller and use all(), find(), or paginate():
public function index(UserRepositoryInterface $users)
{
return view('users.index', ['users' => $users->paginate(15)]);
}
Where to look first:
config/repository.php — generator paths, cache defaults.Prettus\Repository\Contracts\RepositoryInterface — the full method surface (all, paginate, find, findBy, findWhere, create, update, delete, with, scopeQuery, pushCriteria, presenter, skipPresenter, resetCriteria, resetModel).Always type-hint the contract, never the concrete class. This makes it trivial to swap to a cached or in-memory implementation later, and keeps controllers free of Eloquent calls.
class UserController extends Controller
{
public function __construct(private UserRepositoryInterface $users) {}
public function show(int $id)
{
return $this->users->find($id);
}
}
with() sets persistent eager loads; scopeQuery() receives the Eloquent builder for ad-hoc filtering:
$active = $this->users
->with(['posts', 'profile'])
->scopeQuery(fn ($q) => $q->where('active', true))
->paginate(20);
Because with() and scopeQuery() mutate repository state, reset between logical operations:
$this->users->resetModel(); // clears model, with, scopeQuery
$this->users->resetCriteria(); // clears pushed criteria
Criteria are the package's main lever for keeping query logic out of controllers. Implement CriteriaInterface and push it per-call or globally.
use Prettus\Repository\Contracts\CriteriaInterface;
use Prettus\Repository\Contracts\RepositoryInterface;
class ActiveUsersCriteria implements CriteriaInterface
{
public function apply($model, RepositoryInterface $repository)
{
return $model->where('active', true);
}
}
$this->users->pushCriteria(new ActiveUsersCriteria())->paginate(20);
Push globally in boot() for cross-cutting scopes (tenant isolation, soft-delete variants):
public function boot(): void
{
parent::boot(); // REQUIRED
$this->pushCriteria(new TenantCriteria(auth()->user()->tenant_id));
}
RequestCriteria for index endpointsThe package ships Prettus\Repository\Criteria\RequestCriteria, which reads search, searchFields, filter, orderBy, and sortedBy from the current request. This is a fast way to get a filterable index without hand-writing query parsing.
$this->users->pushCriteria(app(RequestCriteria::class));
Requirements:
getFieldsSearchable() in the repository (or set $fieldSearchable) — otherwise search does nothing.When a repository needs to return a transformed response, register a presenter implementing Prettus\Repository\Presenter\FractalPresenter and call presenter() or set it in the constructor.
public function presenter(): string
{
return UserPresenter::class;
}
Then find()/paginate() return transformed payloads; use skipPresenter() when you need raw models (e.g., inside a service that will further mutate data).
Read-heavy repos can implement Prettus\Repository\Contracts\CacheableRepository and extend CacheRepository (or use the cacheable variant of BaseRepository). Configure via config/repository.php (cache.enabled, cache.minutes, cache.repository). Override cacheKey() and cacheMinutes() per repository, and remember the repository invalidates cache on create/update/delete but not on arbitrary scopeQuery/raw DB writes.
make:entity scaffolds a validator implementing Prettus\Repository\Contracts\ValidatorInterface (Prettus\Validator\LaravelValidator). Assign it via validator(). Rules run on create and update, and throw Prettus\Validator\Exceptions\ValidatorException — catch it in a form request / exception handler and map to 422.
Repositories fire RepositoryEntityCreated, RepositoryEntityUpdated, RepositoryEntityDeleted. Listen for these when you want side effects (search indexing, audit logging) without stuffing them into the repository or model observers.
make:entity)php artisan make:entity User produces model, repository, interface, presenter, validator, controller, and migration in one shot. Treat this as scaffolding: inspect every file, delete what you don't need, and fix namespaces before running tests. It will prompt before overwriting existing files — prefer generating individual pieces (make:repository, make:presenter, make:validator) once a project is off the ground.
hocvt/l5-repository, but all classes are Prettus\.... IDE autocomplete and Stack Overflow answers referencing andersao/l5-repository still apply — the fork tracks the original API.parent::boot() is mandatory. Overriding boot() without calling the parent silently skips criteria registration, event wiring, and reset behavior.resetCriteria() in jobs/queues.with() and scopeQuery() also persist. This is the most common source of "why does this query have stale constraints" bugs. Prefer local variables + resetModel(), or keep repos stateless by always calling resetModel() at the start of a public method.find() throws ModelNotFoundException. Use findWhere([...])->first() or catch it in the exception handler. Do not rely on null returns.getFieldsSearchable() is a whitelist. If search returns everything or nothing, this is the first thing to check. Wildcards (name vs name:*) follow the original package semantics — read the source, not blog posts.file and database cache stores don't support them. Use Redis/Memcached in any environment where caching is enabled, or you'll chase phantom stale data.DB::transaction() does not roll back cache. Invalidate explicitly after commits.presenter() is set, create()/update() may return presenter-transformed data instead of the model. Use skipPresenter() inside services and tests that assert on raw attributes.paginate() returns a LengthAwarePaginator; the Fractal presenter wraps the underlying collection. Assertions against $result->items() may need $result->toArray()['data'] instead.make:entity migrations are stubs. They generate plausible columns from the entity name only — always edit before migrate.:memory: + RefreshDatabase). For unit tests of consumers, mock the interface — the concrete BaseRepository is hard to fake because of its fluent state.model(), presenter(), validator(), cacheKey(), and boot() over overriding all(), find(), etc. The fluent methods are chained internally; overriding them tends to break criteria and caching.How can I help you explore Laravel packages today?