kalnoy/cruddy
Cruddy is a Laravel package for building admin CRUD panels with minimal code. Define controllers with fields, columns, filters, and actions to get ready-made index, create, edit, and delete screens. A clean, customizable structure keeps lists, forms, and permissions easy to tailor.
Step 0 is an install attempt, and it is expected to fail. Cruddy's only release is 2014-12-26, which predates Laravel 5.0. On anything from Laravel 5 through 11, composer require kalnoy/cruddy will conflict out against your illuminate/* constraints before a single file lands. Confirm that in five minutes, record the failure, and then decide what you're actually doing — reading it, or porting it. There is no third option where you require it and move on.
Where to look first, in this order:
belongsTo / many-to-many are resolved. This is where the real design decisions live.First use case: a single admin resource over a boring model — a Post or a Product — with a list screen (search, sort, pagination), a create form, and an edit form. Do it once by hand against Cruddy's config shape so you feel where the boilerplate actually is. That exercise, not the package, is the deliverable.
If you're on Laravel 4.2 with Cruddy already in the tree, the first use case is different: pick the one endpoint your ops team actually uses, write a characterization test around it, and only then touch anything.
The core pattern: configuration on the controller, generation everywhere else. Every legitimate use of Cruddy is a variation on "declare the resource, let the base class do the rest." Internalize the shape:
belongsTo / many-to-many.Pattern 1 — thin controller, fat config. Never override the generated actions to add "just one" behavior. The moment you do, you've written a bespoke admin controller with a scaffolding package bolted on, and you've lost the only benefit Cruddy offers. If you need per-resource behavior, add a hook to the config, not a hand-written method.
Pattern 2 — one resource per model, no shared controllers. Cruddy's mental model is a 1:1 mapping. Resist the urge to build a generic "manage anything" controller on top of it; that's a different, harder product.
Pattern 3 — authorization belongs in your middleware, not the package. Cruddy does not ship policies or gates. The idiomatic integration is to wrap the generated resource routes in a route group with your auth/admin middleware, and to add a per-resource gate check in the controller constructor. Do this before the first feature, not after — retrofitting authorization onto generated endpoints means auditing every route.
Pattern 4 — override views, not logic. Publishing and editing the Blade templates is the intended customization seam. UI changes go in templates; behavior changes go in config; anything else is a fork.
Pattern 5 — the portable one: reimplement the config object on your current stack. This is how you actually leverage Cruddy day-to-day in 2024+. Take the declarative shape and build it yourself in a few hundred lines:
abstract class CrudController extends Controller
{
abstract protected function columns(): array; // list screen
abstract protected function fields(): array; // create/edit
abstract protected function rules(?Model $model = null): array;
public function index(Request $request) { /* whitelisted sort/filter/paginate */ }
public function store(FormRequest $request) { /* validated create */ }
public function update(FormRequest $request, Model $model) { /* validated update */ }
}
Then: a FormRequest whose rules() delegates to the controller's rules(), a set of Blade components (<x-field.text>, <x-field.select) that replace the old Form/Html helpers, resource routes, and a policy check in authorizeResource. You keep the declarative ergonomics, you own the write path, and you get route-model binding, FormRequests, and policies for free — the three things Cruddy structurally cannot give you.
Migration mapping, if you're porting a 4.2 install: illuminate/html → laravelcollective/html or Blade components; Input:: facade → Request; concrete Illuminate\Validation\Validator type hints → contracts; lists() → pluck(); array_get → data_get / Arr::get; {{ }} in L4 views → verify escaping semantics, they changed.
Integration tip — eager loading. Add the relations rendered in the list to the query's with() in one place. Cruddy-era configs typically don't do this for you, and an admin list over a belongsTo is the classic place an N+1 hides for months.
Assume the write path is unsafe until proven otherwise. CRUD generators of this vintage routinely do $model->fill(Input::all()) with no $fillable/$guarded discipline. Before anything touches a network: grep the source for fill(, confirm $fillable is set on every model, and confirm there is no fillable bypass. This is the failure that matters — remote and quiet.
Sort and filter parameters are attacker-controlled column names. Check whether orderBy and filter clauses are built directly from request input. If they are, whitelist against a declared sortable/filterable list before the query builder ever sees them. Both mass assignment and this are the same class of bug and both are likely present.
No policies, no gates, no roles. The generated endpoints are role-less. Any access control is entirely the host application's job, and the package will not remind you. Put a gate in front of every resource route before you demo it to anyone.
Composer will not resolve on modern Laravel — don't fight it. PHP 5.4–5.6 and Laravel 4.0–4.2 is the only environment it was built against, and that framework's security support ended in 2017. Even a working install means EOL everything.
PHP 8 deprecations surface as errors. Dynamic property creation (deprecated in 8.2) and removed helpers will bite on minor upgrades. There is no upstream to patch them — you are the maintainer from the first commit.
Blade escaping semantics changed between L4 and L5. If you port views, check every {{ }}: things that were previously unescaped are now escaped (double-escaping HTML), and existing {{{ }}} or raw blocks may now mean something different. Visual bugs here look like data corruption.
Pagination defaults are a scaling cliff. If the config doesn't specify a page size, or specifies a large one, admin lists become unbounded queries. Set it explicitly and cap it.
Filter/sort applied in PHP after retrieval is the other scaling trap — it works fine at 500 rows and dies at 500,000. Push it into SQL.
Customization creep is the structural risk. The package's value is that the resource is declarative. One-off overrides — a custom action here, a hand-rendered column there — erode that until you're maintaining a bespoke admin framework nobody signed up for. If you need a custom behavior more than twice, make it a first-class config option or don't build it.
Extension points, such as they are: published views, and subclassing the controller. That's it. There is no event system, no field-type registry, no plugin surface. Anything beyond templates and config is a fork.
Documentation is a decade old and thin on the parts that matter. Authorization, validation behavior, and query construction are undocumented — you will read source to answer those questions. Write down what you learn, with a named owner, because knowledge concentration on a single engineer is a real operational risk here.
Zero dependents means no community fork and no shared patches. No Stack Overflow corpus from the last decade either. Every question is answered by reading 2014 source, by you.
The honest day-to-day summary: on a modern stack you don't run this package, you read it for an afternoon and steal the config shape. On a legacy 4.2 app you fork it, pin it, and treat it as owned code — because it is. Adopting it saves a few days of scaffolding and costs a permanent unsupported fork sitting in the write path of internal tooling, which is the worst place to carry technical debt.
How can I help you explore Laravel packages today?