kachnitel / dynamic-form-bundle
Zero-configuration Symfony form generation from Doctrine entity metadata
Package info
github.com/kachnitel/dynamic-form-bundle
Type:symfony-bundle
pkg:composer/kachnitel/dynamic-form-bundle
Requires
- php: >=8.2
- doctrine/doctrine-bundle: ^3.0
- doctrine/orm: ^3.5
- symfony/doctrine-bridge: ^6.4|^7.0|^8.0
- symfony/form: ^6.4|^7.0|^8.0
- symfony/framework-bundle: ^6.4|^7.0|^8.0
- symfony/ux-autocomplete: ^3.0
- symfony/ux-live-component: ^2.13
- symfony/validator: ^6.4|^7.0|^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.51
- marcocesarato/php-conventional-changelog: dev-main
- phpmd/phpmd: ^2.12
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.0
- rector/rector: ^2.5
- symfony/browser-kit: ^7.0|^8.0
- symfony/console: ^6.4|^7.0|^8.0
- symfony/intl: ^6.4|^7.0|^8.0
- symfony/yaml: ^6.4|^7.0|^8.0
Suggests
- symfony/intl: Required at runtime if any entity used with DynamicEntityFormType has #[Assert\Country], #[Assert\Language], #[Assert\Currency], or #[Assert\Locale] on a property — both for the corresponding CountryType/LanguageType/CurrencyType/LocaleType form widget to render its choice list, and for the constraint itself to validate at all (Symfony throws without it, independent of this bundle). See docs/TYPE_GUESSING.md.
This package is auto-updated.
Last update: 2026-07-20 05:44:20 UTC
README
Zero-configuration Symfony form generation from Doctrine entity metadata. Point DynamicEntityFormType at an entity and get a working create/edit form — scalar fields, associations, and Symfony UX LiveComponent collections included — without writing a FormType class.
Extracted from kachnitel/admin-bundle, where it now serves as the auto-form engine behind the generic CRUD controller. It has no dependency on admin-bundle and is usable standalone in any Symfony + Doctrine application.
License note: this package is MPL-2.0, not MIT like admin-bundle. It's a file-level copyleft license — compatible with proprietary and closed-source use — see License below for what it actually requires.
Quick Start
1. Install
composer require kachnitel/dynamic-form-bundle
Register the bundle in config/bundles.php:
return [ // ... Kachnitel\DynamicFormBundle\KachnitelDynamicFormBundle::class => ['all' => true], ];
No further configuration — the bundle has no config tree yet.
2. Build a form
use Kachnitel\DynamicFormBundle\Form\DynamicEntityFormType; $form = $this->createForm(DynamicEntityFormType::class, $product, [ 'entity_class' => Product::class, 'data_class' => Product::class, ]);
entity_class is a required option. data_class isn't set by the form type itself — pass it yourself as shown, so submitted values land on the right entity; leaving it out means Symfony has no PropertyAccessor target to write to.
3. That's it
Every scalar field and every owning-side association on Product gets a form field, with a type-appropriate widget, validation, and nullability handling all derived straight from Doctrine metadata. Fields whose Doctrine string column carries a matching validator constraint (#[Assert\Email], #[Assert\Url], ...) are automatically upgraded to a more specific widget — see Type Guessing.
How It Works
Four collaborating pieces, one job each:
| Class | Responsibility |
|---|---|
DoctrineFormTypeMapper |
Maps a single Doctrine field/association mapping to a Symfony form field config (type + options) |
DynamicEntityFormType |
Walks an entity's metadata, calls the mapper for each field/association, and decides what to include |
FieldEditabilityResolverInterface |
The first extension point — decides whether a given property should be in the form at all |
FormTypeGuesserInterface (Symfony's own) |
The second extension point — decides whether a Doctrine string field should use a more specific type than the generic TextType, based on validator constraints and/or naming convention |
DynamicEntityFormType itself has no knowledge of attributes, expressions, or permissions — every inclusion/exclusion decision beyond Doctrine's own structure (the identifier field, unsupported types) is delegated to the injected FieldEditabilityResolverInterface. The bundle ships a permissive default that includes everything; consumers override the service alias to plug in their own policy. See Editability.
Similarly, DoctrineFormTypeMapper has no hardcoded opinion about which string fields deserve a more specific widget beyond what Symfony's own form.type_guesser.validator already provides out of the box — see Type Guessing.
Supported Field Types
| Doctrine type | Symfony form type |
|---|---|
string, text |
TextType / TextareaType (upgraded automatically for some string fields — see Type Guessing) |
integer, smallint, bigint |
IntegerType |
decimal, float |
NumberType |
boolean |
CheckboxType |
date, date_immutable |
DateType (single_text) |
datetime, datetimetz, datetime_immutable, datetimetz_immutable |
DateTimeType (single_text) |
time, time_immutable |
TimeType (single_text) |
| Backed PHP enum | EnumType |
json, array, simple_array, object, blob, binary have no sensible form widget and are silently skipped. Nullability, empty_data, and required-field validation all have non-obvious behaviour driven by real Symfony transformer quirks — see Field Mapping for the full story.
Associations
| Doctrine type | Form field | UI |
|---|---|---|
ManyToOne, OneToOne (owning) |
EntityType |
Autocomplete dropdown |
ManyToMany (owning side) |
EntityType with multiple: true |
Autocomplete multi-select |
OneToMany |
LiveCollectionType with recursive DynamicEntityFormType |
Add / remove rows |
Inverse-side associations and parent back-references are skipped automatically to avoid confusing, redundant controls — with an opt-in escape hatch. Full detail, including the cascade/orphanRemoval/adder-remover requirements for OneToMany, is in Associations.
Controlling Field Inclusion
Every field/association inclusion decision — beyond the identifier field and unsupported Doctrine types, which are always handled internally — goes through one interface:
interface FieldEditabilityResolverInterface { public function canEdit(string $entityClass, string $property, ?object $entity = null): bool; public function isExplicitOverride(string $entityClass, string $property, ?object $entity = null): bool; }
The default binding, AlwaysEditableFieldResolver, includes everything unconditionally — matching the bundle's zero-config philosophy out of the box. Override the service alias in your own services.yaml to enforce a real policy (attribute-driven, role-driven, whatever fits your app). See Editability for the full contract and a worked example.
kachnitel/admin-bundle is a real-world consumer: it binds this interface to AdminColumnEditabilityResolver, which reads the #[AdminColumn(editable: ...)] attribute — bound via a compiler pass rather than a plain alias, since it needs its override to win regardless of bundle registration order. Worth a look if you're solving the same "my default has to beat a sibling package's default" problem — the exact same pattern is how it also opts into naming-convention type guessing; see Type Guessing.
Controlling Field Widgets
A Doctrine string field with a matching validator constraint (#[Assert\Email], #[Assert\Url], #[Assert\Country], ...) is upgraded from TextType to a more specific widget automatically, via Symfony's own form.type_guesser.validator. This bundle also ships an optional, opt-in naming-convention guesser (ConventionalFieldTypeGuesser, covering tel/color/search/email/url) for the cases no validator constraint can express. See Type Guessing for the full mechanism, how to enable naming-convention guessing, and how to write your own guesser.
Documentation
| Guide | Description |
|---|---|
| Field Mapping | Full Doctrine → Symfony type table, nullability rules, empty_data behaviour, RequiredValueTransformer |
| Associations | Collection handling, cascade/orphanRemoval requirements, auto-skip rules, recursion prevention, troubleshooting |
| Editability | The FieldEditabilityResolverInterface extension point, with a worked custom-resolver example |
| Type Guessing | Constraint-driven and naming-convention widget upgrades, the FormTypeGuesserInterface extension point, and the kachnitel/admin-bundle integration recipe |
Development
composer test # phpstan (level 10) + phpcs + phpunit + phpmd composer phpstan composer phpunit composer phpmd
Tests are organised into feature groups — run just what you're touching:
vendor/bin/phpunit --group auto-form vendor/bin/phpunit --group collections vendor/bin/phpunit --group dynamic-form vendor/bin/phpunit --group editability vendor/bin/phpunit --group form-transformers vendor/bin/phpunit --group form-exceptions vendor/bin/phpunit --group inline-add vendor/bin/phpunit --group type-guessing vendor/bin/phpunit --group integration
Requirements
- PHP 8.2+
- Symfony 6.4 / 7.0 / 8.0 —
doctrine-bridge,form,framework-bundle,validator - Doctrine ORM 3.5+,
doctrine/doctrine-bundle^3.0 - Symfony UX Live Component ^2.13 (for
OneToMany→LiveCollectionType) - Symfony UX Autocomplete ^3.0 (for association
EntityTypefields) - Symfony Intl (suggested; required only if any entity uses
#[Assert\Country],#[Assert\Language],#[Assert\Currency], or#[Assert\Locale]— see Type Guessing)
License
MPL-2.0 — see LICENSE.
Mozilla Public License 2.0 is a file-level copyleft: if you distribute a modified version of a file from this package, that specific file's source has to stay available under MPL-2.0. It does not require you to open source a larger application that merely depends on this package unmodified, and — unlike AGPL, this package's earlier license — it has no network-use clause, so running it as part of a SaaS/network service doesn't trigger anything extra. That's the compatibility reasoning behind the switch: it plays cleanly with kachnitel/admin-bundle's MIT license and with closed-source consuming applications generally. This isn't legal advice; get your own read on it if licensing terms matter for your project.