Introducing the SymfonyConsoleForm package

Posted on by Matthias Noback

About 2 years ago I created a package that combines the power of two famous Symfony components: the Form component and the Console component. In short: this package allows you to interactively fill in a form by typing in the answers at the CLI. When I started working on it, this seemed like a pretty far-fetched idea. However, it made a lot of sense to me in terms of a ports & adapters architecture that I was looking for back then (and still always am, by the way). We could (and often should) write the code in our application layer in such a way that it doesn't make a big difference whether we call applications services from a web controller or from a CLI "controller".

As described by Bernhard Sch├╝ssek (author of the Symfony Form component) in his article Symfony2 Form Architecture, the Form component strictly separates model and view responsibilities. It also makes a clear distinction between processing a form and rendering it (e.g. in an HTML template). If you're familiar with Symfony forms, you already know this. You first define the structure and behavior of a form, then you convert it to a view (which is basically a DTO). This simple data structure is used to render HTML.

This strict separation of concerns has brought us a well-designed, yet fairly complicated form component. In my quest for "console forms", this was a great gift: it was actually quite easy to render form fields to the terminal output, instead of to an HTTP response.

I decided to rely on the existing Question Helper which allows console commands to ask questions to the user. This basically left me with the need for "bridging the gap" between the Form and Console component. This is a quick list of things I needed to fix:

  • Different form fields require different console question types. A choice type form field matches more or less with a ChoiceQuestion. But since the mapping isn't one-to-one, I introduced the concept of a FormToQuestionResolver which configures a Question object in such a way that the user experience matches to that of a web form field. For example: a password type form field gets transformed into a Question with hidden output.
  • It's relatively easy to ask single questions (e.g. "Your name: "), but it was a bit harder to ask for "collections", like "Phone number 1: ", "Phone number 2: ", etc.). I fixed this by introducing something called a FormInteractor which gets asked to arrange any number of user interactions that are required to fill in the form.
  • CSRF tokens aren't needed for console commands, so CSRF protection will automatically be disabled.
  • In some places this package relies on behaviors of the Form and Console component that are not covered by the Symfony Backwards Compatibility Promise. This means that for every minor release the package is bound to break for some (often obscure) reason. I have kept up with Symfony 2.8, 3.0, 3.1 and 3.2, but if things get too hard to maintain, I will consider dropping some versions.

A short list of changes in external libraries that have been causing trouble so far (I'm by no means criticizing the Symfony development team for this, it just might be interesting to share this with you):

  • The behavior of a ChoiceQuestion changed at some point, switching what was returned as the selected value (instead of the selected "key" at some point it started returning the selected "label" of a choice). See also my custom solution, AlwaysReturnKeyOfChoiceQuestion. Furthermore, around the same time the Form component switched the behavior of its ChoiceType form type, accepting an array of "label" => "data" value pairs, where I was used to providing the exact opposite (an array of "data" => "label" pairs).
  • Of course, there was the major overhaul of the form type system, where form types changed to being fully-qualified class names instead of simple names. I wanted to keep supporting both styles, so this took some time to get right. At some point, this became simply too complex to maintain, so I dropped support for old-style form types.

Usage

Follow the instructions from the README of the project to install the package and register it in your project as a Symfony bundle. Then define a Symfony form type like this:

class DemoType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add(
                'name',
                'text',
                [
                    'label' => 'Your name',
                    'required' => true,
                    'data' => 'Matthias'
                ]
            )
            // maybe add some more fields
            ...
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'data_class' => 'Some\Namespace\Demo',
            ]
        );
    }
}

Now create a data class for your form, like this:

class Demo
{
    public $name;

    // add some more properties to match the form fields
    ...
}

Finally, create a console command, like this one:

class DemoCommand extends Command
{
    protected function configure()
    {
        $this->setName('form:demo');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        /** @var FormHelper $formHelper */

        $formHelper = $this->getHelper('form');

        $formData = $formHelper->interactUsingForm(new DemoType(), $input, $output);

        // $formData is the valid and populated form data object
        ...
    }
}

Register it as a console command and run it: bin/console form:demo. The output will look something like this (provided you've also added an email field an a country choice field):

Example of form interaction

Findings

Although it took some fiddling, along the way I found out that the Form component is in fact very suitable for types of input/output other than just plain old HTTP requests/responses.

I also found out that it was impossible (and still is, I think) to globally register:

  • styles for formatting console messages,
  • command helpers (like the question helper).

So I created event listeners and compiler passes to accomplish this. These may deserve their own bundle at some point. Feel free to create it, based on the code from this package.

Use cases

The main use case for me so far was to demonstrate delivery-mechanism-agnosticness of application services in my workshops. But I've heard of projects adopting this package to allow installation wizards to be used from the CLI as well as the web.

This package might make it easier for you to get started with interactive console commands if you already know how to work with forms - in that case, you don't need to learn anything new.

Anyway, I'd love to hear what you're doing with it!

PHP Symfony Symfony forms console