Checkout w Magento 2 to jeden z najtrudniejszych obszarów do customizacji – głęboko zagnieżdżony JSON konfiguracji knockout.js, wielowarstwowe mixiny JavaScript i kilkanaście kroków przetwarzania po stronie PHP. Pokazuję jak dodać własne pole do formularza checkout, własną walidację i jak zmodyfikować kroki procesu bez rozbijania istniejącej funkcjonalności.
Architektura checkout Magento 2
Checkout składa się z trzech warstw:
- PHP Steps – przetwarzanie po stronie serwera: płatność, wysyłka, zapis zamówienia
- JavaScript (knockout.js) – interfejs użytkownika, walidacja po stronie klienta, komponenty React-like
- Layout XML + jsLayout – konfiguracja struktury checkout jako JSON przekazywany do JS
Konfiguracja layoutu checkout trafia do frontendu jako jeden duży obiekt JSON przez blok checkout.root w checkout_index_index.xml.
Dodanie własnego pola do formularza adresu wysyłki
<!-- view/frontend/layout/checkout_index_index.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="shipping-step" xsi:type="array">
<item name="children" xsi:type="array">
<item name="shippingAddress" xsi:type="array">
<item name="children" xsi:type="array">
<item name="shipping-address-fieldset" xsi:type="array">
<item name="children" xsi:type="array">
<!-- Własne pole - numer domu/mieszkania -->
<item name="custom_delivery_note" xsi:type="array">
<item name="component" xsi:type="string">Magento_Ui/js/form/element/textarea</item>
<item name="config" xsi:type="array">
<item name="customScope" xsi:type="string">shippingAddress.custom_attributes</item>
<item name="template" xsi:type="string">ui/form/field</item>
<item name="elementTmpl" xsi:type="string">ui/form/element/textarea</item>
</item>
<item name="dataScope" xsi:type="string">shippingAddress.custom_attributes.delivery_note</item>
<item name="label" xsi:type="string" translate="true">Uwagi do dostawy</item>
<item name="provider" xsi:type="string">checkoutProvider</item>
<item name="visible" xsi:type="boolean">true</item>
<item name="validation" xsi:type="array">
<item name="max_text_length" xsi:type="number">500</item>
</item>
<item name="sortOrder" xsi:type="number">250</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
Zapis własnego pola przez Plugin do ShippingInformationManagement
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin;
use Magento\Checkout\Api\Data\ShippingInformationInterface;
use Magento\Checkout\Model\ShippingInformationManagement;
use Magento\Quote\Api\CartRepositoryInterface;
class SaveDeliveryNotePlugin
{
public function __construct(
private CartRepositoryInterface $quoteRepository
) {}
public function beforeSaveAddressInformation(
ShippingInformationManagement $subject,
int $cartId,
ShippingInformationInterface $addressInformation
): array {
$extAttributes = $addressInformation->getShippingAddress()->getExtensionAttributes();
$deliveryNote = $extAttributes?->getDeliveryNote();
if ($deliveryNote) {
$quote = $this->quoteRepository->getActive($cartId);
$quote->setData('delivery_note', htmlspecialchars(strip_tags($deliveryNote), ENT_QUOTES));
$this->quoteRepository->save($quote);
}
return [$cartId, $addressInformation];
}
}
<!-- etc/di.xml - rejestracja pluginu -->
<?xml version="1.0"?>
<config>
<type name="Magento\Checkout\Model\ShippingInformationManagement">
<plugin name="vendor_module_save_delivery_note"
type="Vendor\Module\Plugin\SaveDeliveryNotePlugin"/>
</type>
</config>
Przeniesienie danych z Quote do Order
<?php
namespace Vendor\Module\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Sales\Model\Order;
use Magento\Quote\Model\Quote;
class CopyDeliveryNoteToOrder implements ObserverInterface
{
public function execute(Observer $observer): void
{
/** @var Order $order */
$order = $observer->getData('order');
/** @var Quote $quote */
$quote = $observer->getData('quote');
$deliveryNote = $quote->getData('delivery_note');
if ($deliveryNote) {
$order->setData('delivery_note', $deliveryNote);
}
}
}
<!-- etc/events.xml -->
<?xml version="1.0"?>
<config>
<event name="sales_model_service_quote_submit_before">
<observer name="vendor_module_copy_delivery_note"
instance="Vendor\Module\Observer\CopyDeliveryNoteToOrder"/>
</event>
</config>
Własna walidacja po stronie JavaScript
// view/frontend/web/js/validator/delivery-note-validator.js
define([
'jquery',
'mage/translate',
'mage/validation'
], function ($, $t) {
'use strict';
// Rejestracja własnej reguły walidacji
$.validator.addMethod(
'validate-delivery-note',
function (value) {
if (!value) {
return true; // puste pole - ok (pole nieobowiązkowe)
}
// Sprawdź czy nie zawiera HTML/skryptów
const hasHtml = /<[^>]*>/g.test(value);
if (hasHtml) {
return false;
}
return value.length <= 500;
},
$t('Uwagi do dostawy: max 500 znaków, bez tagów HTML')
);
return $.validator;
});
// Mixin do komponentu checkout - rozszerzanie bez nadpisywania
// view/frontend/web/js/view/checkout-mixin.js
define([
'ko',
'mage/translate',
'../validator/delivery-note-validator'
], function (ko, $t) {
'use strict';
return function (Component) {
return Component.extend({
// Dodaj własną logikę do validate()
validate: function () {
const result = this._super();
const deliveryNote = this.source.get(
'shippingAddress.custom_attributes.delivery_note'
);
if (deliveryNote && deliveryNote.length > 500) {
this.source.set('params.invalid', true);
this.source.trigger('shippingAddress.data.validate');
return false;
}
return result;
}
});
};
});
// view/frontend/requirejs-config.js - rejestracja mixinu
var config = {
config: {
mixins: {
'Magento_Checkout/js/view/shipping': {
'Vendor_Module/js/view/checkout-mixin': true
}
}
}
};
Dodanie kroku do procesu checkout
// Własny krok checkout - np. krok wyboru daty dostawy
// view/frontend/web/js/view/checkout/delivery-date-step.js
define([
'ko',
'uiComponent',
'Magento_Checkout/js/model/step-navigator'
], function (ko, Component, stepNavigator) {
'use strict';
return Component.extend({
defaults: {
template: 'Vendor_Module/checkout/delivery-date-step'
},
isVisible: ko.observable(false),
selectedDate: ko.observable(''),
stepCode: 'delivery-date',
stepTitle: 'Data dostawy',
initialize: function () {
this._super();
// Rejestracja kroku w nawigatorze
stepNavigator.registerStep(
this.stepCode,
null,
this.stepTitle,
this.isVisible,
this.navigate.bind(this),
15 // sortOrder - po shipping (10), przed payment (20)
);
return this;
},
navigate: function () {
this.isVisible(true);
},
navigateToNextStep: function () {
stepNavigator.next();
},
isDeliveryDateValid: function () {
return this.selectedDate() !== '';
}
});
});
Podsumowanie
Checkout w Magento 2 jest skomplikowany z powodu warstwy JavaScript opartej na knockout.js i głęboko zagnieżdżonym JSON konfiguracji. Kluczowe zasady: używaj mixinów zamiast nadpisywania komponentów JS, zawsze zapisuj dane przez pluginy do odpowiednich serwisów (ShippingInformationManagement, PaymentInformationManagement), i przenoś dane z Quote do Order przez event sales_model_service_quote_submit_before. Testuj przy każdej zmianie – checkout jest wrażliwy na błędy konfiguracji JS.
