# Symfony Form Component The Symfony Form component is a powerful PHP library for creating, processing, and validating HTML forms in web applications. It provides a complete form handling solution with support for complex form structures, data transformation between different representations (model, normalized, and view), validation integration with the Symfony Validator component, and flexible rendering through a view system. The component follows an object-oriented architecture with form types, builders, and factories that enable both simple forms and sophisticated multi-step wizards. At its core, the component uses a three-layer data transformation pipeline: model data (your domain objects), normalized data (internal processing format), and view data (HTML-ready strings/arrays). This architecture allows seamless handling of various data types while maintaining a clean separation between your business logic and presentation layer. The component includes 30+ built-in form types, an event system for dynamic form modification, CSRF protection, and extensibility through custom types and extensions. ## Forms::createFormFactory Creates a form factory with default configuration. This is the main entry point for using the Form component standalone. ```php createBuilder() ->add('firstName', TextType::class) ->add('lastName', TextType::class) ->add('age', IntegerType::class) ->add('color', ChoiceType::class, [ 'choices' => ['Red' => 'r', 'Blue' => 'b', 'Green' => 'g'], ]) ->getForm(); // Submit data to the form $form->submit([ 'firstName' => 'John', 'lastName' => 'Doe', 'age' => 30, 'color' => 'b', ]); // Check validity and retrieve data if ($form->isSubmitted() && $form->isValid()) { $data = $form->getData(); // ['firstName' => 'John', 'lastName' => 'Doe', 'age' => 30, 'color' => 'b'] } ``` ## Forms::createFormFactoryBuilder Creates a form factory builder for custom configuration with extensions, custom types, and validators. ```php addExtension(new ValidatorExtension($validator)) ->getFormFactory(); // You can also add custom types and type extensions $formFactory = Forms::createFormFactoryBuilder() ->addType(new CustomType()) ->addTypeExtension(new CustomTypeExtension()) ->getFormFactory(); ``` ## FormFactoryInterface::create Creates a form directly from a type class. Use this when you don't need to customize the form builder. ```php create(TextType::class, 'default value', [ 'required' => true, 'attr' => ['maxlength' => 100], ]); // Create a choice field with options $choiceForm = $formFactory->create(ChoiceType::class, null, [ 'choices' => [ 'Yes' => true, 'No' => false, 'N/A' => null, ], 'placeholder' => 'Choose an option', ]); $choiceForm->submit('1'); // Submit 'true' $data = $choiceForm->getData(); // true (boolean) ``` ## FormFactoryInterface::createNamed Creates a named form, useful when you need to control the form's name attribute in HTML. ```php createNamed('contact', FormType::class) ->add('name', TextType::class) ->add('email', EmailType::class); // HTML fields will be named: contact[name], contact[email] // Submit with the correct structure $form->submit([ 'name' => 'Jane Doe', 'email' => 'jane@example.com', ]); echo $form->isValid(); // true ``` ## FormFactoryInterface::createBuilder Creates a form builder for fluent form construction with full control over fields, transformers, and events. ```php createBuilder(FormType::class, null, [ 'method' => 'POST', 'action' => '/login', ]); $builder ->add('username', TextType::class, [ 'required' => true, 'label' => 'Username', 'attr' => ['placeholder' => 'Enter username'], ]) ->add('password', PasswordType::class, [ 'required' => true, 'label' => 'Password', ]) ->add('submit', SubmitType::class, [ 'label' => 'Login', ]); // Build the form $form = $builder->getForm(); // Access child builders before building $usernameBuilder = $builder->get('username'); $builder->remove('submit'); // Remove a field $builder->has('username'); // true ``` ## FormBuilderInterface::add Adds a child field to a form builder. Accepts field name, type, and options. ```php createBuilder(FormType::class); // Add various field types with options $builder ->add('title', TextType::class, [ 'required' => true, 'trim' => true, 'attr' => ['class' => 'form-control'], ]) ->add('publishDate', DateType::class, [ 'widget' => 'single_text', 'input' => 'datetime', 'format' => 'yyyy-MM-dd', ]) ->add('price', MoneyType::class, [ 'currency' => 'USD', 'scale' => 2, ]) ->add('category', ChoiceType::class, [ 'choices' => [ 'Technology' => 'tech', 'Science' => 'science', 'Arts' => 'arts', ], 'expanded' => false, // dropdown 'multiple' => false, // single selection ]); $form = $builder->getForm(); ``` ## FormInterface::submit Submits data to the form, triggering validation and data transformation. ```php createBuilder(FormType::class) ->add('firstName', TextType::class) ->add('lastName', TextType::class) ->getForm(); // Full submission - clears missing fields $form->submit([ 'firstName' => 'John', 'lastName' => 'Doe', ]); // Partial submission - preserves missing fields (PATCH-like behavior) $form2 = $formFactory->createBuilder(FormType::class) ->add('firstName', TextType::class) ->add('lastName', TextType::class) ->getForm(); $form2->setData(['firstName' => 'Original', 'lastName' => 'Name']); $form2->submit(['firstName' => 'Updated'], false); // clearMissing = false echo $form2->getData()['firstName']; // 'Updated' echo $form2->getData()['lastName']; // 'Name' (preserved) // Check submission state echo $form->isSubmitted(); // true echo $form->isValid(); // true (if no validation errors) ``` ## FormInterface::getData / getNormData / getViewData Retrieves form data in different representations along the transformation pipeline. ```php create(DateType::class, null, [ 'widget' => 'single_text', 'input' => 'datetime', ]); $dateForm->submit('2024-01-15'); // getData() - Model data (what your application works with) $modelData = $dateForm->getData(); // DateTime object: 2024-01-15 // getNormData() - Normalized data (internal processing format) $normData = $dateForm->getNormData(); // DateTime object (same as model for dates) // getViewData() - View data (what appears in HTML) $viewData = $dateForm->getViewData(); // String: '2024-01-15' // Money example $moneyForm = $formFactory->create(MoneyType::class, null, [ 'currency' => 'EUR', ]); $moneyForm->submit('1234.56'); echo $moneyForm->getData(); // float: 1234.56 (model) echo $moneyForm->getViewData(); // string: '1234.56' (view) ``` ## FormInterface::getErrors Retrieves validation errors from a form, with options for deep traversal and flattening. ```php createBuilder(FormType::class) ->add('name', TextType::class) ->add('email', EmailType::class) ->getForm(); $form->submit(['name' => '', 'email' => 'invalid']); // Add errors manually (normally added by validator) $form->get('name')->addError(new FormError('Name is required')); $form->get('email')->addError(new FormError('Invalid email format')); $form->addError(new FormError('Form-level error')); // Get errors for current form only (not children) $formErrors = $form->getErrors(); foreach ($formErrors as $error) { echo $error->getMessage(); // 'Form-level error' } // Get all errors including children (deep) $allErrors = $form->getErrors(true); foreach ($allErrors as $error) { echo $error->getMessage(); // 'Form-level error', 'Name is required', 'Invalid email format' } // Get all errors flattened (deep + flatten) $flatErrors = $form->getErrors(true, true); foreach ($flatErrors as $error) { echo $error->getOrigin()->getName() . ': ' . $error->getMessage(); } ``` ## FormInterface::createView Creates a FormView object for rendering the form in templates. ```php createBuilder(FormType::class) ->add('name', TextType::class, ['label' => 'Full Name']) ->add('status', ChoiceType::class, [ 'choices' => ['Active' => 'active', 'Inactive' => 'inactive'], ]) ->getForm(); $form->submit(['name' => 'John Doe', 'status' => 'active']); // Create the view for rendering $view = $form->createView(); // Access view variables echo $view->vars['id']; // Form ID for HTML echo $view->vars['name']; // Form name echo $view->vars['full_name'];// Full HTML name attribute echo $view->vars['value']; // Current value echo $view->vars['method']; // HTTP method (POST) echo $view->vars['action']; // Form action URL echo $view->vars['valid']; // Validation status // Access child views $nameView = $view['name']; echo $nameView->vars['label']; // 'Full Name' echo $nameView->vars['value']; // 'John Doe' echo $nameView->vars['required']; // true echo $nameView->vars['id']; // HTML id attribute // Iterate children foreach ($view as $childView) { echo $childView->vars['name']; } ``` ## ChoiceType Renders select dropdowns, radio buttons, or checkboxes depending on options. ```php create(ChoiceType::class, null, [ 'choices' => [ 'Small' => 's', 'Medium' => 'm', 'Large' => 'l', ], 'placeholder' => 'Select size', ]); // Radio buttons (expanded + single) $radios = $formFactory->create(ChoiceType::class, null, [ 'choices' => [ 'Option A' => 'a', 'Option B' => 'b', 'Option C' => 'c', ], 'expanded' => true, 'multiple' => false, ]); // Checkboxes (expanded + multiple) $checkboxes = $formFactory->create(ChoiceType::class, null, [ 'choices' => [ 'Red' => 'red', 'Green' => 'green', 'Blue' => 'blue', ], 'expanded' => true, 'multiple' => true, ]); // Grouped choices $grouped = $formFactory->create(ChoiceType::class, null, [ 'choices' => [ 'Fruits' => [ 'Apple' => 'apple', 'Banana' => 'banana', ], 'Vegetables' => [ 'Carrot' => 'carrot', 'Lettuce' => 'lettuce', ], ], ]); // Multi-select dropdown $multiSelect = $formFactory->create(ChoiceType::class, null, [ 'choices' => ['A' => 'a', 'B' => 'b', 'C' => 'c'], 'multiple' => true, 'expanded' => false, ]); $multiSelect->submit(['a', 'c']); $data = $multiSelect->getData(); // ['a', 'c'] // With preferred choices (shown first) $preferred = $formFactory->create(ChoiceType::class, null, [ 'choices' => [ 'United States' => 'us', 'Canada' => 'ca', 'France' => 'fr', 'Germany' => 'de', 'Japan' => 'jp', ], 'preferred_choices' => ['us', 'ca'], ]); ``` ## CollectionType Manages collections of repeated form elements with add/remove capabilities. ```php createBuilder(FormType::class) ->add('emails', CollectionType::class, [ 'entry_type' => EmailType::class, 'entry_options' => ['label' => false], 'allow_add' => true, 'allow_delete' => true, 'prototype' => true, 'prototype_name' => '__email__', ]) ->getForm(); // Set initial data $form->setData([ 'emails' => ['john@example.com', 'jane@example.com'], ]); // Submit with modified collection $form->submit([ 'emails' => [ 'john@example.com', // kept 'new@example.com', // added // jane@example.com removed ], ]); $data = $form->getData(); // ['emails' => ['john@example.com', 'new@example.com']] // Collection with compound entry type $builder = $formFactory->createBuilder(FormType::class) ->add('contacts', CollectionType::class, [ 'entry_type' => FormType::class, 'entry_options' => [ 'data_class' => null, ], 'allow_add' => true, 'prototype' => true, 'delete_empty' => true, // Auto-delete empty entries ]); // Add fields to collection entry via event listener $builder->get('contacts')->addEventListener( \Symfony\Component\Form\FormEvents::POST_SET_DATA, function ($event) { $form = $event->getForm(); foreach ($form as $entry) { $entry->add('name', TextType::class); $entry->add('email', EmailType::class); } } ); ``` ## DateType and DateTimeType Date and datetime pickers with various widget and input format options. ```php create(DateType::class, null, [ 'widget' => 'single_text', 'html5' => true, 'input' => 'datetime', // Returns DateTime object ]); $html5Date->submit('2024-06-15'); $date = $html5Date->getData(); // DateTime object // Date with select dropdowns $selectDate = $formFactory->create(DateType::class, null, [ 'widget' => 'choice', 'years' => range(date('Y') - 100, date('Y')), 'format' => 'dd-MM-yyyy', ]); // DateTime with various options $dateTime = $formFactory->create(DateTimeType::class, null, [ 'widget' => 'single_text', 'input' => 'datetime_immutable', // Returns DateTimeImmutable 'with_seconds' => false, 'model_timezone' => 'UTC', 'view_timezone' => 'America/New_York', ]); // Time only $time = $formFactory->create(TimeType::class, null, [ 'widget' => 'single_text', 'input' => 'string', 'input_format' => 'H:i', 'with_seconds' => false, ]); $time->submit('14:30'); echo $time->getData(); // '14:30' // Date with string input $stringDate = $formFactory->create(DateType::class, null, [ 'widget' => 'single_text', 'input' => 'string', 'input_format' => 'Y-m-d', ]); $stringDate->submit('2024-06-15'); echo $stringDate->getData(); // '2024-06-15' (string) ``` ## DataTransformerInterface and CallbackTransformer Transform data between model, normalized, and view representations. ```php createBuilder(TextType::class); $builder->addViewTransformer(new CallbackTransformer( // transform: model -> view (for display) function ($value) { return $value ? strtoupper($value) : ''; }, // reverseTransform: view -> model (on submit) function ($value) { return $value ? strtolower($value) : ''; } )); $form = $builder->getForm(); $form->setData('hello'); $view = $form->createView(); echo $view->vars['value']; // 'HELLO' $form->submit('WORLD'); echo $form->getData(); // 'world' // Custom DataTransformer class class JsonToArrayTransformer implements DataTransformerInterface { public function transform(mixed $value): mixed { if (null === $value) { return ''; } return json_encode($value, JSON_PRETTY_PRINT); } public function reverseTransform(mixed $value): mixed { if (empty($value)) { return []; } $decoded = json_decode($value, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new TransformationFailedException('Invalid JSON'); } return $decoded; } } $jsonBuilder = $formFactory->createBuilder(TextType::class); $jsonBuilder->addViewTransformer(new JsonToArrayTransformer()); $jsonForm = $jsonBuilder->getForm(); $jsonForm->setData(['name' => 'John', 'age' => 30]); // View shows: {"name": "John", "age": 30} $jsonForm->submit('{"city": "NYC"}'); $data = $jsonForm->getData(); // ['city' => 'NYC'] ``` ## FormEvents and Event Listeners Modify forms dynamically using the event system during various lifecycle stages. ```php createBuilder(FormType::class); $builder->add('country', ChoiceType::class, [ 'choices' => [ 'United States' => 'us', 'Canada' => 'ca', 'Other' => 'other', ], ]); // PRE_SET_DATA: Modify form based on initial data $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); // Add fields based on existing data if ($data && isset($data['country']) && $data['country'] === 'us') { $form->add('state', ChoiceType::class, [ 'choices' => [ 'California' => 'CA', 'New York' => 'NY', 'Texas' => 'TX', ], ]); } }); // PRE_SUBMIT: Modify submitted data or form structure $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); // Normalize email to lowercase if (isset($data['email'])) { $data['email'] = strtolower($data['email']); $event->setData($data); } // Dynamically add state field if US selected if (isset($data['country']) && $data['country'] === 'us') { $form->add('state', ChoiceType::class, [ 'choices' => ['CA' => 'CA', 'NY' => 'NY', 'TX' => 'TX'], ]); } }); // POST_SUBMIT: Access final data after transformation $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) { $form = $event->getForm(); if ($form->isValid()) { $data = $form->getData(); // Process valid data } }); $builder->add('email', TextType::class); $form = $builder->getForm(); $form->submit([ 'country' => 'us', 'email' => 'JOHN@EXAMPLE.COM', 'state' => 'CA', ]); echo $form->getData()['email']; // 'john@example.com' (normalized) ``` ## AbstractType - Custom Form Types Create reusable custom form types by extending AbstractType. ```php add('street', TextType::class, [ 'required' => true, ]) ->add('city', TextType::class, [ 'required' => true, ]) ->add('postalCode', TextType::class, [ 'required' => true, ]) ->add('country', ChoiceType::class, [ 'choices' => $options['countries'], ]); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'countries' => [ 'United States' => 'US', 'Canada' => 'CA', ], ]); } } class PersonType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('firstName', TextType::class) ->add('lastName', TextType::class) ->add('email', EmailType::class) ->add('phone', TelType::class, ['required' => false]) ->add('address', AddressType::class, [ 'countries' => $options['available_countries'], ]); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => null, // Or your Person entity class 'available_countries' => [ 'US' => 'US', 'CA' => 'CA', 'UK' => 'UK', ], ]); } } // Register and use custom types $formFactory = Forms::createFormFactoryBuilder() ->addType(new AddressType()) ->addType(new PersonType()) ->getFormFactory(); $form = $formFactory->create(PersonType::class, null, [ 'available_countries' => ['USA' => 'US', 'Mexico' => 'MX'], ]); $form->submit([ 'firstName' => 'John', 'lastName' => 'Doe', 'email' => 'john@example.com', 'phone' => '+1-555-0123', 'address' => [ 'street' => '123 Main St', 'city' => 'Anytown', 'postalCode' => '12345', 'country' => 'US', ], ]); $data = $form->getData(); // Nested array with all form data ``` ## Data Binding with Objects Bind forms to entity/model objects with automatic property mapping. ```php firstName; } public function setFirstName(string $value): void { $this->firstName = $value; } public function getLastName(): string { return $this->lastName; } public function setLastName(string $value): void { $this->lastName = $value; } public function getEmail(): string { return $this->email; } public function setEmail(string $value): void { $this->email = $value; } public function getAge(): int { return $this->age; } public function setAge(int $value): void { $this->age = $value; } } $formFactory = Forms::createFormFactory(); // Create form bound to User class $form = $formFactory->createBuilder(FormType::class, null, [ 'data_class' => User::class, ]) ->add('firstName', TextType::class) ->add('lastName', TextType::class) ->add('email', EmailType::class) ->add('age', IntegerType::class) ->getForm(); // Submit data - creates User object automatically $form->submit([ 'firstName' => 'Alice', 'lastName' => 'Smith', 'email' => 'alice@example.com', 'age' => 28, ]); $user = $form->getData(); // User object with all properties set echo $user->getFirstName(); // 'Alice' echo $user->getEmail(); // 'alice@example.com' // Edit existing object $existingUser = new User(); $existingUser->setFirstName('Bob'); $existingUser->setEmail('bob@example.com'); $editForm = $formFactory->createBuilder(FormType::class, $existingUser, [ 'data_class' => User::class, ]) ->add('firstName', TextType::class) ->add('email', EmailType::class) ->getForm(); // Form is pre-populated with existing data $view = $editForm->createView(); echo $view['firstName']->vars['value']; // 'Bob' // Submit updates the existing object $editForm->submit(['firstName' => 'Robert', 'email' => 'robert@example.com']); echo $existingUser->getFirstName(); // 'Robert' (same object, updated) ``` ## FormTypeExtensionInterface Extend existing form types to add functionality across all forms. ```php setDefaults([ 'help_text' => null, 'help_html' => false, ]); } public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['help_text'] = $options['help_text']; $view->vars['help_html'] = $options['help_html']; } } class PlaceholderExtension extends AbstractTypeExtension { public static function getExtendedTypes(): iterable { return [FormType::class]; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'auto_placeholder' => false, ]); } public function buildForm(FormBuilderInterface $builder, array $options): void { if ($options['auto_placeholder']) { $currentAttr = $builder->getOption('attr') ?? []; if (!isset($currentAttr['placeholder'])) { $currentAttr['placeholder'] = $builder->getOption('label') ?? ucfirst($builder->getName()); $builder->setAttributes(['attr' => $currentAttr]); } } } } // Register extensions $formFactory = Forms::createFormFactoryBuilder() ->addTypeExtension(new HelpTextExtension()) ->getFormFactory(); // All forms now support help_text option $form = $formFactory->createBuilder() ->add('username', \Symfony\Component\Form\Extension\Core\Type\TextType::class, [ 'help_text' => 'Enter your username (3-20 characters)', 'help_html' => false, ]) ->getForm(); $view = $form->createView(); echo $view['username']->vars['help_text']; // 'Enter your username...' ``` ## Form Flows (Multi-step Forms) Create multi-step wizard forms using FormFlow (Symfony 7.4+). ```php [ 'label' => 'Account Information', ], 'profile' => [ 'label' => 'Profile Details', ], 'confirmation' => [ 'label' => 'Confirm & Submit', ], ]; } public function buildForm(FormBuilderInterface $builder, array $options): void { $step = $options['flow_step']; switch ($step) { case 'account': $builder ->add('email', EmailType::class) ->add('username', TextType::class); break; case 'profile': $builder ->add('firstName', TextType::class) ->add('lastName', TextType::class) ->add('bio', TextareaType::class, ['required' => false]); break; case 'confirmation': // Review step - read-only or confirmation checkbox $builder->add('acceptTerms', ChoiceType::class, [ 'choices' => ['I accept the terms' => true], 'expanded' => true, ]); break; } } } // Usage (conceptual - requires full Symfony integration) // $flow = $formFactory->create(RegistrationFlowType::class); // $flow->newStepForm(); // // if ($flow->isSubmitted() && $flow->isValid()) { // if ($flow->isFinished()) { // // All steps complete, save data // $data = $flow->getData(); // } else { // $flow->moveNext(); // } // } ``` ## Choice Loaders for Dynamic Choices Load choices dynamically from databases or external sources. ```php create(ChoiceType::class, null, [ 'choice_loader' => new CallbackChoiceLoader(function () { // Simulate database query return [ 'admin' => 'Administrator', 'user' => 'Regular User', 'guest' => 'Guest', ]; }), 'choice_label' => function ($value) { return $value; // The label is the value from our array }, ]); // Custom choice loader with value extraction class CategoryChoiceLoader extends AbstractChoiceLoader { private array $categories; public function __construct() { // Simulate loading from database $this->categories = [ ['id' => 1, 'name' => 'Electronics'], ['id' => 2, 'name' => 'Clothing'], ['id' => 3, 'name' => 'Books'], ]; } protected function loadChoices(): array { return $this->categories; } } $categoryForm = $formFactory->create(ChoiceType::class, null, [ 'choice_loader' => new CategoryChoiceLoader(), 'choice_value' => function ($category) { return $category ? $category['id'] : ''; }, 'choice_label' => function ($category) { return $category['name']; }, ]); // Lazy choice loader (loads only when needed) $lazyForm = $formFactory->create(ChoiceType::class, null, [ 'choice_loader' => new CallbackChoiceLoader(function () { // Heavy operation - only called when rendering or validating sleep(0); // Simulated delay return ['opt1' => 'Option 1', 'opt2' => 'Option 2']; }), 'choice_lazy' => true, // Symfony 7.2+ ]); ``` The Symfony Form component excels at building complex, data-bound forms with robust validation and transformation pipelines. Common use cases include user registration forms, multi-step wizards, CRUD interfaces for entities, dynamic forms that change based on user input, and complex data entry systems with nested objects and collections. The component integrates seamlessly with Symfony's Validator component for constraint-based validation and with Twig for powerful form rendering with customizable themes. For integration, the recommended approach is to use the Forms::createFormFactory() entry point for standalone usage, or leverage Symfony's dependency injection container in full-stack applications. Custom form types should extend AbstractType and be registered as services. Data transformers handle the conversion between your domain objects and HTML-friendly representations, while event listeners enable dynamic form modification. The FormView system provides all necessary data for rendering in any templating engine, with first-class Twig support through form themes and helper functions.