kachnitel/dynamic-form-bundle

Zero-configuration Symfony form generation from Doctrine entity metadata

Maintainers

Package info

github.com/kachnitel/dynamic-form-bundle

Type:symfony-bundle

pkg:composer/kachnitel/dynamic-form-bundle

Transparency log

Statistics

Installs: 51

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

0.0.2 2026-07-20 05:42 UTC

This package is auto-updated.

Last update: 2026-07-20 05:44:20 UTC


README

Tests Coverage Assertions PHPStan PHP Symfony

Doctrine ORM PHPStan License

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 OneToManyLiveCollectionType)
  • Symfony UX Autocomplete ^3.0 (for association EntityType fields)
  • 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.