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.