Skip to content

Introduction

Our Form API is designed to bring together various form elements into a single phtml file, making it easy to create fully interactive forms without sacrificing customization options later on. We've built it on the idea that any part of a form can be changed by third-party modules, like adding an address auto-complete feature, without needing to edit the main phtml file.

This API offers guides for creating all the necessary components to construct a form, including fields, buttons, and other essential elements.

Why use it?

It's always a challenge to build forms that can be easily modified by third parties. If you're planning to create a form that needs to be altered later on, creating a form extended from our provided abstraction layer, would be the most sensible approach. However, if you only need fixed fields or require minimal modifications, sticking with a regular phtml writing a form with some fields and buttons, perhaps mixed with Magewire, would be most sufficient.

All address forms in Hyvä Checkout are constructed using our form abstraction, allowing developers to fully customize the forms without needing access to the core. In the future, we plan to migrate components such as the order comment and coupon code forms as well.

Example scenario

Let's use our shipping address form as an example. This form is particularly straightforward, as it should include fields that reflect all required quote address attributes provided by Magento. From a user's perspective, it may not seem very difficult. However, complexity arises when we introduce elements such as fields that should be displayed based on system configuration settings or attribute settings, a region dropdown that should only appear for specific countries, or third-party extensions that aim to hide particular address fields by default, replacing them with a single field linked to an address auto-complete JavaScript API.

As you can imagine, customization requirements become apparent quite rapidly. Fortunately, thanks to the Form API, modifications can be implemented in a streamlined manner without concerns about overwriting templates. This prevents scenarios where multiple sources require different modifications to the same form element.

Example result

For this example, we're opting for a Magewire-driven form, which necessitates several components coming together to solidify the form.

For a more advanced example, please refer to "Magewire Driven Forms" available here.

Layout form block

To render a form, we offer a default form template specifically designed for Magewire-driven forms. It's a good practice to wrap the form with a parent block, which allows for adding blocks in front of or behind the form in the future.

<block name="magewire-driven-form"
       template="Hyva_Example::checkout/magewire-driven-form.phtml"
>
    <block name="magewire-driven-form.form"
           as="form"
           template="Hyva_Checkout::magewire/component/form.phtml"
    >
        <arguments>
            <argument name="magewire" xsi:type="object">
                \Hyva\Example\Magewire\Checkout\MagewireDrivenForm
            </argument>
        </arguments>
    </block>
</block>

Magewire component class

Since the form handling is driven by Magewire, we can extend our form from \Hyva\Checkout\Magewire\Component\AbstractForm.

<?php

class MagewireDriven extends \Hyva\Checkout\Magewire\Component\AbstractForm
{
    public function __construct(
        \Rakit\Validation\Validator $validator,
        \Hyva\Example\Model\Form\MagewireDrivenForm $form,
        \Psr\Log\LoggerInterface $logger,
        \Hyva\Checkout\Model\Magewire\Component\Evaluation\Batch $evaluationResultBatch
    ) {
        parent::__construct($validator, $form, $logger, $evaluationResultBatch);
    }
}

Form class

The component requires a Hyva\Checkout\Model\Form\AbstractEntityForm form object.

<?php

class MagewireDrivenForm extends \Hyva\Checkout\Model\Form\AbstractEntityForm
{
    // Publicly visible form namespace constant (required).
    public const FORM_NAMESPACE = 'my_form';

    public function __construct(
        Hyva\Checkout\Model\Form\EntityFormFieldFactory $entityFormFieldFactory,
        Magento\Framework\View\LayoutInterface $layout,
        Psr\Log\LoggerInterface $logger,
        Hyva\Example\Model\Form\SaveService\MagewireDrivenFormSaveService $formSaveService,
        Magento\Framework\Serialize\Serializer\Json $jsonSerializer,
        array $entityFormModifiers = [],
        array $factories = []
    ) {
        parent::__construct($entityFormFieldFactory, $layout, $logger, $formSaveService, $jsonSerializer, $entityFormModifiers, $factories);
    }

    public function populate(): EntityFormInterface
    {
        $this->addField(
            $this->createField('firstname', 'text', [
                'data' => [
                    'label' => 'Firstname'
                ]
            ])
        );

        $this->addElement(
            $this->createElement('submit', [
                'data' => [
                    'label' => 'Save'
                ]
            ])
        );

        // Specify a form attribute indicating that Magewire will handle the submission with the "submit" method.
        $this->setAttribute('wire:submit.prevent="submit"');

        return $this;
    }

    public function getTitle(): string
    {
        return 'My form';
    }
}

Breaking it down, we have a couple of requirements:

  1. The FORM_NAMESPACE public constant, which must be unique for each form you create.
  2. The $formSaveService, which needs to be a custom class specifically designed to handle the form field values.
  3. The populate() and getTitle() methods serve distinct purposes. The populate() method is responsible for constructing the default form elements.

The FORM_NAMESPACE should describe the form's purpose and is publicly utilized when the form is being rendered. In other words, choose a name wisely.

Form Save Service class

The last missing piece of the puzzle is the form Save Service, which handles the incoming data when the user completes the form. Where this data is stored is entirely up to the developer.

<?php

class MagewireDrivenFormSaveService extends \Hyva\Checkout\Model\Form\AbstractEntityFormSaveService
{
    public function save(Hyva\Checkout\Model\Form\EntityFormInterface $form): Hyva\Checkout\Model\Form\EntityFormInterface
    {
        return $form;
    }
}

The \Hyva\Checkout\Model\Form\EntityFormInterface will be marked as deprecated, but it can still be used. However, we encourage new concepts that require a form object to use \Hyva\Checkout\Model\Form\AbstractEntityForm instead.

Form modifier class (optional)

Form modifiers can be optionally added and are primarily intended for tweaking an existing form. Therefore, it is important that all essential fields are created via the populate function. Optional fields, for example, could be added via a modifier.

An example is a login component where the password field should be optional when the user has entered an email address that is already known as a customer. Once the sync to Magento is done, a modifier can be hooked in to first check the value of the email field and based on that, display the password field.

For this modifier we stay close to our example.

<?php

class WithLastnameModifier implements \Hyva\Checkout\Model\Form\EntityFormModifierInterface
{
    public function apply(\Hyva\Checkout\Model\Form\EntityFormInterface $form): \Hyva\Checkout\Model\Form\EntityFormInterface
    {
        $form->registerModificationListener(
            'someUniqueNameDescribingTheModifierPurpose',
            'form:init',
            fn ($form) => $this->includeAuthentication($form)
        );
    }

    private function includeAuthentication(\Hyva\Checkout\Model\Form\EntityFormInterface $form)
    {
        $form->addField(
            $form->createField(
                'password',
                'password',
                [
                    'label' => 'Password'
                ]
            )
        )
    }
}

Modifiers are not form constructors

It's important to understand that modifiers are primarily intended to hook into specific moments or events and make adjustments. In theory, a form should be reusable without making modifications to it.