Разработка кастомного модуля Magento 2

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка кастомного модуля Magento 2
Сложная
~1-2 недели
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка кастомного модуля Magento 2

Кастомный модуль — единственный правильный способ добавить функциональность в Magento 2. Прямое изменение кода ядра или сторонних модулей гарантирует проблемы при обновлениях. Модуль изолирует кастомный код и взаимодействует с платформой через официальные точки расширения.

Структура модуля

app/code/MyCompany/ModuleName/
├── Api/
│   ├── Data/
│   │   └── CustomEntityInterface.php  # DTO интерфейс
│   └── CustomEntityRepositoryInterface.php
├── Block/
│   └── Adminhtml/
│       └── CustomEntity/
│           └── Grid.php
├── Controller/
│   ├── Adminhtml/
│   │   └── CustomEntity/
│   │       ├── Index.php
│   │       └── Save.php
│   └── Index/
│       └── View.php
├── etc/
│   ├── module.xml
│   ├── di.xml
│   ├── acl.xml
│   ├── events.xml         # подписки на события
│   ├── adminhtml/
│   │   └── routes.xml
│   └── frontend/
│       ├── routes.xml
│       └── events.xml
├── Model/
│   ├── CustomEntity.php
│   ├── ResourceModel/
│   │   ├── CustomEntity.php
│   │   └── CustomEntity/
│   │       └── Collection.php
│   └── Repository/
│       └── CustomEntityRepository.php
├── Observer/
│   └── OrderPlaceAfter.php
├── Plugin/
│   └── ProductSavePlugin.php
├── Setup/
│   └── Patch/
│       ├── Schema/
│       │   └── CreateCustomEntityTable.php
│       └── Data/
│           └── AddDefaultData.php
├── view/
│   ├── adminhtml/
│   │   ├── layout/
│   │   └── templates/
│   └── frontend/
│       ├── layout/
│       └── templates/
├── composer.json
└── registration.php

Декларация модуля

<!-- 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="MyCompany_ModuleName" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Catalog"/>
            <module name="Magento_Sales"/>
        </sequence>
    </module>
</config>
<?php
// registration.php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'MyCompany_ModuleName',
    __DIR__
);

Schema Patch — создание таблицы

<?php
// Setup/Patch/Schema/CreateCustomEntityTable.php
namespace MyCompany\ModuleName\Setup\Patch\Schema;

use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\Patch\SchemaPatchInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

class CreateCustomEntityTable implements SchemaPatchInterface
{
    public function __construct(
        private readonly SchemaSetupInterface $schemaSetup
    ) {}

    public function apply(): void
    {
        $setup = $this->schemaSetup;
        $setup->startSetup();
        $connection = $setup->getConnection();
        $tableName = $setup->getTable('mycompany_custom_entity');

        if (!$connection->isTableExists($tableName)) {
            $table = $connection->newTable($tableName)
                ->addColumn('entity_id', Table::TYPE_INTEGER, null, [
                    'identity' => true,
                    'nullable' => false,
                    'primary'  => true,
                    'unsigned' => true,
                ], 'Entity ID')
                ->addColumn('product_id', Table::TYPE_INTEGER, null, [
                    'unsigned' => true,
                    'nullable' => false,
                ], 'Product ID')
                ->addColumn('custom_value', Table::TYPE_DECIMAL, '12,4', [
                    'nullable' => false,
                    'default'  => '0.0000',
                ], 'Custom Value')
                ->addColumn('status', Table::TYPE_SMALLINT, null, [
                    'nullable' => false,
                    'default'  => 1,
                ], 'Status')
                ->addColumn('created_at', Table::TYPE_TIMESTAMP, null, [
                    'nullable' => false,
                    'default'  => Table::TIMESTAMP_INIT,
                ], 'Created At')
                ->addColumn('updated_at', Table::TYPE_TIMESTAMP, null, [
                    'nullable' => false,
                    'default'  => Table::TIMESTAMP_INIT_UPDATE,
                ], 'Updated At')
                ->addForeignKey(
                    $setup->getFkName($tableName, 'product_id', 'catalog_product_entity', 'entity_id'),
                    'product_id',
                    $setup->getTable('catalog_product_entity'),
                    'entity_id',
                    Table::ACTION_CASCADE
                )
                ->addIndex($setup->getIdxName($tableName, ['status']), ['status'])
                ->setComment('MyCompany Custom Entity Table');

            $connection->createTable($table);
        }

        $setup->endSetup();
    }

    public static function getDependencies(): array { return []; }
    public function getAliases(): array { return []; }
}

Model / ResourceModel / Collection

<?php
// Model/CustomEntity.php
namespace MyCompany\ModuleName\Model;

use Magento\Framework\Model\AbstractModel;

