Модификация страницы оформления заказа в Magento 2

Введение

В Magento 2 реализация страницы оформления заказа кардинально отличается от реализации в первой версии Magento. Теперь форма отправки заказа формируется полностью на стороне клиента с помощью js-модулей, так называемых UI компонентов, с использованием js-фреймворка knockout (Источник 1). Все js-компоненты подключаются и взаимодействую между собой через библиотеку RequireJS (Источник 2). Кроме того, изменения коснулись и backend-части. Например, данные, вводимые пользователем, сохраняются теперь через внутренний api-вызов, а не обычным POST-запросом на контроллер, как это было в прежней версии.

Понятно, что приемы, которые ранее применялись для модификации страницы заказа в Magento 1 теперь не работают. В статье подробно рассмотрим процесс модификации страницы заказа на примере добавления поля "Дата доставки" на форму оформления заказа.

Перед началом работы

В статье используется последняя доступная на момент написания статьи версия Magento 2.1.6.

Перед началом работы необходимо переключиться в режим разработчика, это можно сделать командой:

php bin/magento deploy:mode:set developer

В developer-режиме все ошибки будут сразу выводиться в окне браузера, а статичные файлы в папке pub/static будут создаваться в виде символических ссылок на файлы, лежащие в папке с модулем, less-файлы будут компилироваться в css "на лету". Всё это значительно упрощает процесс разработки.

Также можно отключить некоторые типы кэша, которые будут часто использоваться в процессе работы, в нашем случае это config и layout. Для этого можно воспользоваться командой:

php bin/magento cache:disable config layout

Если с выключенным кешем страницы грузятся слишком долго, можно оставить кеш включенным, а после изменений очищать кеш командой:

php bin/magento cache:clean config layout

или

php bin/magento cache:flush

Последняя команда полностью очистит хранилище кэша.

Создание модуля

Для начала сделаем рабочий каркас модуля. Создадим следующую структуру файлов и папок:

app/code/Vendor/OrderDeliveryDate/registration.php

app/code/Vendor/OrderDeliveryDate/etc/module.xml

Файл registration.php:

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendor_OrderDeliveryDate',
    __DIR__
);

Файл module.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_OrderDeliveryDate" setup_version="1.0.0"/>
</config>

Название "Vendor" выбрано произвольно и может быть заменено на название вашей компании. Включаем модуль командой:

php bin/magento module:enable Vendor_OrderDeliveryDate

Теперь добавим колонку к таблицам quote и sales_order для хранения даты доставки. Для этого создадим файла install-скрипта.

Файл app/code/Vendor/OrderDeliveryDate/Setup/InstallSchema.php:

<?php

namespace Vendor\OrderDeliveryDate\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

class InstallSchema implements InstallSchemaInterface
{

    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;

        $installer->startSetup();

        $installer->getConnection()->addColumn(
            $installer->getTable('quote'),
            'delivery_date',
            [
                'type' => \Magento\Framework\DB\Ddl\Table::TYPE_DATE,
                'nullable' => true,
                'comment' => 'Delivery Date',
            ]
        );

        $installer->getConnection()->addColumn(
            $installer->getTable('sales_order'),
            'delivery_date',
            [
                'type' => \Magento\Framework\DB\Ddl\Table::TYPE_DATE,
                'nullable' => true,
                'comment' => 'Delivery Date',
            ]
        );

        $installer->endSetup();
    }
}

Для запуска обновления базы данных выполняем команду:

php bin/magento setup:upgrade

Примечание

Если команда setup:upgrade уже выполнялась ранее и необходимо запустить install-скрипт еще раз, нужно предварительно удалить строчку с версией модуля из таблицы setup_module sql-запросом

DELETE FROM `setup_module` WHERE `module` LIKE 'Vendor_OrderDeliveryDate'

Перейдем непосредственно к странице оформления заказа. Для начала добавим поле для выбора даты на форму, а после займемся сохранением значения в базу данных.

