mcpField
M
magento-module-development

magento-module-development

github.com/finsilabs开发6080 次安装5 天前更新
代码仓库

关于 magento-module-development

面向 Magento 2 开发者的模块开发指南,介绍模块骨架、依赖注入、服务契约、插件、观察者和声明式数据库架构。也涵盖自定义 REST/GraphQL API、管理界面及常见开发问题。

主要功能

  1. 01搭建模块目录与注册文件
  2. 02使用服务契约和仓储模式
  3. 03配置依赖注入、插件与观察者
  4. 04通过声明式架构管理数据库
  5. 05创建自定义 REST 与 GraphQL API

适用场景

  1. 01为商店添加自定义模块功能
  2. 02扩展或调整 Magento 核心行为
  3. 03为无头集成提供 API 端点
  4. 04新增数据库表及管理界面

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill magento-module-development

安装后在 Claude Code 中输入/magento-module-development确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

tile.json
evals/declarative-schema-and-rest-api-endpoint/
criteria.json
task.md
evals/module-skeleton-service-contracts-and-di/
criteria.json
task.md
evals/plugins-observers-and-event-handling/
criteria.json
task.md

详细介绍

Magento 2 模块开发

概述

使用模块架构、依赖注入(DI)、服务契约(接口)、插件(拦截器)、观察者和仓储模式构建自定义 Magento 2 模块。本技能涵盖 Magento 2 模块骨架、基于 XML 的配置、Object Manager 和 DI 容器、自定义 REST/GraphQL API 端点,以及使用 db_schema.xml(声明式架构)管理数据库架构。

何时使用此技能

  • 为 Magento 2 商店构建添加新功能的自定义模块时
  • 使用插件或偏好设置扩展或覆盖 Magento 核心行为时
  • 为无头集成创建自定义 REST API 或 GraphQL 端点时
  • 使用声明式架构添加自定义数据库表时
  • 实现管理网格、表单和系统配置时

