This article covers how to perform basic tasks related to the Symfony Forms component.

Almost everything in Sylius depends on the forms in various ways — ResourceController, adequate to route configuration, decides which one should be used under a particular path.

Forms are based on Type classes which define what fields are bounded to a particular form. It also may specify what variables are sent to the renderer.

Forms could be also extended by Extensions. They provide similar methods to Types but you can alter any existing form, even from a different bundle.

Templates are not required thing to be present in order to render any form. They’re — mostly — template-agnostic, so any abstract template may just render a form without being aware which was requested.

But in Sylius, we mostly operate on pair type+template for the majority of URLs. To begin with any customization, we need to find what template is related to the URL. Suppose, we want to customize a user profile form.

Finding out the template by URL

Typical Sylius URL — web profiler

Open any page you want to get a template for. Look for the Symfony profiler toolbar at the bottom and click on the Twig icon:

On a newly opened page, you should see a table with a list of all rendered templates. The topmost is the one we’re interested in, in terms of further customization.

Typical Sylius URL — console

Sometimes a profiler is not available or difficult to enable. Also, we operate on an URL. Suppose, it ends with /pl_PL/account/profile/edit. Now, we have to find a correct Symfony route.

We assume you have an access to CLI of working project.

Once you’re in application directory, issue command:

php bin/console debug:route | grep /pl_PL/account/profile/edit

You’ll get something similar to:

sylius_shop_account_profile_update GET|PUT ANY ANY /{_locale}/account/profile/edit

Note the sylius_shop_account_profile_update and re-issue command:

php bin/console debug:route sylius_shop_account_profile_update

And you end up with:

| _sylius: array ('template' => '@SyliusShop/Account/profileUpdate.html.twig','form' => 'Sylius\\Bundle\\CustomerBundle\\Form\\Type\\CustomerProfileType','repository' => array ('method' => 'find','arguments' => array (0 => 'expr:service(\'sylius.context.customer\').getCustomer()',),),'redirect' => array ('route' => 'sylius_shop_account_profile_update','parameters' => array (),),) |

A template key contains a path in the format bundle/path/template.html.twig. Having this, we’re on a good way to figuring out where the template is. If you have a PHPStorm/Webstorm, you can simply tap the twice Shift key with the template name, eg. shift, shift, profileUpdate.html.twig. If you use another IDE/editor, you need to go deeper.

Another way to get a template name

If key _sylius is absent, we have to figure out what controller renders a view. Look for _controller key under Defaults section. Any route within Symfony requires this key to be present.

You’ll end up with a value similar to sylius.controller.customer:updateAction. We have to break our investigation into two pieces: finding the controller and finding action. Once you issue:

php bin/console debug:container sylius.controller.customer

find a Class key and head up to a particular file. Path resolving is similar to one described in the previous section. Once you open that .php file, find a method after the colon, where we rely on updateAction.

Look for a line similar to $this->templating->render('template.html.twig) and then you’ll find the main template.

This method shouldn’t be necessary in a majority of cases but „just in case”.

Accessing a template file

Sylius bundles utilize a pre-defined structure of directories. They follow-up convention vendor/sylius/src/<name>Bundle. We’re talking about SyliusShop, so its files should be under /vendor/sylius/src/Bundle/SyliusShopBundle. Once we got into the directory, we should look for Resources/views or Resources/templates. There should be your template.

Now, we’re on the main template entry point, now…

Overriding templates

First, identify what part of the application you want to customize. A profiler method mentioned above may point you at a particular template including you need to override/customize.

Sylius template events

The simplest way to inject anything into the Sylius template is to utilize template events. If your application works under the dev environment (you can spot a profiler toolbar), head into the markup inspector and find a comment nearest to the location of a placeholder which you want to inject custom content:

If an event has been already used, you’ll find a template path to override (see: below). But if you want to append something new, for example, add a panel before the menu is shown, we need to hook a block event.

We have an event: sylius.shop.account.layout.menu and block name menu.

Create a template in your application, eg.

sylius_ui:
    events:
        sylius.shop.account.layout.menu:
            blocks:
                menu_vader:
                    template: 'blocks/account_vader.html.twig'
                    priority: 11

The higher priority is, the sooner a block is being rendered. Using an already existing block name will replace the pre-defined block with your template. The configuration above should produce something like this.

This is also the simplest way to append anything to your forms without a need to override a template; for example, we inject some disclaimer:

We just added a block to a form event:

sylius_ui:
    events:
        sylius.shop.account.profile.update.form:
            blocks:
                disclaimer:
                    template: 'blocks/account_vader.html.twig'
                    priority: 11

This is a preferred way of customization, especially for plug-ins because leverages the amount of instructions in README to be done by the end-user.

Basic template override

Any template within Symfony can be customized by override. Simply copy found a template to templates/bundles/BundleName/path/template.html.twig and customize to your own needs.

Themes

Sometimes you want to globally modify forms widgets, without a need to alter a particular form type. Simply apply a form theme. It’s described pretty well in the documentation.

Templates customization

And this is the place we want to really dig into. First, get familiar to form rendering customization documentation. It describes what you can do with views. Let’s focus on the profile update screen. Open _profileUpdate.html.twig file. Then you will find lines with something like:

{{ form_row(form.phoneNumber) }}

As you can notice in the rendering section of docs, it renders form label, error messages, widgets and so on. Mostly, you’ll probably want to adjust some attributes on form input, so this is how it should be accomplished:1

{{ form_label(form.fieldName) }}{{ form_widget(form.fieldName, {'attr': {'class': 'task_field'}}) }}

Remember the attributes defined in the template are final — what you override is what you’ll get within the rendering process.

Overriding things from PHP Type classes

Any attribute we discussed above can be adjusted from the form class. We have to distinguish what types we’re customizing: an application form or plug-in. The further decreases amount of work to customize a Type, but makes changes less re-usable. Consult your team members on which overriding method should be used for your work.

Before we dig into Type customization, one important thing has to be continuously kept in mind: once a field is added to form, it cannot be altered. So if we have:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('holder', TextType::class, array('label' => 'form.credit_card.holder'))
        ->add('number', TextType::class, array('label' => 'form.credit_card.number'))
        ->add('securityCode', TextType::class, array('label' => 'form.credit_card.security_code'))
        ->add(
            'expireAt',
            CreditCardExpirationDateType::class,
            array(
                'input' => 'datetime',
                'widget' =>'choice',
                'label' => 'form.credit_card.expire_at',
            )
        );
}