Процесс стандартного оформления заказа в Magento 2 состоит из двух шагов. На первом указывается адрес доставки и выбирается метод доставки, а на втором шаге указывает billing-адрес и метод оплаты. Поскольку дата доставки относится к shipping-информации, добавим поле выбора даты доставки в эту область формы на первом шаге:

Как уже было сказано выше, форма оформления заказа формируется с помощью UI-компонентов, которые образует древовидную структуру. Структура компонентов по умолчанию, которые занимаются отрисовкой формы, указывается в файле vendor/magento/module-checkout/view/frontend/layout/checkout_index_index.xml. Нас интересует аргумент jsLayout, который передается в блок Magento\Checkout\Block\Onepage.

Создадим аналогичный layout-файл в нашем модуле и расширем данную структуру.

Файл app/code/Vendor/OrderDeliveryDate/view/frontend/layout/checkout_index_index.xml:

<?xml version="1.0"?>
<page>
    <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="delivery-date-block" xsi:type="array">
                                                            <item name="component" xsi:type="string">uiComponent</item>
                                                            <item name="displayArea" xsi:type="string">shippingAdditional</item>
                                                            <item name="config" xsi:type="array">
                                                                <item name="template" xsi:type="string">Vendor_OrderDeliveryDate/shipping/delivery-date</item>
                                                            </item>
                                                            <item name="children" xsi:type="array">
                                                                <item name="delivery-date" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Vendor_OrderDeliveryDate/js/view/form/element/delivery-date</item>
                                                                    <item name="dataScope" xsi:type="string">deliveryDate</item>
                                                                    <item name="provider" xsi:type="string">checkoutProvider</item>
                                                                    <item name="config" xsi:type="array">
                                                                        <item name="template" xsi:type="string">ui/form/field</item>
                                                                        <item name="elementTmpl" xsi:type="string">ui/form/element/date</item>
                                                                    </item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

В этом файле мы добавили новый компонент delivery-date-block со стандартным типом uiComponent в уже существующий компонент shippingAddress. Мы указали параметр displayArea равный "shippingAdditional", поскольку в шаблоне компонента shippingAddress присутствует следующий код:

Файл vendor/magento/module-checkout/view/frontend/web/template/shipping.html:

<!-- ko foreach: getRegion('shippingAdditional') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- /ko -->

Наш компонент будет выводиться в этом месте.

Шаблон нашего компонента (файл app/code/Vendor/OrderDeliveryDate/view/frontend/web/template/shipping/delivery-date.html):

<div class="checkout-delivery-date form-shipping-address">
    <div class="step-title" data-bind="i18n: 'Delivery Date'" data-role="title"></div>
    <each args="data: elems, as: 'element'">
        <render if="hasTemplate()"/>
    </each>
</div>

Здесь мы выводим заголовок блока и выводим все дочерние компоненты. Наш компонент содержит один дочерний элемент - поле с выбором даты.

Код компонента выбора даты. Файл app/code/Vendor/OrderDeliveryDate/view/frontend/web/js/view/form/element/delivery-date.js:

define(
    [
        'jquery',
        'Magento_Ui/js/form/element/date',
        'ko',
        'Vendor_OrderDeliveryDate/js/model/delivery-date',
        'Vendor_OrderDeliveryDate/js/model/shipping-save-processor/default',
        'Magento_Checkout/js/model/shipping-save-processor'
    ],
    function (
        $,
        dateComponent,
        ko,
        deliveryDateModel,
        defaultSaveProcessor,
        shippingSaveProcessor
    ) {
        'use strict';
        shippingSaveProcessor.registerProcessor('default', defaultSaveProcessor);
        return dateComponent.extend({
            onValueChange: function(value) {
                this._super(value);
                deliveryDateModel.date(value);
            }
        });
    }
);

Компонент наследуется от стандартного компонента Magento_Ui/js/form/element/date. В родительском классе уже есть функция onValueChange, которая вызывается при изменении значения в поле выбора даты. Мы используем эту функцию чтобы сохранить значение в нашу data-модель.

