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