How to add a live form for a custom model?

This guide shows how to integrate Symfony UX Live Components into a Sylius form for a custom model (resource). We’ll build a Supplier resource that:

  • Manages a collection of Zones via a Live Collection component

  • Offers an autocomplete for selecting a Channel via a Live Autocomplete component

Prerequisites

For the purpose of this guide, we'll be using the previously created Supplier resource in the examples.

Also, make sure you have an understanding of Symfony UX Live Components, which you can read about here:

Build your Live Components

Live form

Once you create the Supplier resource, it comes with a fully functional form type. However, it lacks features like dynamic validation. To add these, the default form maintained by the resource bundle needs to be transformed into a dynamic one.

Constraints

Make sure the Supplier resource includes constraints that can be validated:

// src/Entity/Supplier.php

use Symfony\Component\Validator\Constraints as Assert;

class Supplier implements ResourceInterface
{
    ...

    #[Assert\NotBlank]
    private ?string $name = null;

    ...
}

Register live component

First, we need to register the component. It’s important to set the correct tag. As we are dealing with the resource in the admin context (a Supplier resource managed by an admin user), the component must be registered to reflect this context accordingly.

# config/services.yaml

services:
    app_admin.twig.component.supplier.form: # Your custom ID name, but it’s best to follow the convention.
        class: Sylius\Bundle\UiBundle\Twig\Component\ResourceFormComponent # Here we specify the base form component that already supports live actions.  
        arguments:
            - '@app.repository.supplier'
            - '@form.factory'
            - '%app.model.supplier.class%'
            - 'Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType'
        tags:
            - { name: 'sylius.live_component.admin', key: 'app_admin:twig:component:supplier:form' } # key == identifier used in Twig hooks configuration.

The service has been registered, however, it's not yet used anywhere.

The next step is to override the generic hookables that render the component with the live version we created:

# config/packages/twig_hooks.yaml

sylius_twig_hooks:
    hooks:
        'sylius_admin.supplier.create.content':
            form:
                component: 'app_admin:twig:component:supplier:form'
                props:
                    template: '@SyliusAdmin/shared/crud/common/content/form.html.twig'
                    resource: '@=_context.resource'

        'sylius_admin.supplier.update.content':
            form:
                component: 'app_admin:twig:component:supplier:form'
                props:
                    template: '@SyliusAdmin/shared/crud/common/content/form.html.twig'
                    resource: '@=_context.resource'

We need to provide:

# config/packages/sylius_resource.yaml

sylius_resource:
    resources:
        app.supplier:
            driver: doctrine/orm
            classes:
                model: App\Entity\Supplier
                repository: App\Repository\SupplierRepository

From now on, when you focus on a required field, dynamic validation will be triggered once the field loses focus, without the need to explicitly submit the form:

Live collection

Expanding the Supplier Entity

First, let’s expand the Supplier entity by adding a new field called zones. The easiest way to do this is by using the MakerBundle to create a many-to-many relation with the App\Entity\Addressing\Zone entity through the wizard. Don’t forget to create and execute the migration afterwards.

Supplier FormType

Since we want to add a LiveCollectionType to our entity, we can no longer rely on the default form type from the resource bundle. Let's to create a new one:

<?php

// src/Form/Type/SupplierType.php

namespace App\Form\Type;

use App\Entity\Supplier;
use Sylius\Bundle\AddressingBundle\Form\Type\ZoneChoiceType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;

class SupplierType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name')
            ->add('description')
            ->add('enabled')
            ->add('zones', LiveCollectionType::class, [
                'entry_type' => ZoneChoiceType::class,
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false,
                'button_add_options' => [
                    'label' => 'sylius.form.zone.add_member'
                ],
                'button_delete_options' => [
                    'attr' => ['class' => 'btn btn-outline-danger']
                ],
                'delete_empty' => false,
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Supplier::class,
        ]);
    }
}

The Supplier configuration needs to be updated accordingly:

# config/packages/sylius_resource.yaml

sylius_resource:
    resources:
        app.supplier:
            driver: doctrine/orm
            classes:
                model: App\Entity\Supplier
                repository: App\Repository\SupplierRepository
                form: App\Form\Type\SupplierType

The component also needs to be updated to use the new SupplierType form type:

# config/services.yaml

services:
    app_admin.twig.component.supplier.form:
        class: Sylius\Bundle\UiBundle\Twig\Component\ResourceFormComponent
        arguments:
            - '@app.repository.supplier'
            - '@form.factory'
            - '%app.model.supplier.class%'
            - 'App\Form\Type\SupplierType'
        tags:
            - { name: 'sylius.live_component.admin', key: 'app_admin:twig:component:supplier:form' }

Organization of form fields

When you want to render all defined form fields as they are, no special adjustments are needed—the internal code handles everything. However, if you’d like to change the order or exclude certain fields from rendering, you can follow Symfony’s documentation. For example, you can adjust the order in the form type configuration or override the form rendering template using Twig hooks to manually render selected fields with {{ form_row(...) }}, and so on.

sylius_twig_hooks:
    hooks:
        'sylius_admin.supplier.create.content':
            form:
                component: 'app.twig.component.supplier.form'
                props:
                    template: 'supplier/form.html.twig' # Your alternative form setup
                    resource: '@=_context.resource'
        'sylius_admin.supplier.update.content':
            form:
                component: 'app.twig.component.supplier.form'
                props:
                    template: 'supplier/form.html.twig' # Your alternative form setup
                    resource: '@=_context.resource'

Live autocomplete

Expanding the Supplier Entity again

Let’s expand the Supplier entity by adding another field called channels with a many-to-many relation with the App\Entity\Channel\Channel entity through the Symfony Maker wizard and handle the migration afterwards.

Channel Autocomplete Type

<?php

// src/Form/Channel/ChannelAutocompleteType.php

namespace App\Form\Channel;

use App\Entity\Channel\Channel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;

#[AsEntityAutocompleteField(route: 'sylius_admin_entity_autocomplete')]
class ChannelAutocompleteType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'class' => Channel::class,
            'placeholder' => 'Choose a Channel',
            'choice_label' => 'name',
            'searchable_fields' => ['name'],
        ]);
    }

    public function getParent(): string
    {
        return BaseEntityAutocompleteType::class;
    }
}

Take a look at the configured route: sylius_admin_entity_autocomplete. It must be set appropriately to reflect usage within the admin context.

Supplier FormType

Include channels field in the form type:

// src/Form/Type/SupplierType.php
...
->add('channels', ChannelAutocompleteType::class, [
    'multiple' => true,
    'required' => true,
    'constraints' => [
        new Count([
            'min'        => 1,
            'minMessage' => 'app.supplier.channels.min_count',
        ]),
    ],
])

Ready to go. No further changes needed.

Last updated

Was this helpful?