核心说明

  1. 创建模块骨架

    每个 Magento 2 模块都位于 app/code/Vendor/Module 中,且至少需要两个文件:

    app/code/Acme/CustomModule/
    ├── etc/
    │   └── module.xml
    ├── registration.php
    ├── Api/
    │   └── CustomRepositoryInterface.php
    ├── Model/
    │   ├── CustomRepository.php
    │   └── ResourceModel/
    ├── Controller/
    ├── Block/
    ├── view/
    │   ├── frontend/
    │   └── adminhtml/
    └── Setup/
        └── Patch/
            └── Data/
    
    // registration.php
    <?php
    declare(strict_types=1);
    
    use Magento\Framework\Component\ComponentRegistrar;
    
    ComponentRegistrar::register(
        ComponentRegistrar::MODULE,
        'Acme_CustomModule',
        __DIR__
    );
    
    
    <?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="Acme_CustomModule" setup_version="1.0.0">
            <sequence>
                <module name="Magento_Catalog"/>
                <module name="Magento_Sales"/>
            </sequence>
        </module>
    </config>
    
  2. 定义服务契约(接口)并实现它们

    服务契约可确保模块提供稳定的 API,供其他模块和集成依赖:

    // Api/Data/CustomEntityInterface.php
    <?php
    declare(strict_types=1);
    
    namespace Acme\CustomModule\Api\Data;
    
    interface CustomEntityInterface
    {
        const ENTITY_ID = 'entity_id';
        const NAME = 'name';
        const STATUS = 'status';
        const CREATED_AT = 'created_at';
    
        public function getEntityId(): ?int;
        public function getName(): string;
        public function setName(string $name): self;
        public function getStatus(): string;
        public function setStatus(string $status): self;
        public function getCreatedAt(): ?string;
    }
    
    // Api/CustomRepositoryInterface.php
    <?php
    declare(strict_types=1);
    
    namespace Acme\CustomModule\Api;
    
    use Acme\CustomModule\Api\Data\CustomEntityInterface;
    use Magento\Framework\Api\SearchCriteriaInterface;
    use Magento\Framework\Api\SearchResultsInterface;
    use Magento\Framework\Exception\NoSuchEntityException;
    
    interface CustomRepositoryInterface
    {
        /**
         * @throws NoSuchEntityException
         */
        public function getById(int $id): CustomEntityInterface;
    
        public function save(CustomEntityInterface $entity): CustomEntityInterface;
    
        public function delete(CustomEntityInterface $entity): bool;
    
        public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface;
    }
    
  3. 实现 Model、ResourceModel 和 Repository

    // Model/CustomEntity.php
    <?php
    declare(strict_types=1);
    
    namespace Acme\CustomModule\Model;
    
    use Acme\CustomModule\Api\Data\CustomEntityInterface;
    use Magento\Framework\Model\AbstractModel;
    
    class CustomEntity extends AbstractModel implements CustomEntityInterface
    {
        protected function _construct(): void
        {
            $this->_init(\Acme\CustomModule\Model\ResourceModel\CustomEntity::class);
        }
    
        public function getEntityId(): ?int
        {
            return $this->getData(self::ENTITY_ID) ? (int) $this->getData(self::ENTITY_ID) : null;
        }
    
        public function getName(): string
        {
            return (string) $this->getData(self::NAME);
        }
    
        public function setName(string $name): CustomEntityInterface
        {
            return $this->setData(self::NAME, $name);
        }
    
        public function getStatus(): string
        {
            return (string) $this->getData(self::STATUS);
        }
    
        public function setStatus(string $status): CustomEntityInterface
        {
            return $this->setData(self::STATUS, $status);
        }
    
        public function getCreatedAt(): ?string
        {
            return $this->getData(self::CREATED_AT);
        }
    }
    
    // Model/ResourceModel/CustomEntity.php
    <?php
    declare(strict_types=1);
    
    namespace Acme\CustomModule\Model\ResourceModel;
    
    use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
    
    class CustomEntity extends AbstractDb
    {
        protected function _construct(): void
        {
            $this->_init('acme_custom_entity', 'entity_id');
        }
    }
    
    // Model/CustomRepository.php
    <?php
    declare(strict_types=1);
    
    namespace Acme\CustomModule\Model;
    
    use Acme\CustomModule\Api\CustomRepositoryInterface;
    use Acme\CustomModule\Api\Data\CustomEntityInterface;
    use Acme\CustomModule\Model\ResourceModel\CustomEntity as ResourceModel;
    use Acme\CustomModule\Model\CustomEntityFactory;
    use Magento\Framework\Api\SearchCriteriaInterface;
    use Magento\Framework\Api\SearchResultsInterface;
    use Magento\Framework\Api\SearchResultsInterfaceFactory;
    use Magento\Framework\Exception\NoSuchEntityException;
    
    class CustomRepository implements CustomRepositoryInterface
    {
        public function __construct(
            private readonly ResourceModel $resourceModel,
            private readonly CustomEntityFactory $entityFactory,
            private readonly SearchResultsInterfaceFactory $searchResultsFactory,
            private readonly \Acme\CustomModule\Model\ResourceModel\CustomEntity\CollectionFactory $collectionFactory
        ) {}
    
        public function getById(int $id): CustomEntityInterface
        {
            $entity = $this->entityFactory->create();
            $this->resourceModel->load($entity, $id);
            if (!$entity->getEntityId()) {
                throw new NoSuchEntityException(
                    __('Entity with ID "%1" does not exist.', $id)
                );
            }
            return $entity;
        }
    
        public function save(CustomEntityInterface $entity): CustomEntityInterface
        {
            $this->resourceModel->save($entity);
            return $entity;
        }
    
        public function delete(CustomEntityInterface $entity): bool
        {
            $this->resourceModel->delete($entity);
            return true;
        }
    
        public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
        {
            $collection = $this->collectionFactory->create();
    
            foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
                foreach ($filterGroup->getFilters() as $filter) {
                    $collection->addFieldToFilter(
                        $filter->getField(),
                        [$filter->getConditionType() => $filter->getValue()]
                    );
                }
            }
    
            $searchResults = $this->searchResultsFactory->create();
            $searchResults->setSearchCriteria($searchCriteria);
            $searchResults->setItems($collection->getItems());
            $searchResults->setTotalCount($collection->getSize());
    
            return $searchResults;
        }
    }
    
  4. 使用 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">
    
        
        <preference for="Acme\CustomModule\Api\Data\CustomEntityInterface"
                    type="Acme\CustomModule\Model\CustomEntity"/>
        <preference for="Acme\CustomModule\Api\CustomRepositoryInterface"
                    type="Acme\CustomModule\Model\CustomRepository"/>
    
        
        <virtualType name="Acme\CustomModule\Model\ResourceModel\CustomEntity\Grid\Collection"
                     type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
            <arguments>
                <argument name="mainTable" xsi:type="string">acme_custom_entity</argument>
                <argument name="resourceModel"
                          xsi:type="string">Acme\CustomModule\Model\ResourceModel\CustomEntity</argument>
            </arguments>
        </virtualType>
    
        
        <type name="Acme\CustomModule\Model\SomeService">
            <arguments>
                <argument name="maxRetries" xsi:type="number">3</argument>
                <argument name="logger" xsi:type="object">Psr\Log\LoggerInterface</argument>
            </arguments>
        </type>
    </config>
    
  5. 使用声明式架构创建数据库表

    
    <?xml version="1.0"?>
    <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
        <table name="acme_custom_entity" resource="default" engine="innodb"
               comment="Acme Custom Entity Table">
            <column xsi:type="int" name="entity_id" unsigned="true" nullable="false"
                    identity="true" comment="Entity ID"/>
            <column xsi:type="varchar" name="name" nullable="false" length="255"
                    comment="Name"/>
            <column xsi:type="varchar" name="status" nullable="false" length="20"
                    default="active" comment="Status"/>
            <column xsi:type="decimal" name="amount" precision="12" scale="4"
                    nullable="true" comment="Amount"/>
            <column xsi:type="timestamp" name="created_at" nullable="false"
                    default="CURRENT_TIMESTAMP" comment="Created At"/>
            <column xsi:type="timestamp" name="updated_at" nullable="false"
                    default="CURRENT_TIMESTAMP" on_update="true" comment="Updated At"/>
            <constraint xsi:type="primary" referenceId="PRIMARY">
                <column name="entity_id"/>
            </constraint>
            <index referenceId="ACME_CUSTOM_ENTITY_STATUS" indexType="btree">
                <column name="status"/>
            </index>
        </table>
    </schema>
    

    修改 db_schema.xml 后生成白名单文件:

    bin/magento setup:db-declaration:generate-whitelist --module-name=Acme_CustomModule
    
  6. 使用插件(拦截器)修改核心行为

    // Plugin/ProductPricePlugin.php
    <?php
    declare(strict_types=1);
    
    namespace Acme\CustomModule\Plugin;
    
    use Magento\Catalog\Model\Product;
    
    class ProductPricePlugin
    {
        /**
         * After-plugin: modify the return value of getPrice()
         */
        public function afterGetPrice(Product $subject, float $result): float
        {
            // Example: apply a 10% surcharge for a specific attribute
            if ($subject->getData('requires_special_handling')) {
                return $result * 1.10;
            }
            return $result;
        }
    
        /**
         * Before-plugin: modify input arguments
         */
        public function beforeSetPrice(Product $subject, $price): array
        {
            // Ensure price is never negative
            return [max(0, (float) $price)];
        }
    
        /**
         * Around-plugin: wrap the original method (use sparingly)
         */
        public function aroundGetName(Product $subject, callable $proceed): string
        {
            $name = $proceed();
            $badge = $subject->getData('custom_badge');
            return $badge ? "[{$badge}] {$name}" : $name;
        }
    }
    

    在 di.xml 中注册插件:

    <type name="Magento\Catalog\Model\Product">
        <plugin name="acme_custom_price_plugin"
                type="Acme\CustomModule\Plugin\ProductPricePlugin"
                sortOrder="10"/>
    </type>
    