Файл app/code/Vendor/OrderDeliveryDate/view/frontend/web/js/model/delivery-date.js:

define(
    [
        'ko'
    ],
    function (
        ko
    ) {
        'use strict';
        var deliveryDate = null;
        return {
            date: ko.observable(deliveryDate)
        }
    }
);

Кроме того, мы переопределили стандартный обработчик сохранения shipping-информации (файл vendor/magento/module-checkout/view/frontend/web/js/model/shipping-save-processor/default.js), для того чтобы добавить туда значение нашего поля.

Файл app/code/Vendor/OrderDeliveryDate/view/frontend/web/js/model/shipping-save-processor/default.js

/*global define,alert*/
define(
    [
        'ko',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/resource-url-manager',
        'mage/storage',
        'Magento_Checkout/js/model/payment-service',
        'Magento_Checkout/js/model/payment/method-converter',
        'Magento_Checkout/js/model/error-processor',
        'Magento_Checkout/js/model/full-screen-loader',
        'Magento_Checkout/js/action/select-billing-address',
        'Vendor_OrderDeliveryDate/js/model/delivery-date'
    ],
    function (
        ko,
        quote,
        resourceUrlManager,
        storage,
        paymentService,
        methodConverter,
        errorProcessor,
        fullScreenLoader,
        selectBillingAddressAction,
        deliveryDateModel
    ) {
        'use strict';

        return {
            source: false,
            saveShippingInformation: function () {
                var payload;
                
                if (!quote.billingAddress()) {
                    selectBillingAddressAction(quote.shippingAddress());
                }

                payload = {
                    addressInformation: {
                        shipping_address: quote.shippingAddress(),
                        billing_address: quote.billingAddress(),
                        shipping_method_code: quote.shippingMethod().method_code,
                        shipping_carrier_code: quote.shippingMethod().carrier_code,
                        extension_attributes: {delivery_date: deliveryDateModel.date()}
                    }
                };

                fullScreenLoader.startLoader();

                return storage.post(
                    resourceUrlManager.getUrlForSetShippingInformation(quote),
                    JSON.stringify(payload)
                ).done(
                    function (response) {
                        quote.setTotals(response.totals);
                        paymentService.setPaymentMethods(methodConverter(response.payment_methods));
                        fullScreenLoader.stopLoader();
                    }
                ).fail(
                    function (response) {
                        errorProcessor.process(response);
                        fullScreenLoader.stopLoader();
                    }
                );
            }
        };
    }
);

Как видно из кода, единственное изменение в том, что мы добавили параметр delivery_date в объект с данными, которые отправляются на сервер, из нашей data-модели.

extension_attributes: {delivery_date: deliveryDateModel.date()}

Здесь мы использовали стандартный механизм Extension Attributes (Источник 3).

Дело в том, что для сохранения shipping-информации используется api-вызов функции saveAddressInformation класса \Magento\Checkout\Model\ShippingInformationManagement. Если посмотреть интерфейс \Magento\Checkout\Api\ShippingInformationManagementInterface , видно, что функция принимает на вход 2 параметра $cartId и $addressInformation с типом \Magento\Checkout\Api\Data\ShippingInformationInterface. Для того чтобы расширить интерфейс \Magento\Checkout\Api\Data\ShippingInformationInterface и добавить параметр delivery_date мы и используем механизм Extension Attributes.

Для добавления нового extension-атрибута создаём файл app/code/Vendor/OrderDeliveryDate/etc/extension_attributes.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Magento\Checkout\Api\Data\ShippingInformationInterface">
        <attribute code="delivery_date" type="string" />
    </extension_attributes>
</config>

Теперь, после очистки кеша конфигурации и папки var/generation, параметр delivery_date будет успешно приходить на сервер.

Новое поле выглядит следующим образом:

Стандартный компонент Magento_Ui/js/form/element/date уже содержит js-плагин для выбора даты, поэтому с текстовым полем больше ничего делать не требуется.

