prezent/crud-bundle
prezent/crud-bundle is a PHP bundle for scaffolding CRUD workflows, helping developers generate create, read, update, and delete screens with less boilerplate. It targets admin panels and data-management interfaces, simplifying routine entity handling and rapid backend prototyping.
Install and register. composer require prezent/crud-bundle then, on Symfony Flex, accept the recipe (or manually add Prezent\CrudBundle\PrezentCrudBundle to config/bundles.php). If there's no Flex recipe, you also need to import routes yourself:
# config/routes/prezent_crud.yaml
prezent_crud:
resource: '@PrezentCrudBundle/Resources/config/routing.xml'
prefix: /admin
Where to look first. In order: README.md → src/Controller/ (the base controller/traits you'll extend) → src/DependencyInjection/Configuration.php (the real config schema — the README is usually stale on a 2-star package) → src/Resources/views/ (template names you can override) → composer.json for the supported Symfony range.
First use case. Wire a single small entity end-to-end before touching anything else: an Article with id/title/body/createdAt. Get an index list, a create form, an edit form, and a delete action rendering against the bundle's default templates. That single vertical slice tells you whether the bundle assumes a specific entity shape (identifier name, timestamp fields), how it resolves the entity from the route, and whether it ships usable defaults or expects you to override everything.
Laravel mental mapping. You're a Laravel dev, so frame it as: this package is roughly Route::resource() + a resource controller + Blade scaffolding, except nothing is convention-over-configuration. There is no make:crud generator, no implicit route-model binding, and no $fillable mass-assignment safety net. You declare the resource, and Symfony's ParamConverter (or the bundle's own resolver) maps {id} to an entity. Every piece you'd get for free in Laravel — validation rules, authorization, pagination — is a separate Symfony component you must wire in.
Declarative resource registration. The dominant pattern for this family of bundles is a config block (or PHP attribute) per entity, pointing at an entity class, a form type, and templates:
prezent_crud:
resources:
article:
entity: App\Entity\Article
form: App\Form\ArticleType
route_prefix: admin_article
Confirm the exact key names against Configuration.php; on an unmaintained bundle the README drifts from the schema and you'll burn an hour on a config that silently ignores unknown keys.
Controller inheritance over configuration. Extend the bundle's base controller per resource and override only the hooks you need (createEntity, getQueryBuilder, getRoutePrefix). This is the highest-leverage pattern: you get the bundle's request handling, form processing, and flash/redirect flow, but every domain decision stays in your app. Prefer this to forking.
Service decoration for cross-cutting behavior. Rather than overriding five controller actions to add audit logging or tenancy scoping, decorate the bundle's manager/repository service in services.yaml:
App\Crud\AuditedCrudManager:
decorates: prezent_crud.manager
arguments: ['@.inner']
This works only if the bundle's services are public or referenced by interface — check debug:container before committing to this approach.
Query customization for lists. Almost every real CRUD needs a filtered, joined list. Implement the bundle's query-builder hook to add ->leftJoin(...)->addSelect(...) for relations rendered in the index table, otherwise you ship an N+1 on day one. Do filters (search, status, date range) as query-string parameters read in the same hook, and preserve them across pagination links in your template override.
Template override by bundle path. Symfony resolves overrides at templates/bundles/PrezentCrudBundle/<original path>. Copy the bundle's templates in wholesale, then edit — never try to override a fragment by guessing the block name. Use bin/console debug:twig to confirm which file actually wins.
Security per action. The bundle almost certainly does not ship authorization. Add #[IsGranted('ROLE_ADMIN')] at the controller class level and, for object-level checks, a voter invoked on edit/delete. Without this you have a public write endpoint the moment you mount the routes.
Testing pattern. Functional tests with WebTestCase cover this well: log in a user, POST the create form with a valid payload and an invalid one, assert redirects and validation errors, then assert the row exists. Test the delete action with the CSRF token — the failure mode on these bundles is a 400 from a missing/misnamed token that only shows up in a browser.
Zero dependents is the headline risk. Two stars and no dependents means no community to have found the edge cases for you. Pin the exact version in composer.json, read src/ rather than trusting docs, and assume you may need to fork. Budget for that; if this is going into anything long-lived, vendor the code into your app or write a thin adapter interface so you can swap it later.
Check the Symfony version constraint before anything else. CRUD bundles are tightly coupled to Form, HttpKernel, and Routing APIs. If it targets Symfony 4/5 and you're on 6/7, you'll hit deprecation-level breakage (getDoctrine(), Controller::createForm() signatures, Request::get() changes) that looks like a bundle bug but is a version mismatch.
Config keys fail silently. Symfony's config tree ignores unknown keys at the top level in some setups. After editing YAML, run bin/console config:dump-reference prezent_crud and bin/console cache:clear. In dev, if a change has no effect, it's almost always the config cache or an unimported routes file — not the bundle.
Template override paths must match exactly, including the bundle's internal directory nesting. If you override index.html.twig and the page still shows the default, you have the wrong path, not a caching bug. debug:twig names the winning template; trust it.
Route collisions. Resource route prefixes generate predictable names (admin_article_index, admin_article_edit). If two resources share a prefix, or you already have an admin_article_edit route, Symfony silently uses the first match. Run bin/console debug:router | grep article and confirm counts after adding each resource.
Delete is a POST, not a link. The bundle will expect a form with a CSRF token and _method=DELETE (or its own convention). Hand-rolled <a href=".../delete"> links will 405 or 403 depending on its CSRF strategy. Check the shipped template and copy the form markup exactly.
Entity identifier assumptions. If the bundle assumes getId() returns an integer and your entity uses a UUID or slug, route generation and ParamConverter resolution break in non-obvious ways. Use the bundle's ID-field config if it exposes one; otherwise expect to override the entity-resolution method.
List-page performance. Default list queries are unfiltered findAll(). Once the table exceeds a few thousand rows you'll feel it. Override the query builder with explicit setMaxResults/setFirstResult (or use Pagerfanta if the bundle integrates it) and add a default sort on an indexed column.
Validation lives in your Form type. The bundle renders and processes forms but won't infer constraints. Missing @Assert annotations and a missing constraints array on form fields produce empty-looking 500s rather than friendly validation errors. Verify the form type declares required => false where appropriate too — Symfony defaults to required and quietly rejects empty optional fields.
Debugging toolkit. bin/console debug:container prezent (service IDs and whether they're public), debug:event-dispatcher (did the bundle dispatch a pre-persist event you can hook instead of overriding?), and the profiler's Forms and Doctrine panels for the "form submitted but nothing saved" class of bug — usually a validation failure you never rendered.
Extension points, in order of preference. Event subscribers the bundle dispatches → service decoration → overriding a protected method on the base controller → template override → forking. Forking is a last resort but on this package it may be the realistic end state; if you fork, keep the diff to src/ so upstream merges stay possible.
How can I help you explore Laravel packages today?