Polymorphic Form Type
softspring/polymorphic-form-type solves one specific Symfony form problem: a collection where each item may use a different form type and a different PHP class.
Use it when one list can contain several node types, such as:
- product properties with different structures
- block or module editors
- rule builders
- workflows made of different step types
This component is the base building block that later packages such as cms-bundle reuse to build richer editors.
Installation
composer require softspring/polymorphic-form-type:^6.0
The Main Idea
Symfony CollectionType works well when every item uses the same form type.
This component is for the opposite case:
Entity form
-> properties: PolymorphicCollectionType
-> size node -> SizePropertyType
-> weight node -> WeightPropertyType
-> category node -> CategoryPropertyType
Each submitted item carries a discriminator field. The component reads that discriminator and decides:
- which child form type to build
- which PHP class to instantiate
- which prototype to expose to the frontend
The Two Collection Types
The package provides two collection types.
PolymorphicCollectionType
Use Softspring\Component\PolymorphicFormType\Form\Type\PolymorphicCollectionType when you want full control through explicit maps.
This is the right choice for:
- plain PHP objects
- arrays
- custom models not backed by Doctrine inheritance
DoctrinePolymorphicCollectionType
Use Softspring\Component\PolymorphicFormType\Form\Type\DoctrinePolymorphicCollectionType when the collection items are Doctrine entities in an inheritance hierarchy.
This variant resolves classes through Doctrine metadata and can reload existing entities by id during submit.
Create Node Types
Every node form type must extend:
Softspring\Component\PolymorphicFormType\Form\Type\Node\AbstractNodeType
That base class adds the internal hidden fields needed by the collection:
- the discriminator field,
_node_discrby default - the id field, when the Doctrine variant needs it
Your node type should focus on the real business fields:
use Softspring\Component\PolymorphicFormType\Form\Type\Node\AbstractNodeType;
use Symfony\Component\Form\FormBuilderInterface;
class SizePropertyType extends AbstractNodeType
{
protected function buildChildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('length');
$builder->add('width');
$builder->add('height');
}
}
Use PolymorphicCollectionType
The non-Doctrine collection type needs three pieces of information:
types_map: discriminator => form typediscriminator_map: discriminator => PHP classform_factory: the Symfony form factory
Example:
use App\Form\Type\CategoryPropertyType;
use App\Form\Type\SizePropertyType;
use App\Form\Type\WeightPropertyType;
use App\Model\Properties\Category;
use App\Model\Properties\Size;
use App\Model\Properties\Weight;
use Softspring\Component\PolymorphicFormType\Form\Type\PolymorphicCollectionType;
$builder->add('properties', PolymorphicCollectionType::class, [
'form_factory' => $options['form_factory'],
'types_map' => [
'size' => SizePropertyType::class,
'weight' => WeightPropertyType::class,
'category' => CategoryPropertyType::class,
],
'discriminator_map' => [
'size' => Size::class,
'weight' => Weight::class,
'category' => Category::class,
],
]);
What Happens On Submit
For each submitted node:
- the listener reads
_node_discr - the matching child form type is added
- the transformer creates the right object class
- the submitted fields are mapped back into that object
That means the collection can return a mixed list of objects after one normal form submit.
Use The Doctrine Variant
When nodes are Doctrine entities using inheritance, use DoctrinePolymorphicCollectionType.
In this case you configure:
abstract_classtypes_mapentity_manager
Example:
use App\Entity\Property\AbstractProperty;
use App\Form\Type\CategoryPropertyType;
use App\Form\Type\SizePropertyType;
use App\Form\Type\WeightPropertyType;
use Softspring\Component\PolymorphicFormType\Form\Type\DoctrinePolymorphicCollectionType;
$builder->add('properties', DoctrinePolymorphicCollectionType::class, [
'abstract_class' => AbstractProperty::class,
'entity_manager' => $entityManager,
'types_map' => [
'size' => SizePropertyType::class,
'weight' => WeightPropertyType::class,
'category' => CategoryPropertyType::class,
],
]);
The Doctrine variant uses _node_id by default so existing entities can be found again during submit.
Important Options
Common options:
types_maptypes_optionsdiscriminator_mapform_factorydiscriminator_fieldid_fieldallow_addallow_deleteprototype_name
Doctrine-only options:
abstract_classentity_manager
Use Types Options Per Node Type
types_options lets you pass different options to each node form type.
Example:
'types_options' => [
'size' => [
'prototype_button_label' => 'Add size field',
],
'weight' => [
'prototype_button_label' => 'Add weight field',
],
]
This is especially useful when:
- prototypes need different button labels
- one node type needs more options than another
- you want to pass UI metadata per discriminator
This is exactly the pattern used by cms-bundle to build module collections.
Prototypes And Frontend Integration
When allow_add and prototype are enabled, the collection view exposes one prototype per discriminator in form.vars.prototypes.
That gives the frontend enough information to render different “add node” buttons and insert the correct subform structure for each node type.
The package also ships a Twig theme:
@polymorphic-form-theme.html.twig
This theme renders:
- the collection wrapper
- one button per prototype
- the encoded prototype markup used by frontend add actions
How Other Packages Extend It
The best real example in this repository is cms-bundle.
ModuleCollectionType extends PolymorphicCollectionType and adds:
- module-specific discriminator maps
- grouped prototypes
- translated prototype button labels
- per-module options
- custom data mapping
That shows the intended extension pattern:
- keep the base polymorphic mechanics here
- build domain-specific collection types on top
Recommended Usage Patterns
This component is a good fit when:
- each item type has different fields
- you want one collection field in one form
- the discriminator is stable and explicit
- your application already has some frontend logic to add items dynamically
Good examples:
- content blocks
- rule nodes
- heterogeneous settings lists
- configurable product properties
Limits And Current Notes
A few limits matter in practice:
- the plain variant needs both explicit class and form type maps
- the Doctrine variant only supports one identifier field
- the component resolves backend form structure, but your application still needs frontend behavior to insert prototypes dynamically
- every node type must extend
AbstractNodeType, otherwise the internal discriminator and id workflow is not present