Skip to content

Introduction

The Hyvä Checkout Form API provides a structured approach to building interactive forms with full customization support. The 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. The Form API enables third-party modules to modify forms through dedicated extension points like modifiers and factories, without needing to edit the main phtml template files.

The Hyvä Checkout Form API offers guides for creating all the necessary components to construct a form, including fields, buttons, and other essential elements. Forms built with the Form API separate concerns—form structure is defined in PHP classes, rendering is controlled through Layout XML, and presentation is handled in templates.

Why use it?

The Hyvä Checkout Form API solves the challenge of building forms that can be easily modified by third parties. 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

The Hyvä Checkout shipping address form demonstrates the practical value of the Form API. Let's use our shipping address form as an example. The shipping address form is particularly straightforward, as it should include fields that reflect all required quote address attributes provided by Magento.

From a user's perspective, the shipping address form 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

This example demonstrates a complete Hyvä Checkout form implementation using Magewire as the form driver. 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

The layout block defines where and how the form renders on the page. 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.

This example demonstrates the Layout XML block configuration for a Magewire-driven form:

<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

The Magewire component handles form interaction and submission logic for Magewire-driven forms. Since the form handling is driven by Magewire, we can extend our form from \Hyva\Checkout\Magewire\Component\AbstractForm.

This example shows the minimal Magewire component class required for a form:

<?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 Form class defines the form structure including fields, elements, and attributes. The component requires a Hyva\Checkout\Model\Form\AbstractEntityForm form object.

This example demonstrates a complete form class with field construction:

<?php

class MagewireDrivenForm extends \Hyva\Checkout\Model\Form\AbstractEntityForm
{
    // Publicly visible form namespace constant (required).
    // This namespace is used for renderer lookups and should be unique.
    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);
    }

    /**
     * Populate the form with fields and elements.
     * This method is called during form initialization to build the default form structure.
     *
     * @return EntityFormInterface
     */
    public function populate(): EntityFormInterface
    {
        // Add a text input field for the firstname
        $this->addField(
            $this->createField('firstname', 'text', [
                'data' => [
                    'label' => 'Firstname'
                ]
            ])
        );

        // Add a submit button element
        $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;
    }

    /**
     * Get the form title for display purposes.
     *
     * @return string
     */
    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 Save Service handles data persistence when the form is submitted. 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.

This example demonstrates a minimal Save Service implementation:

<?php

class MagewireDrivenFormSaveService extends \Hyva\Checkout\Model\Form\AbstractEntityFormSaveService
{
    /**
     * Save the form data to the appropriate destination.
     * This method receives the populated form and is responsible for persisting the data.
     *
     * @param Hyva\Checkout\Model\Form\EntityFormInterface $form
     * @return Hyva\Checkout\Model\Form\EntityFormInterface
     */
    public function save(Hyva\Checkout\Model\Form\EntityFormInterface $form): Hyva\Checkout\Model\Form\EntityFormInterface
    {
        // Implement your save logic here
        // Example: save to database, session, or external API
        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 enable optional customization of existing forms through hooks. 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.

This example demonstrates a form modifier that adds a password field:

<?php

class WithLastnameModifier implements \Hyva\Checkout\Model\Form\EntityFormModifierInterface
{
    /**
     * Apply modifications to the form using registered hooks.
     * This method registers callbacks that execute at specific lifecycle points.
     *
     * @param \Hyva\Checkout\Model\Form\EntityFormInterface $form
     * @return \Hyva\Checkout\Model\Form\EntityFormInterface
     */
    public function apply(\Hyva\Checkout\Model\Form\EntityFormInterface $form): \Hyva\Checkout\Model\Form\EntityFormInterface
    {
        // Register a modification listener for the 'form:init' hook
        $form->registerModificationListener(
            'someUniqueNameDescribingTheModifierPurpose',
            'form:init',
            fn ($form) => $this->includeAuthentication($form)
        );
    }

    /**
     * Add a password field to the form for authentication.
     * This method is called by the registered modification listener.
     *
     * @param \Hyva\Checkout\Model\Form\EntityFormInterface $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.