it’s impossible to get a field and modify any option further. Having this memorized will significantly reduce the amount of work/looking for assistance.

Decoration

The simplest thing to do is — directly edit the values above. But sometimes you need to alter a 3rd-party plug-in form type in your application. There’s an option to copy a class from the vendor and override the type:

app.form.some_form:
   class: My\Form\Type
   decorates: vendor.form.some_form
   tags: 
       - form.type

This is the simplest way of customization but you have to maintain any further changes in decorated form (adding a new field and so on).

Extension

The most flexible way to extend a form is to use form extensions. They’re similar to form types, but they also allow to keep base form class untouched.

These classes look like this:

namespace App\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FileType;

class MyExtension extends AbstractTypeExtension
{
    public static function getExtendedTypes(): iterable
    {
        return [\My\Bundle\With\Form\ToExtend::class];
    }
}

Also, needs to be registered:

app.form.some_extension:
   class: App\Form\Extension\MyExtension
   tags: 
       - form.type_extension

Let’s say, we want to add a field:

namespace App\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;

class MyExtension extends AbstractTypeExtension
{

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('holder', TextType::class, array('label' => 'form.credit_card.holder'))
        ;
    }            

    public static function getExtendedTypes(): iterable
    {
        return [\My\Bundle\With\Form\ToExtend::class];
    }
}

And we have a place we can start from.

Looking for help with Symfony development?

Form Types — frequently encountered issues

Once we have entry points, we also can start to play and solve a few common problems. All beneath can be applied to either form type or extension. Feel free to experiment.

Injecting a value to view.

Sometimes we need a value that is unavailable in the current form template. A buildView the method is the solution:

public function buildView(FormView $view, FormInterface $form, array $options): void
{
    $view->vars['some_value'] = 'W Szczebrzeszynie chrząszcz brzmi w trzcinie';
}

And in template…

{{ some_value }}

Pay attention you can override a target form value by overwriting value key. It’s not recommended, you’ve been warned!

Transforming values for view purposes

Problem: we have a form text field but we want its value to be encoded via base64 or any other way of transformation. Symfony also solves this problem by Data Transformers. They are classes which the main purpose is to translate a value between layers. We’re interested in the view layer.

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('holder', TextType::class, array('label' => 'form.credit_card.holder'))
        ->add('number', TextType::class, array('label' => 'form.credit_card.number'))
        ->add('securityCode', TextType::class, array('label' => 'form.credit_card.security_code'))
        ->add(
            'expireAt',
            CreditCardExpirationDateType::class,
            array(
                'input' => 'datetime',
                'widget' =>'choice',
                'label' => 'form.credit_card.expire_at',
            )
        );
        
    $builder->get('holder')->addViewTransformer(
        new class implements DataTransformerInterface
        {
            public function transform($value): string
            {
                if (null === $issue) {
                    return '';
                }
        
                return base64_encode($value);
            }
        
            public function reverseTransform($value): ?Issue
            {
                if (!$value) {
                    return null;
                }
        
                return base64_decode($value);
            }
        }
    );
}

It’s recommended to extract an inline class to a separate one. This is only for demonstrational purposes.

Overriding already existing field’s options

If none of the above solves your problem, we have an opportunity to modify already existing fields…

WAIT: we have mentioned before it’s impossible to modify added fields.

Yes, we did. So use this method only if everything else fails. This is a workaround over Symfony Forms’ immutability.

What we need is to iterate all existing children and re-create a particular field.

public function buildForm(FormBuilderInterface $builder, array $options)
{
    /**
      * @var FormBuilderInterface $field
      */
    foreach($builder as $name=>$field){
        $builder->remove($name);
    
        if($name !== 'what_i_want'){
            $builder->add($field);
            continue;
        }
    
        $builder->add($name, $field->getType()->getBlockPrefix(), $field->getOptions() + [
            'my_option'=>'my_value'
        ]);
    }
}

Remember: it’s a hack, so use it wisely. It doesn’t cover sub-forms, you need them manually for a particular field.

If you need help with forms customization, reach out to us. We have already developed many similar solutions for our clients.