Extension-атрибуты автоматически не сохраняются, поэтому мы должны сами передать данные в quote-объект. Для этого воспользуемся механизмом плагинов (Источник 4).

Файл app/code/Vendor/OrderDeliveryDate/etc/di.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Api\ShippingInformationManagementInterface">
        <plugin name="vendor_delivery_date" type="Vendor\OrderDeliveryDate\Plugin\Magento\Checkout\Api\ShippingInformationManagement" />
    </type>
</config>

Файл app/code/Vendor/OrderDeliveryDate/Plugin/Magento/Checkout/Api/ShippingInformationManagement.php:

<?php

namespace Vendor\OrderDeliveryDate\Plugin\Magento\Checkout\Api;

use Magento\Checkout\Api\ShippingInformationManagementInterface as Subject;


class ShippingInformationManagement
{

    protected $quoteRepository;

    public function __construct(\Magento\Quote\Api\CartRepositoryInterface $cartRepository)
    {
        $this->quoteRepository = $cartRepository;
    }

    public function beforeSaveAddressInformation(Subject $subject, $cartId, \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation)
    {
        if ($extensionAttributes = $addressInformation->getExtensionAttributes()) {
            if ($date = $extensionAttributes->getDeliveryDate()) {
                /** @var \Magento\Quote\Model\Quote $quote */
                $quote = $this->quoteRepository->getActive($cartId);
                $quote->setData('delivery_date', $date);
            }

        }
        return [$cartId, $addressInformation];
    }
}

Теперь нужно скопировать данные из quote в order-объект. Для этого воспользуемся событием sales_model_service_quote_submit_before.

Файл app/code/Vendor/OrderDeliveryDate/etc/events.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="sales_model_service_quote_submit_before">
        <observer name="vendor-order-delivery-date" instance="Vendor\OrderDeliveryDate\Observer\QuoteSubmitBefore" />
    </event>
</config>

Файл app/code/Vendor/OrderDeliveryDate/Observer/QuoteSubmitBefore.php:

<?php

namespace Vendor\OrderDeliveryDate\Observer;

use Magento\Framework\Event\ObserverInterface;

class QuoteSubmitBefore implements ObserverInterface
{
    
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        $quote = $observer->getQuote();
        $order = $observer->getOrder();
        
        if ($quote->getData('delivery_date')) {
            $order->setData('delivery_date', $quote->getData('delivery_date'));
        }
    }
}

И, наконец, выведем введенное пользователем значение на странице просмотра заказа в административном интерфейсе.

Файл app/code/Vendor/OrderDeliveryDate/view/adminhtml/layout/sales_order_view.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="order_shipping_view" template="Vendor_OrderDeliveryDate::order/view/info.phtml"/>
    </body>
</page>

Копируем файл vendor/magento/module-shipping/view/adminhtml/templates/order/view/info.phtml в app/code/Vendor/OrderDeliveryDate/view/adminhtml/templates/order/view/info.phtml и добавляем в файле в самый конец перед закрывающим тегом div:

<?php if ($order->getDeliveryDate()):?>
	<?php echo $this->escapeHtml(__('Delivery Date: %1', $order->getDeliveryDate()))?>
<?php endif?>

Для корректного переопределения шаблона нужно убедиться, что наш модуль загружается после модуля Magento_Shipping. Для этого добавим в файл с описанием нашего модуля последовательность загрузки.

Файл app/code/Vendor/OrderDeliveryDate/etc/module.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_OrderDeliveryDate" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Shipping"/>
        </sequence>
    </module>
</config>

Результат:

Заключение

Реализация оформления заказа в Magento 2 достаточно сложная. Для модификации данного функционала разработчику требуется хорошее знание всех аспектов работы системы. Для добавления одного поля на форму оформления заказа потребовалось использовать несколько нововведений Magento 2: UI компоненты, плагины и механизм Extenstion Attributes. Надеемся, эта статья сэкономит вам время при написании собственных модулей.

 

Источники

  1. Knockout : Home
  2. RequireJS
  3. EAV and extension attributes
  4. Plugins (Interceptors)