matthiasnoback/symfony-console-form
Symfony Console Form lets you reuse Symfony's Form component to build interactive console commands, rendering fields as questions and validating user input. It simplifies collecting multiple values with form types, constraints, and data mapping in CLI applications.
Install with Composer:
composer require matthiasnoback/symfony-console-form
In a Symfony application, register Matthias\SymfonyConsoleForm\Bundle\SymfonyConsoleFormBundle in config/bundles.php if your app does not auto-register bundles. In a standalone Console application, wire the Symfony Form factory and the package’s FormHelper manually.
Start with one small, flat form: a text field, a choice field, and a checkbox. Do not begin with nested forms or collections.
The first useful use case is an interactive admin/provisioning command where a developer would otherwise write a chain of $io->ask() calls:
final class RegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email', EmailType::class)
->add('role', ChoiceType::class, [
'choices' => [
'Administrator' => 'admin',
'Regular user' => 'user',
],
])
->add('sendWelcomeEmail', CheckboxType::class, [
'required' => false,
]);
}
}
use Matthias\SymfonyConsoleForm\Console\Command\FormBasedCommand;
final class RegisterUserCommand extends FormBasedCommand
{
protected function configure(): void
{
$this->setName('app:register-user');
}
protected function getFormType(): string
{
return RegistrationType::class;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$data = $this->getFormData();
// $data is an array unless the form defines a data_class.
// Persist the user, dispatch a message, etc.
return self::SUCCESS;
}
}
Look first at FormBasedCommand, FormHelper, SymfonyStyleFormatter, and the package tests. The tests are the most reliable documentation for supported field types and exact interaction behavior.
Prefer a DTO with data_class when the command has more than two or three fields. It gives execute() a typed object instead of an untyped array.
FormType between the web form and the console command. Constraints, choices, labels, and transformation logic then stay consistent. Put validation constraints on the DTO rather than duplicating them in the command.data_class and read typed properties/getters in execute():
protected function getFormOptions(): array
{
return [
'data_class' => RegistrationData::class,
'csrf_protection' => false,
];
}
getFormOptions() with a data object, or form field defaults, so the command can be used repeatedly with sensible defaults:
protected function getFormOptions(): array
{
return [
'data' => new RegistrationData(defaultRole: 'user'),
];
}
InputOptions for the critical fields and skip the form when those options are supplied. If the base class always prompts, either override interact() carefully or use the package’s FormHelper directly after checking $input->isInteractive() and the supplied options.ChoiceType with a curated list, not EntityType, unless you really need it. EntityType is web/Doctrine-oriented and can load too much data or behave unexpectedly in a console context. For selecting an existing entity, consider an ID argument plus a small confirmation form.FormBasedCommand. The package is essentially a bridge between Console questions and Symfony Form. If a command needs custom control flow — conditional prompts, retries, or non-interactive fallbacks — inject/use the FormHelper directly and call its form-asking method instead of forcing everything through the base command.SymfonyStyle, call the service, and render the result. This makes the workflow testable without simulating every console question.CommandTester::setInputs() for one happy-path command test, but do not make it the primary test strategy: question order is coupled to form field order and becomes brittle.SymfonyStyle after the form. The form handles structured input; SymfonyStyle is still the right tool for section output, confirmations, tables, and progress bars after submission.interact() is skipped with --no-interaction. Symfony Console does not call interact() when input is non-interactive. If FormBasedCommand collects data there, getFormData() may be empty or null in CI, cron, or deployment scripts. Always guard and provide an option-driven or argument-driven path.ChoiceType, and checkboxes are straightforward. CollectionType, RepeatedType, FileType, and EntityType often need custom handling or should be replaced with simpler command options.CommandTester::setInputs() feeds answers in the order questions are asked. Adding a field to the form can silently break tests. Prefer testing the execution service and using one narrow command test for smoke coverage.ChoiceType backed by thousands of entities is unusable. Use an ID option, a search step, or a small filtered list.-vvv when debugging exceptions. If the default output is insufficient, customize the question/formatter layer rather than catching and hiding errors.console.command in a Symfony app, and any form type that depends on services should itself be a service. Clear the cache after changing bundle or service configuration.composer why-not against your Symfony Console/Form versions before upgrading a major Symfony release, and read the package tests for supported behavior.getFormOptions(), form type extensions, a custom DTO, or a wrapper service around the form helper. Forking the package for one command usually creates more maintenance than writing a small console-specific input layer.How can I help you explore Laravel packages today?