class CustomEntity extends AbstractModel
{
    protected function _construct(): void
    {
        $this->_init(ResourceModel\CustomEntity::class);
    }
}
<?php
// Model/ResourceModel/CustomEntity.php
namespace MyCompany\ModuleName\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class CustomEntity extends AbstractDb
{
    protected function _construct(): void
    {
        $this->_init('mycompany_custom_entity', 'entity_id');
    }
}
<?php
// Model/ResourceModel/CustomEntity/Collection.php
namespace MyCompany\ModuleName\Model\ResourceModel\CustomEntity;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;

class Collection extends AbstractCollection
{
    protected function _construct(): void
    {
        $this->_init(
            \MyCompany\ModuleName\Model\CustomEntity::class,
            \MyCompany\ModuleName\Model\ResourceModel\CustomEntity::class
        );
    }
}

Observer — подписка на событие

<?php
// Observer/OrderPlaceAfter.php
namespace MyCompany\ModuleName\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Psr\Log\LoggerInterface;

class OrderPlaceAfter implements ObserverInterface
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly \MyCompany\ModuleName\Model\CustomEntityFactory $entityFactory,
        private readonly \MyCompany\ModuleName\Model\ResourceModel\CustomEntity $entityResource,
    ) {}

    public function execute(Observer $observer): void
    {
        /** @var \Magento\Sales\Model\Order $order */
        $order = $observer->getEvent()->getOrder();

        try {
            foreach ($order->getAllVisibleItems() as $item) {
                $entity = $this->entityFactory->create();
                $entity->setData([
                    'product_id'   => (int)$item->getProductId(),
                    'custom_value' => $item->getQtyOrdered(),
                    'status'       => 1,
                ]);
                $this->entityResource->save($entity);
            }
        } catch (\Exception $e) {
            $this->logger->error('OrderPlaceAfter observer error: ' . $e->getMessage(), [
                'order_id' => $order->getId(),
            ]);
        }
    }
}
<!-- etc/events.xml -->
<config>
    <event name="sales_order_place_after">
        <observer name="mycompany_order_place_after"
                  instance="MyCompany\ModuleName\Observer\OrderPlaceAfter"/>
    </event>
</config>

Plugin (Interceptor)

<?php
// Plugin/ProductSavePlugin.php
namespace MyCompany\ModuleName\Plugin;

use Magento\Catalog\Model\Product;

class ProductSavePlugin
{
    // Before — выполняется до метода, может изменить аргументы
    public function beforeSave(Product $subject): void
    {
        if (!$subject->getData('custom_field')) {
            $subject->setData('custom_field', 'default_value');
        }
    }

    // After — выполняется после метода, получает результат
    public function afterSave(Product $subject, Product $result): Product
    {
        // Инвалидация кастомного кеша при сохранении продукта
        // ...
        return $result;
    }

    // Around — полный контроль, вызов $proceed() обязателен
    // Использовать только когда before/after не подходят
}
<!-- etc/di.xml -->
<type name="Magento\Catalog\Model\Product">
    <plugin name="mycompany_product_save_plugin"
            type="MyCompany\ModuleName\Plugin\ProductSavePlugin"
            sortOrder="10"
            disabled="false"/>
</type>

Console Command

<?php
// Console/Command/SyncDataCommand.php
namespace MyCompany\ModuleName\Console\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;

class SyncDataCommand extends Command
{
    protected function configure(): void
    {
        $this->setName('mycompany:sync-data')
             ->setDescription('Синхронизация данных с внешней системой')
             ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Только проверка без записи');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $isDryRun = $input->getOption('dry-run');
        $output->writeln('<info>Начало синхронизации...</info>');

        // логика синхронизации

        if (!$isDryRun) {
            $output->writeln('<info>Данные записаны</info>');
        } else {
            $output->writeln('<comment>Dry-run режим, данные не записаны</comment>');
        }

        return Command::SUCCESS;
    }
}

Регистрация в di.xml:

<type name="Magento\Framework\Console\CommandList">
    <arguments>
        <argument name="commands" xsi:type="array">
            <item name="mycompany_sync_data" xsi:type="object">
                MyCompany\ModuleName\Console\Command\SyncDataCommand
            </item>
        </argument>
    </arguments>
</type>

Запуск: bin/magento mycompany:sync-data --dry-run

Тестирование

# Unit tests
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml app/code/MyCompany/ModuleName/Test/Unit/

# Integration tests (требует отдельную БД)
vendor/bin/phpunit -c dev/tests/integration/phpunit.xml \
    app/code/MyCompany/ModuleName/Test/Integration/

Сроки

Простой модуль (новая таблица + CRUD в admin + observer): 3–5 дней. Модуль с REST API, Repository pattern, unit-тестами и Admin Grid: 1–2 недели. Сложный модуль (интеграция с внешним API, очереди, GraphQL расширение, полноценное тестирование): 3–6 недель.