开发 Magento2 的模块
- 这是在 magento2.4 上开发的
- magento2 的组件有三种
- modules 模块
- themes 主题
- language packages 语言包
- 其中 主题 只包含前端代码,模块可以包含 主题 和 语言包
新建模块的代码
- 假设已经安装好 magento2
- 新建模块的代码
- 启用模块 和 刷新缓存
模块的路径是这样的,开发商名称和模块名称都使用 大驼峰 的形式命名
app/code/开发商名称/模块名称
默认路由是这样的
routeid/controller/action
最简单的例子
- 新建模块目录
app/code/LocalDev/HelloModule
- 在模块目录下新建 registration.php 并写入以下内容
<?php \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'LocalDev_HelloModule', __DIR__ );
- 在模块目录下新建 etc 文件夹,在 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="LocalDev_HelloModule" setup_version="1.0.9"></module> </config>
- 新建路由,在 etc 文件夹下新建 frontend 文件夹,在 frontend 下新建 routes.xml 并写入以下内容
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/routes.xsd"> <router id="standard"> <route id="localdev" frontName="localdev"> <module name="LocalDev_HelloModule" /> </route> </router> </config>
- 这是一个前台的路由,如果是后台的路由,那么需要在 etc/adminhtml 下新建 routes.xml
- frontName 就是 route id
- 新建 Controller 和 action
- 在模块目录下新建 Controller 文件夹
- 在 Controller 文件夹下,新建一个以控制器名称命名的文件名,例如
Hello
- 在 控制器 文件夹下,新建一个以方法名命名的文件,例如
World.php
- 在方法的文件里写入以下内容
<?php namespace LocalDev\HelloModule\Controller\Hello; class World extends \Magento\Framework\App\Action\Action { public function __construct( \Magento\Framework\App\Action\Context $context, ) { parent::__construct($context); } public function execute() { /** @var \Magento\Backend\Model\View\Result\Page $result */ $result = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE); return $result; } }
- 新建视图
- 在模块目录下新建 view 文件夹
- 在 view 文件夹下,新建一个 frontend 文件夹
- 在 frontend 文件夹下,新建一个 layout 文件夹 和 一个 templates 文件夹
- 在 layout 文件夹下,新建一个以路由命名的 xml 文件,例如
localdev_hello_world.xml
- 在 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> <referenceContainer name="content"> <block class="Magento\Framework\View\Element\Template" template="LocalDev_HelloModule::container.phtml" name="localdev.container"/> </referenceContainer> </body> </page>
- 在 templates 文件夹下,新建一个名为
container.phtml
的文件,这个文件名要和 xml 文件里的 template 属性对应 - 在 phtml 文件里写入以下内容
<?php /** @var \Magento\Framework\View\Element\Template $block */ ?> <p><?=$block->getBaseUrl()?></p>
完整的模块目录结构是这样的
app
code
LocalDev
HelloModule
Controller
Hello
World.php
etc
frontend
routes.xml
module.xml
view
frontend
layout
localdev_hello_world.xml
templates
container.phtml
registration.php
启用模块和刷新缓存后,访问这样的链接 http://localhost-magento/localdev/hello/world
,应该就能看到 hello world
的输出
启用模块 和 刷新缓存
查看启用的模块
php bin/magento module:status
启用模块
php bin/magento module:enable 模块名
禁用模块
php bin/magento module:disable 模块名
刷新缓存
php bin/magento cache:clean 清除缓存
php bin/magento setup:upgrade 更新数据 Upgrades the Magento application, DB data, and schema
php bin/magento setup:di:compile 编译
php bin/magento setup:static-content:deploy -f 部署静态视图文件
php bin/magento indexer:reindex 刷新全部索引
php bin/magento cache:flush 刷新缓存
模块的代码修改后也要刷新缓存
目录结构
app
code 模块
metapackage 开发商
module 模块
Api
Block
Console
Controller
Cron
etc
areaCode
... 直接写在 etc 目录下的配置是全局的,写在 areaCode 文件下的配置只在对应的 areaCode 下生效
di.xml
events.xml
view.xml
cron_groups.xml
crontab.xml
logging.xml
module.xml
acl.xml
config.xml
routes.xml
system.xml
db_schema_whitelist.json
db_schema.xml
menu.xml
resources.xml
widget.xml
schema.graphqls
Helper
Model
Indexer
Observer
Plugin
Setup
Test
Ui
view
areaCode 区域代码 就是 frontend adminhtml 这种
layout
*.xml
这个目录下的 xml 文件是布局配置文件
这些 xml 的文件名是对应路由的,也就是和路由名称一样
page_layout
这个目录下的 xml 文件就是页面布局文件,文件名就是布局id
ui_component 也是放 xml 文件,但还不知道有什么用
这里的 xml 文件可以在 layout 里引用
templates
*.phtml
web
css
fonts
images
js
action
model
view
这个文件夹下的 js 就是前端的 component ,继承自 magento2 的 uiComponent
这个文件夹下的 js 应该实和 template 里的 html 文件一一对应的,
但也可以在 js 里修改模板的路径
*.js 直接放在 js 目录下的通常是 jq 的 widget
template
这里放的是 html 文件
这些 html 文件通常是 ko 的模板
component 通过 ajax 获取这些模板
layouts.xml 用于声明有哪些布局
requirejs-config.js 用来声明 requirejs 的配置,例如 js 的加载顺序
i18n
其它的文件夹
ViewModel
CustomerData
composer.json
registration.php
design 主题
areaCode 区域代码, frontend 是前台, adminhtml 是后台
开发商
主题 -> 优先级是高于 模块 里的文件
开发商_模块名 -> 和 模块里的 view 文件夹是一样的
etc
view
web
css
fonts
images
js
template
media
composer.json
registration.php
theme.xml
etc 全局配置
i18n 语言包
bin
magento
dev
generated
lib
internal
web
phpserver
pub
static
cron.php
get.php
health_check.php
index.php
static.php
setup
var
vendor
composer.json
新建模型
新建或在 db_schema.xml 文件里添加
<?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="test_model" resource="default" engine="innodb" comment="Test Model"> <column xsi:type="int" name="entity_id" nullable="false" identity="true"/> <column xsi:type="int" name="customer_id" nullable="false" comment="customer_id"/> <column xsi:type="varchar" name="type" nullable="false" length="64" comment="type"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> </table> </schema>
新建 resource model
- 在模块目录 model/ResourceModel 文件夹下新建 TestModel.php
<?php namespace Vendor\Extension\Model\ResourceModel; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; class TestModel extends AbstractDb { const TABLE_NAME = 'test_model'; protected function _construct() { $this->_init(self::TABLE_NAME, 'entity_id'); } }
新建 model
- 在模块目录 model 文件夹下新建 TestModel.php
<?php namespace Vendor\Extension\Model; use Magento\Framework\Model\AbstractModel; class TestModel extends AbstractModel { protected function _construct() { $this->_init(Vendor\Extension\Model\ResourceModel\TestModel::class); } }
新建 collection
- 在模块目录 model/ResourceModel/TestModel 文件夹(这里的 TestModel 对应的是模型名)下新建 Collection.php
<?php namespace Vendor\Extension\Model\ResourceModel\TestModel; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; class Collection extends AbstractCollection { protected function _construct() { $this->_init(Vendor\Extension\Model\TestModel::class, Vendor\Extension\Model\ResourceModel\TestModel::class); } }
然后运行这句新建 db_schema_whitelist.json
php bin/magento setup:db-declaration:generate-whitelist --module-name=Extension
最后运行这句就能新建一个对应的表了
php bin/magento setup:upgrade
- Magento的 模型系统 分为四部分
- 模型 (Model)
- 资源模型 (ResourceModel)
- 集合 (Collection)
- 工厂 (Factory)
- 模型是一个抽象的对象
- 资源模型会对应数据库里的表,模型的增删查改通过资源模型进行,例如 资源模型->save(模型)
- 集合就是模型的集合,一些查询操作也是在集合里进行
- 工厂用于创建模型或集合,工厂类一般是由 magento2 自动生成的,用编译的命令 php bin/magento setup:di:compile
EAV
EAV(实体 - 属性 - 值) entity attribute value
保存 eav 属性的表
eav_attribute 保存 eav 的属性
eav_entity_type 保存 eav 的类
输出某个 eav 类的全部 eav 属性
SELECT * FROM eav_attribute
WHERE entity_type_id = (
SELECT entity_type_id FROM eav_entity_type
WHERE entity_table = 'catalog_product_entity' LIMIT 1
)
eav 的五种属性
varchar
int
text
datetime
decimal
常见的 eav 类,可以在这个表里看到 eav_entity_type
catalog_category_entity
catalog_product_entity
customer_entity
customer_address_entity
eav 的值保存在这类表中
类名_entity
类名_varchar
类名_int
类名_text
类名_datetime
类名_decimal
一次输出 eav 对象全部属性的 sql ,用于一般的 eav 对象
$entityId = '3893';
$entityTabel = 'customer_entity';
$eavTable = [
'varchar',
'int',
'text',
'datetime',
'decimal',
];
$eavTpl = <<<'EOF'
(SELECT `t`.`value_id`,
`t`.`value`,
`t`.`attribute_id`,
`a`.`attribute_code`,
'%s' as `type`
FROM `%s` AS `t`
INNER JOIN `eav_attribute` AS `a`
ON a.attribute_id = t.attribute_id
WHERE (entity_id = @entity_id))
EOF;
$eavSql = join('UNION ALL', array_map(function($item) use ($eavTpl, $entityTabel) {
return sprintf($eavTpl, $item, $entityTabel . '_' . $item);
}, $eavTable));
$entityTpl = <<<'EOF'
select
*,
@entity_id := entity_id
from %s
where entity_id = %s
limit 1;
EOF;
$entitySql = sprintf($entityTpl, $entityTabel, $entityId);
$retSql = $entitySql . PHP_EOL . $eavSql . ';';
echo $retSql;
一次输出 eav 对象全部属性的 sql ,用于 product 和 category 的
$entityId = '3893';
$entityTabel = 'catalog_product_entity'; // catalog_category_entity
$eavTable = [
'varchar',
'int',
'text',
'datetime',
'decimal',
];
$eavTpl = <<<'EOF'
(SELECT `t`.`value_id`,
`t`.`value`,
`t`.`store_id`,
`t`.`attribute_id`,
`a`.`attribute_code`,
'%s' as `type`
FROM `%s` AS `t`
INNER JOIN `eav_attribute` AS `a`
ON a.attribute_id = t.attribute_id
WHERE (row_id = @row_id))
EOF;
$eavSql = join('UNION ALL', array_map(function($item) use ($eavTpl, $entityTabel) {
return sprintf($eavTpl, $item, $entityTabel . '_' . $item);
}, $eavTable));
$entityTpl = <<<'EOF'
select
*,
@row_id := row_id
from %s
where entity_id = %s and UNIX_TIMESTAMP(NOW()) >= created_in AND UNIX_TIMESTAMP(NOW()) < updated_in
order by row_id desc;
EOF;
$entitySql = sprintf($entityTpl, $entityTabel, $entityId);
$retSql = $entitySql . PHP_EOL . $eavSql . ';';
echo $retSql;
新建命令
在模块目录下 etc/di.xml 加上以下内容
<type name="Magento\Framework\Console\CommandList"> <arguments> <argument name="commands" xsi:type="array"> <item name="exampleSayHello" xsi:type="object">Vendor\Extension\Console\SayHello</item> </argument> </arguments> </type>
- 如果是多条命令, item 可以多写几条
- item 的值是运行命令的类的命名空间
在模块目录里新建一个文件夹 Console ,在这个新建的文件夹里新建一个文件 SayHello.php 并写入以下内容
<?php namespace Vendor\Extension\Console; 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 SayHello extends Command { const NAME = "name"; protected function configure() { $options = [ new InputOption(self::NAME, null, InputOption::VALUE_REQUIRED, 'a description text') ]; $this->setName("example:sayhello") // 命令的名字 ->setDescription('example description') // 命令的描述 ->setDefinition($options); parent::configure(); } protected function execute(InputInterface $input, OutputInterface $output) { if ($name = $input->getOption(self::NAME)) { $output->writeln('hello ' . $name); } else { $output->writeln('hello world'); } } }
- configure 方法里的 setName 就是设置命令的运行名称,例如上面的例子,的运行命令就是
php bin/magento example:sayhello
- configure 方法里的 setName 就是设置命令的运行名称,例如上面的例子,的运行命令就是
运行这句命令
php bin/magento setup:upgrade
更新数据可以尝试运行这条命令
php bin/magento list
,看看能不能找到新加的命令最后运行上面新加的命令
php bin/magento example:sayhello
参考 https://developer.adobe.com/commerce/php/development/cli-commands/custom/
新建 rest 的接口
新建 etc\webapi.xml
<route url="/V1/gtm-layer/mine/quote-item-data" method="POST">
<service class="Vendor\Extension\Api\GtmCartRepositoryInterface" method="getQuoteItemData"/>
<resources>
<resource ref="self" />
</resources>
<data>
<parameter name="itemId">%item_id%</parameter>
<parameter name="qty">%qty%</parameter>
</data>
</route>
- data 标签里的参数是可以不要的
<resource ref="self" />
需要登录才能调用<resource ref="anonymous" />
不用登录也能调用
新建 app\code\Vendor\Extension\Api\GtmCartRepositoryInterface.php
<?php
namespace Vendor\Extension\Api;
interface GtmCartRepositoryInterface
{
/**
* @param string $itemId
* @param int $qty
* @return array
* @throws \Magento\Framework\Webapi\Exception
*/
public function getQuoteItemData($itemId, $qty = 0);
}
新建 app\code\Vendor\Extension\Model\GtmCartRepository.php
<?php
namespace Vendor\Extension\Model;
class GtmCartRepository implements GtmCartRepositoryInterface
{
public function getQuoteItemData($itemId, $qty = 0)
{
return [];
}
}
如果是新模块则需要运行一次 setup:upgrade 才能生效。 如果是旧模块则需要运行一次 cache:clear 就能生效。
调用的例子
curl -X POST https://dev.magento.com/rest/en_US/V1/gtm-layer/mine/quote-item-data -k -H "Content-Type: application/json" -d '{"productIds":["3893"]}'
curl -X POST https://dev.magento.com/rest/en_US/V1/gtm-layer/mine/quote-item-data -k -H "Content-Type: application/json" -d '{"itemId":3893,qty:1}'
新建 GraphQl 的接口
在模块目录 etc 下新建一个文件 schema.graphqls 并写入以下内容
type Query { CustomGraphql ( username: String @doc(description: "Email Address/Mobile Number") password: String @doc(description: "Password") websiteId: Int = 1 @doc (description: "Website Id") ): CustomGraphqlOutput @resolver(class: "Vendor\\Extension\\Model\\Resolver\\CustomGraphql") @doc(description:"Custom Module Datapassing") } type CustomGraphqlOutput { customer_id: Int type: String type_id: Int }
- CustomGraphql 是请求的参数
- CustomGraphqlOutput 是返回的参数
- @resolver 是处理请求的类
在模块目录 Model 下新建一个文件夹 Resolver ,然后再在这个文件夹里新建一个类文件 CustomGraphql.php 并写入以下内容
<?php namespace Vendor\Extension\Model\Resolver; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; class CustomGraphql implements ResolverInterface { /** * @param Field $field * @param \Magento\Framework\GraphQl\Query\Resolver\ContextInterface $context * @param ResolveInfo $info * @param array|null $value * @param array|null $args * @return array|\Magento\Framework\GraphQl\Query\Resolver\Value|mixed * @throws GraphQlInputException */ public function resolve( Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { if (!isset($args['username']) || !isset($args['password']) || !isset($args['websiteId'])|| empty($args['username']) || empty($args['password']) || empty($args['websiteId'])) { throw new GraphQlInputException(__('Invalid parameter list.')); } $output = []; $output['customer_id'] = 123; $output['type'] = 'type'; $output['type_id'] = 321; return $output ; } }
- $output 的内容需要和 schema.graphqls 里定义的返回参数一致
运行这句命令
php bin/magento setup:upgrade
更新数据- 在开发模式下
php bin/magento c:c
就能使 schema.graphqls 的修改生效
- 在开发模式下
用这句 curl 命令尝试请求
graphqlquery=$(cat <<- EOF query { CustomGraphql(username: 123, password: "asd", websiteId: 321) { customer_id type type_id } } EOF ); graphqlquery=$(echo -n $graphqlquery | php -r '$data=file_get_contents("php://stdin");print(json_encode($data));'); graphqlquery='{"query":'$graphqlquery',"variables":{},"operationName":null}'; curl 'http://localhost-magento/graphql' \ -H 'accept: application/json' \ -H 'content-type: application/json' \ --data-raw "$graphqlquery" \ --compressed \ --insecure -s -k
- 如无意外应该能返回类似这样的数据
{ "data": { "CustomGraphqlOutput": { "customer_id": 123, "type": "asd", "type_id": 321 } } }
可以用这决 curl 命令来查看当前 magento 项目的 graphql 文档
graphqlquery=$(cat <<- EOF query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } EOF ); graphqlquery=$(echo -n $graphqlquery | php -r '$data=file_get_contents("php://stdin");print(json_encode($data));'); graphqlquery='{"query":'$graphqlquery',"variables":{},"operationName":null}'; curl 'http://localhost-magento/graphql' \ -H 'accept: application/json' \ -H 'content-type: application/json' \ --data-raw "$graphqlquery" \ --compressed \ --insecure -s -k
graphql 里只有这个文件夹下的异常能显示出来,其它的异常都是显示 server error
- vendor\magento\framework\GraphQl\Exception
如果要自定义异常,最好继承 grapqhl 里原本的异常,或实现这个接口 \GraphQL\Error\ClientAware
- 关键还是这个接口 \GraphQL\Error\ClientAware
graphql 接口大概的执行位置
- vendor\magento\module-graph-ql\Controller\GraphQl.php
- vendor\webonyx\graphql-php\src\Executor\ReferenceExecutor.php doExecute
- vendor\webonyx\graphql-php\src\Executor\ReferenceExecutor.php executeOperation
- vendor\webonyx\graphql-php\src\Executor\ReferenceExecutor.php resolveField
- vendor\webonyx\graphql-php\src\Executor\ReferenceExecutor.php resolveOrError
Copyright ©2022 f2h2h1 | All Rights Reserved