Автоматизация затрагивает всё: расчет стоимости доставки, создание и отправка накладной, получение PDF этикетки, отслеживание статусов и многое другое.

Выводить всю информацию (кнопки, статусы, формы отправки) на базовом виде заказа - не удобно, можно вынести всё в дополнительную вкладку.

Пример вкладки для Magento 1:

Опираясь на опыт с Magento 1, сделаем аналогичный функционал для Magento 2.

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

Создадим базу модуля, подключив его через composer к нашему магазину. В нашем примере — модуль разрабатывается как готовый пакет (package) для Magento 2. В тег name вписывается название пакета, по которому модуль будет искаться в репозитариях. M2 работает с autoload (PSR-4), что упрощает работу с различными внешними модулями.

composer.json

{
  "name": "vendor/module",
  "type": "magento2-module",
  "license": "proprietary",
  "homepage": "http://www.example.ru/magento2-module.html",
  "description": "Модуль доставки",
  "keywords": [
    "magento",
    "module",
    "delivery",
    "vendor"
  ],
  "authors": [
    {
      "name": "Vendor",
      "email": "mail@example.net",
      "homepage": "http://www.vendor.ru/"
    }
  ],
  "require": {
    "php": "~5.5.0|~5.6.0|~7.0.0",
    "magento/framework": "*"
  },
  "repositories": [
    {
      "type": "composer",
      "url": "https://repo.magento.com"
    }
  ],
  "autoload": {
    "files": [
      "registration.php"
    ],
    "psr-4": {
      "Vendor\\Module\\": ""
    }
  }
}

Для работы модуля нужно задать его зарегистрировать в системе как компонент.

registration.php

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

и 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_Module" setup_version="2.0.0">
    </module>
</config>

Вторым шагом добавляем layout

Добавляем блок в нужную секцию layout.

view/adminhtml/layout/sales_order_view.xml

<?xml version="1.0"?>
    <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-2columns-left"
          xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="left">
            <referenceBlock name="sales_order_tabs">
                <action method="addTab">
                    <argument name="name" xsi:type="string">Vendor Module</argument>
                    <argument name="block"
                              xsi:type="string">Vendor\Module\Block\Adminhtml\Order\View\Tab\Module</argument>
                </action>
            </referenceBlock>
        </referenceContainer>
    </body>
</page>

В теге argument мы передаем название блока, который будет подключен в качестве дополнительной вкладки на странице заказов

Определим сам блок:

Block/Adminhtml/Order/View/Tab/Module.php

<?php

namespace Vendor\Module\Block\Adminhtml\Order\View\Tab;

class Module extends \Magento\Backend\Block\Template implements \Magento\Backend\Block\Widget\Tab\TabInterface
{

    protected $_template = 'order/view/tab/module.phtml';

    /**
     *
     * @var \Magento\Framework\Registry
     */
    protected $coreRegistry = null;

    /**
     * @param \Magento\Backend\Block\Template\Context $context
     * @param \Magento\Framework\Registry $registry
     * @param array $data
     */
    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Framework\Registry $registry,
        array $data = []
    ) {
        $this->coreRegistry = $registry;
        parent::__construct($context, $data);
    }

    public function getOrder()
    {
        return $this->coreRegistry->registry('current_order');
    }

    /**
     * {@inheritdoc}
     */
    public function getTabLabel()
    {
        return __('Module');
    }

    /**
     * {@inheritdoc}
     */
    public function getTabTitle()
    {
        return __('Module');
    }

    /**
     * {@inheritdoc}
     */
    public function canShowTab()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isHidden()
    {
        return false;
    }

    /**
     *
     * @return string
     */
    public function getTabClass()
    {
        return 'ajax only';
    }

    /**
     *
     * @return string
     */
    public function getClass()
    {
        return $this->getTabClass();
    }

    /**
     *
     * @return string
     */
    public function getTabUrl()
    {
        return $this->getUrl('vendor_module/*/module', ['_current' => true]);
    }

}

В функции $this->getUrl для получения url мы передааем параметры: адрес нужного нам роута 'vendor_module/*/module' и дополнительный массив параметров ['_current' => true].

Символ * в адресе роута 'vendor_module/*/module означает передачу текущего используемого роута. То есть, если мы находимся на контроллере sales, то вместо * в итоговом адресе будет тот же контоллер.

