saritasa/laravel-controllers
A Laravel package that provides base controller classes and reusable CRUD actions to reduce boilerplate when building resource controllers. It standardizes request handling, validation, filtering, pagination, and API responses for consistent endpoints.
Install and inspect before you build anything.
composer require saritasa/laravel-controllers
Because the package is archived and has a tiny install base, treat the source as the documentation. Order of operations:
composer.json first — note the illuminate/* version constraints. If you're on a Laravel version newer than the last release (2024-08-07), verify compatibility before writing a single controller; you may need a fork with a widened constraint rather than --ignore-platform-reqs in production.src/ for the abstract base classes and traits. The package's whole value is a small number of base classes for "typical operations" — CRUD index/show/store/update/destroy, and the glue that connects a FormRequest, a service/action class, and a response. Find the exact FQCN of the base controller you intend to extend; do not copy snippets from blog posts, since this package's API is not stable across forks.tests/ (if present) for the canonical usage. A test fixture controller is usually the most accurate example of how the authors intended the base class to be consumed.First use case: pick exactly one existing hand-rolled resource controller in your app — ideally a simple, non-nested, non-polymorphic CRUD endpoint — and rewrite it to extend the package's base controller. Keep the routes identical and assert with your existing feature tests that responses are byte-for-byte equivalent. That gives you a safe side-by-side comparison of what the base class does for free (binding resolution, 404 handling, response shaping) versus what you were doing manually.
If that first conversion doesn't save you meaningful code, stop there. With 6 stars and an archived status, the package has to earn its place in one controller before you roll it out.
Extend the base, override the hooks. The intended pattern is inheritance rather than composition: your PostController extends the package's CRUD controller, declares the model/service/request classes it should use (typically via protected properties or an abstract method), and overrides only the lifecycle hooks you need (beforeStore, afterUpdate, custom authorization, custom eager-loading).
class PostController extends CrudController
{
protected function getModelClass(): string { return Post::class; }
protected function getStoreRequest(): string { return StorePostRequest::class; }
protected function applyFilters($query)
{
return $query->when(request('status'), fn ($q, $s) => $q->where('status', $s));
}
}
Wire it through the service layer, not the model. The typical-operation controllers exist to keep HTTP concerns thin. Keep validation in FormRequests, business rules in your service/action classes, and let the controller only translate request → service call → response. If the base class expects a service, register it in the container and let type-hinting do the work; that's what makes the same base controller reusable across resources.
Use it as a convention enforcement tool, not a feature. The day-to-day win is that every CRUD endpoint in the codebase returns the same status codes, the same pagination envelope, and the same 404/422 shapes. That's worth more than the lines of code saved — it means frontend and API consumers stop special-casing individual endpoints. Adopt it on new resources first and let consistency spread.
Partial adoption via traits. If the base controller is too opinionated for a given resource (say, a read-only reporting endpoint), look for granular traits per action and mix in only index/show. This is the lowest-risk integration path and the easiest to reason about during code review.
Integration tips
Route::apiResource() as normal — the package governs controller behavior, not routing.arch()) asserting all API controllers extend the base class, so drift is caught in CI rather than in review.Archived means frozen. No security patches, no Laravel 11/12 support unless the constraint happens to allow it, no fixes for PHP 8.4 deprecations. Mitigations: pin the exact version, vendor-fork it under your own namespace if you need changes, and record in your ADR why the dependency exists so a future maintainer can rip it out. Do not build a large surface area on top of it.
Method signature coupling. Base-controller packages like this one lock you into specific method names, visibility, and parameter lists for hooks. A minor release (if one ever lands) can silently break an override because PHP won't warn you when a parent method's signature changes — you'll get a fatal at runtime, not at install. Write at least one feature test per endpoint so upgrades fail loudly.
Constructor/injection surprises. Since the base controller may resolve models, services, and requests from the container, new PostController(...) in a unit test is usually wrong. Go through the HTTP layer ($this->get('/posts')) or use app()->make(); mocking dependencies in a directly-instantiated controller is where most debugging time will go.
Route model binding mismatches. If the base class resolves the model itself from a route parameter name, a rename like {post} → {article} will 404 with no obvious error. First debugging step on any unexpected 404 from a controller of this type: php artisan route:list --path=posts and confirm the parameter names, then confirm the model's route key.
Config quirks. If the package ships a config file, publish it (php artisan vendor:publish) rather than editing vendor/. Any behavior toggled by config (pagination size, response wrapping, error format) should be set explicitly in the published file — relying on package defaults means a fork or upgrade changes your API contract without a diff in your repo.
Extension points to prefer, in order: (1) config, (2) overriding protected hook methods, (3) composing traits for single actions, (4) forking. Avoid editing vendor code, and avoid extending the base controller with a second intermediate abstract class — that layer becomes the place where every future developer is confused about which index actually runs.
Debugging checklist for weird responses: dump the resolved controller with php artisan route:list -v, check whether a middleware is rewriting the response, and confirm you haven't accidentally left a parent::store() call that runs the default behavior plus yours (a double-insert is the classic symptom).
How can I help you explore Laravel packages today?