示例

自定义 REST API 端点
// etc/webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
    <route url="/V1/acme/custom-entities" method="GET">
        <service class="Acme\CustomModule\Api\CustomRepositoryInterface" method="getList"/>
        <resources>
            <resource ref="Magento_Catalog::catalog"/>
        </resources>
    </route>
    <route url="/V1/acme/custom-entities/:id" method="GET">
        <service class="Acme\CustomModule\Api\CustomRepositoryInterface" method="getById"/>
        <resources>
            <resource ref="anonymous"/>
        </resources>
    </route>
    <route url="/V1/acme/custom-entities" method="POST">
        <service class="Acme\CustomModule\Api\CustomRepositoryInterface" method="save"/>
        <resources>
            <resource ref="Magento_Catalog::catalog"/>
        </resources>
    </route>
</routes>
订单创建后事件的观察者
// Observer/OrderPlaceAfterObserver.php
<?php
declare(strict_types=1);

namespace Acme\CustomModule\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Sales\Model\Order;
use Psr\Log\LoggerInterface;

class OrderPlaceAfterObserver implements ObserverInterface
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly \Acme\CustomModule\Model\ExternalSyncService $syncService
    ) {}

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

        try {
            $this->syncService->pushOrder($order);
            $this->logger->info(
                sprintf('Order #%s synced to external system.', $order->getIncrementId())
            );
        } catch (\Exception $e) {
            // Log but do not block order placement
            $this->logger->error(
                sprintf('Failed to sync order #%s: %s', $order->getIncrementId(), $e->getMessage())
            );
        }
    }
}