Параметр '_current' => true отвечает за передачу таких же дополнительных параметрах к основному адресу при получении ссылки. К примеру, если мы находимся на странице с параметрами order_id/12, то эти же параметры будут переданы при генерации url. Если мы не указаываем нужный нам путь, то будет использован тот же модуль, контроллер и action.

Функцию canShowTab можно использовать для того, чтобы в нее передавать условия показа вкладки. К примеру - проверять, ваш ли метод доставки используется в этом заказе.

К примеру, определить принадлежность по $order в carrier можно так:

/**
     *
     * @param \Magento\Sales\Model\Order $order
     * @return boolean
     */
    public function isShippedBy(\Magento\Sales\Model\Order $order)
    {
        if (strpos($order->getShippingMethod(), $this->_code . '_') !== false) {
            return true;
        }
        return false;
    }

Подошли к важному отличию Magento 2 от Magento 1 - в панели администрирования, данные по вкладкам грузятся ajax'ом, поэтому для модуля нужно создать роут (route)

etc/adminhtml/routes.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="vendor_module" frontName="vendor_module">
            <module name="Vendor_Module"/>
        </route>
    </router>
</config>

Создадим контроллер, который будет отвечать за вкладку:

Controller/Adminhtml/Order/CustomTab.php

<?php

namespace Vendor\Module\Controller\Adminhtml\Order;

use Magento\Backend\App\Action;
use Magento\Sales\Api\OrderManagementInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Psr\Log\LoggerInterface;

class Module extends \Magento\Sales\Controller\Adminhtml\Order
{
    /**
     * @var \Magento\Framework\View\LayoutFactory
     */
    protected $layoutFactory;

    /**
     * @param Action\Context $context
     * @param \Magento\Framework\Registry $coreRegistry
     * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory
     * @param \Magento\Framework\Translate\InlineInterface $translateInline
     * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
     * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
     * @param \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory
     * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory
     * @param OrderManagementInterface $orderManagement
     * @param OrderRepositoryInterface $orderRepository
     * @param LoggerInterface $logger
     * @param \Magento\Framework\View\LayoutFactory $layoutFactory
     *
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
     */
    public function __construct(
        Action\Context $context,
        \Magento\Framework\Registry $coreRegistry,
        \Magento\Framework\App\Response\Http\FileFactory $fileFactory,
        \Magento\Framework\Translate\InlineInterface $translateInline,
        \Magento\Framework\View\Result\PageFactory $resultPageFactory,
        \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
        \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory,
        \Magento\Framework\Controller\Result\RawFactory $resultRawFactory,
        OrderManagementInterface $orderManagement,
        OrderRepositoryInterface $orderRepository,
        LoggerInterface $logger,
        \Magento\Framework\View\LayoutFactory $layoutFactory
    ) {
        $this->layoutFactory = $layoutFactory;
        parent::__construct(
            $context,
            $coreRegistry,
            $fileFactory,
            $translateInline,
            $resultPageFactory,
            $resultJsonFactory,
            $resultLayoutFactory,
            $resultRawFactory,
            $orderManagement,
            $orderRepository,
            $logger
        );
    }

    public function execute()
    {
        $this->_initOrder();
        $layout = $this->layoutFactory->create();
        $html = $layout->createBlock('Vendor\Module\Block\Adminhtml\Order\View\Tab\Module')
            ->toHtml();
        $this->_translateInline->processResponseBody($html);
        /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */
        $resultRaw = $this->resultRawFactory->create();
        $resultRaw->setContents($html);
        return $resultRaw;
    }
}

В данном контроллере функция execute() непосредственно отвечает за выполнение action.

Отметим еще отличие Magento 2 от Magento 1: в M2 каждый action на контроллер делается как отдельный файл. В M1 контроллер мог включать в себя несколько action сразу.

Создадим шаблон вывода:

order/view/tab/module.phtml

<section class="admin__page-section module-tab-content">
    <h1>Привет из Санкт-Петербурга</h1>
</section>

Делаем очистку кеша

php bin/magento cache:clean

Заходим в заказ и проверяем:

Кликаем:

Conclusion

Готово! Надеемся, что данная статья расширила ваши представления о принципах разработки на Magento 2.