Модификация страницы оформления заказа в 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. Надеемся, эта статья сэкономит вам время при написании собственных модулей.
Источники