<?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_order_place_after">
        <observer name="acme_order_sync" instance="Acme\CustomModule\Observer\OrderPlaceAfterObserver"/>
    </event>
</config>

最佳实践

  • 始终使用服务契约——注入接口(CustomRepositoryInterface),而非具体类;这样可确保与 REST、GraphQL 和其他模块兼容
  • 优先使用插件,而非类重写——插件(拦截器)可组合使用,允许多个模块修改同一方法;偏好设置(重写)会导致冲突
  • 谨慎使用 around 插件——它们会包装整个方法调用;如果忘记调用 $proceed(),其他插件将无法执行;优先使用 before/after 插件
  • 在 module.xml 中声明模块依赖项——<sequence> 节点可确保模块在其依赖项之后加载;缺少序列会导致不易察觉的加载顺序错误
  • 使用声明式架构(db_schema.xml)——避免使用旧式 InstallSchema/UpgradeSchema 脚本;声明式架构具有幂等性并支持回滚
  • 切勿直接使用 Object Manager——始终使用构造函数依赖注入;直接调用 ObjectManager::getInstance() 会绕过 DI 配置,并破坏可测试性
  • 更改后运行 bin/magento setup:di:compile——DI 编译步骤会生成拦截器和工厂;缺少编译会导致生产模式下出现“class not found”错误
  • 遵循 Magento 编码标准——发布前对模块运行 vendor/bin/phpcs --standard=Magento2

常见问题

问题解决方案
添加新类后出现“Class does not exist”运行 bin/magento setup:di:compile 和 bin/magento cache:flush;检查命名空间是否与目录路径完全匹配
插件未执行确认插件已在正确范围的 di.xml 中注册(前端使用 etc/frontend/di.xml,全局使用 etc/di.xml),并检查 sortOrder 是否冲突
声明式架构更改未生效运行 bin/magento setup:upgrade,并使用 setup:db-declaration:generate-whitelist 重新生成白名单
循环依赖注入错误重构其中一个依赖类,在 di.xml 中使用 Proxy(\Acme\CustomModule\Model\SomeClass\Proxy)来打破循环
观察者抛出异常并阻止结账使用 try/catch 包裹观察者逻辑;观察者应记录错误,但绝不能抛出会阻止关键流程的异常
找不到工厂类工厂类由 DI 编译自动生成;运行 bin/magento setup:di:compile,或检查基类是否存在

相关技能

  • @product-data-modeling
  • @erp-integration
  • @ecommerce-caching
  • @pci-dss-compliance
  • @ecommerce-seo

评论

支持 Markdown