- 这是在 magento2.4 上开发的
- magento2 的组件(components)有三种
- modules 模块
- themes 主题
- language packages 语言包
- 模块 可以包含 主题 和 语言包
- 主题 原则上只包含前端代码
实际上可以在phtml文件里搞各种奇技淫巧
新建模块的代码
- 假设已经安装好 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
Fixture
Mftf
Unit
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 的类
SELECT * FROM eav_entity_type
输出某个 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));
$eavSql = 'select * from (' . $eavSql . ') as temptable order by attribute_code';
$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));
$eavSql = 'select * from (' . $eavSql . ') as temptable order by attribute_code';
$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;
增加 eav 属性
通过后台新建
catalog_product_entity 的 eav 属性可以通过 product 的设置页新建
通过模块里的 setup 新建
- 参考这个文档 https://experienceleague.adobe.com/en/docs/commerce-learn/tutorials/backend-development/add-product-attribute
- 在模块里的 setup 文件夹下新建 InstallData.php 或 AddClothingMaterial.php
- app/code/Learning/ClothingMaterial/Setup/InstallData.php
- 这种方式, etc/module.xml 里必须有 setup_version="0.0.1" 才会生效
- app/codeLearning/ClothingMaterial/Setup/Patch/Data/AddClothingMaterial.php
- 这种方式,只要 upgrade 都会生效,但重复创建属性可能会导致 upgrade 是报错
- setup:upgrade 跑完之后 patch_list 表会有记录的,如果有记录,下次 setup:upgrade 就不会跑了
- app/code/Learning/ClothingMaterial/Setup/InstallData.php
- 新建好对应文件后,运行一次 upgrade 命令,后台的配置页面就自动有对应的属性设置了
app/code/Learning/ClothingMaterial/Setup/InstallData.php
<?php
declare(strict_types=1);
namespace Learning\ClothingMaterial\Setup;
use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
class InstallData implements InstallDataInterface
{
protected $eavSetupFactory;
public function __construct(
\Magento\Eav\Setup\EavSetupFactory $eavSetupFactory
)
{
$this->eavSetupFactory = $eavSetupFactory;
}
/**
* @param ModuleDataSetupInterface $setup
* @param ModuleContextInterface $context
* {@inheritDoc}
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @suppressWarnings(PHPMD.ExessiveMethodLength)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$eavSetup = $this->eavSetupFactory->create();
$eavSetup->addAttribute(
Product::ENTITY,
'app_only',
[
'group' => 'General',
'type' => 'int',
'label' => 'App Only',
'input' => 'boolean',
'source' => \Magento\Eav\Model\Entity\Attribute\Source\Boolean::class,
'required' => false,
'sort_order' => 50,
'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
'is_used_in_grid' => false,
'is_visible_in_grid' => false,
'is_filterable_in_grid' => false,
'visible' => true,
'is_html_allowed_on_frontend' => true,
'visible_on_front' => true,
]
);
}
}
app/codeLearning/ClothingMaterial/Setup/Patch/Data/AddClothingMaterial.php
<?php
namespace Learning\ClothingMaterial\Setup\Patch\Data;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Learning\ClothingMaterial\Model\Attribute\Frontend\Material as Frontend;
use Learning\ClothingMaterial\Model\Attribute\Source\Material as Source;
use Learning\ClothingMaterial\Model\Attribute\Backend\Material as Backend;
use Magento\Catalog\Model\Product;
class AddClothingMaterial implements DataPatchInterface
{
/**
* @var ModuleDataSetupInterface
*/
private $moduleDataSetup;
/**
* @var EavSetupFactory
*/
private $eavSetupFactory;
/**
* AddInstallmentProductAttributes constructor.
* @param ModuleDataSetupInterface $moduleDataSetup
* @param EavSetupFactory $eavSetupFactory
*/
public function __construct(
ModuleDataSetupInterface $moduleDataSetup,
EavSetupFactory $eavSetupFactory
) {
$this->moduleDataSetup = $moduleDataSetup;
$this->eavSetupFactory = $eavSetupFactory;
}
/**
* @return array|string[]
*/
public static function getDependencies()
{
return [];
}
/**
* @return array|string[]
*/
public function getAliases()
{
return [];
}
/**
* @inheritDoc
*/
public function apply()
{
$eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
try {
$eavSetup->addAttribute(
Product::ENTITY,
'clothing_material',
[
'group' => 'General',
'type' => 'varchar',
'label' => 'Clothing Material',
'input' => 'select',
'source' => Source::class,
'frontend' => Frontend::class,
'backend' => Backend::class,
'required' => false,
'sort_order' => 50,
'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
'is_used_in_grid' => false,
'is_visible_in_grid' => false,
'is_filterable_in_grid' => false,
'visible' => true,
'is_html_allowed_on_frontend' => true,
'visible_on_front' => true,
]
);
} catch (LocalizedException $e) {
} catch (\Zend_Validate_Exception $e) {
}
}
}
新建命令
在模块目录下 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 -kgraphql 里只有这个文件夹下的异常能显示出来,其它的异常都是显示 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
可以用类似这样的方式直接执行 graphql 的查询
try { // 引入 magento2 的引导文件 require __DIR__ . '/app/bootstrap.php'; // 创建一个应用对象 $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); // 获取一个对象管理器 $objectManager = $bootstrap->getObjectManager(); // 如果出现这种错误 area code is not set ,则加上这两句, area 的值可以根据实际场景修改 $state = $objectManager->get(\Magento\Framework\App\State::class); $state->setAreaCode(\Magento\Framework\App\Area::AREA_GRAPHQL); // app\code\Magento\Authorization\Model\UserContextInterface.php /** @var \Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface */ $schemaGenerator = $objectManager->get(\Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface::class); $source = ''; // 这个位置填 graphql 的 查询语句 // $source = <<<EOF // { // currency { // available_currency_codes // base_currency_code // base_currency_symbol // default_display_currecy_code // default_display_currecy_symbol // default_display_currency_code // default_display_currency_symbol // exchange_rates { // currency_to // rate // } // } // } // EOF; $rootValue = null; $contextValue = new class implements \Magento\GraphQl\Model\Query\ContextInterface { public function getUserId(): ?int { return 123; } public function getUserType(): ?int { return \Magento\Authorization\Model\UserContextInterface::USER_TYPE_CUSTOMER; } public function getExtensionAttributes(): \Magento\GraphQl\Model\Query\ContextExtensionInterface { return new class implements \Magento\GraphQl\Model\Query\ContextExtensionInterface { private $store; private $isCustomer; private $customerGroupId; public function __construct() { /** @var \Magento\Customer\Model\CustomerFactory */ $customerFactory = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Customer\Model\CustomerFactory::class); // $customer = $customerFactory->create()->loadByEmail($email); // $customer->getGroupId(); /** @var \Magento\Store\Model\StoreManager */ $storeManager = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Store\Model\StoreManager::class); $storeManager->getStore(); $this->setStore($storeManager->getStore()); $this->setIsCustomer(false); $this->setCustomerGroupId(null); } public function getStore() { return $this->store; } public function setStore(\Magento\Store\Api\Data\StoreInterface $store) { $this->store = $store; return $this; } public function getIsCustomer() { return $this->isCustomer; } public function setIsCustomer($isCustomer) { $this->isCustomer = $isCustomer; return $this; } public function getCustomerGroupId() { return $this->customerGroupId; } public function setCustomerGroupId($customerGroupId) { $this->customerGroupId = $customerGroupId; return $this; } }; } }; $variables = []; $result = \GraphQL\GraphQL::executeQuery($schemaGenerator->generate(), $source, $rootValue, $contextValue, $variables); $output = $result->toArray(); echo json_encode($output); // 这种方式也可以 // $variables = []; /** @var \Magento\Framework\GraphQl\Query\QueryProcessor */ // $queryProcessor = $objectManager->get(\Magento\Framework\GraphQl\Query\QueryProcessor\Interceptor::class); // $result = $queryProcessor->process($schemaGenerator->generate(), $source, $contextValue, $variables); // var_dump($result); } catch (\Throwable $e) { echo $e->getFile() . ':' . $e->getLine() . PHP_EOL; echo $e->getMessage() . PHP_EOL . $e->getTraceAsString(); }通过账号密码生成用于请求的 token
mutation Login { generateCustomerToken(email: "test.test.test@test.com", password: "12345678") { token __typename } }通过 js 发起 graphql 请求
- 原生 js
(function(){ const query = ` query { customer { email firstname lastname } } `; fetch(`${window.location.origin}/graphql`, { method: 'POST', // 如果不是 mutation 类型的请求,这个位置可以改成 GET // headers: { // 登录的 token 加载这个位置,如果在网页里登录了,就不需要 // 'authorization': 'Bearer jh1i138qywjx97yzskkji15v6oxyp2an' // }, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query }) }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); })()- jQuery
(function(){ const query = ` query { customer { email firstname lastname } } `; jQuery.ajax({ url: `${window.location.origin}/graphql`, type: "POST", // 如果不是 mutation 类型的请求,这个位置可以改成 GET contentType: "application/json", data: `{"query":${JSON.stringify(query)}}`, success: function(data){ console.log(data); } }); })();- jQuery + require
(function(){ require(['jquery'], function($) { const query = ` query { customer { email firstname lastname } } `; $.ajax({ url: `${window.location.origin}/graphql`, type: "POST", // 如果不是 mutation 类型的请求,这个位置可以改成 GET contentType: "application/json", data: `{"query":${JSON.stringify(query)}}`, success: function(data){ console.log(data); } }); }); })();通过命令行生成用于请求的 token
try { // 引入 magento2 的引导文件 require __DIR__ . '/app/bootstrap.php'; // 创建一个应用对象 $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); // 获取一个对象管理器 $objectManager = $bootstrap->getObjectManager(); // 如果出现这种错误 area code is not set ,则加上这两句, area 的值可以根据实际场景修改 $areaCode = \Magento\Framework\App\Area::AREA_GRAPHQL; $objectManager->get(\Magento\Framework\App\State::class)->setAreaCode($areaCode); $objectManager->configure( $objectManager ->get(\Magento\Framework\App\ObjectManager\ConfigLoader::class) ->load($areaCode) ); $eventManager = $objectManager->get(\Magento\Framework\Event\ManagerInterface::class); $tokenParametersFactory = $objectManager->get(\Magento\Integration\Model\UserToken\UserTokenParametersFactory::class); $tokenIssuer = $objectManager->get(\Magento\Integration\Api\UserTokenIssuerInterface::class); $storeManager = $objectManager->get(\Magento\Store\Model\StoreManager::class); $email = ''; // 这个位置填 customer 的 邮箱 /** @var \Magento\Customer\Model\CustomerFactory */ $customerFactory = $objectManager->get(\Magento\Customer\Model\CustomerFactory::class); $customer = $customerFactory->create()->setWebsiteId($storeManager->getStore()->getWebsiteId())->loadByEmail($email); $customerDataObject = $customer->getDataModel(); $eventManager->dispatch('customer_login', ['customer' => $customerDataObject]); $context = new \Magento\Integration\Model\CustomUserContext( (int)$customerDataObject->getId(), \Magento\Integration\Model\CustomUserContext::USER_TYPE_CUSTOMER ); $params = $tokenParametersFactory->create(); echo $tokenIssuer->create($context, $params); } catch (\Throwable $e) { echo $e->getFile() . ':' . $e->getLine() . PHP_EOL; echo $e->getMessage() . PHP_EOL . $e->getTraceAsString(); }通过数据库生成用于请求的 token
- graphql 的 token 保存在这个表里 oauth_token
- 在数据库里运行这三句,就能直接生成 token 了
SELECT @customer_id := entity_id FROM `customer_entity` WHERE (email = 'test.test.test@test.com'); INSERT INTO oauth_token (consumer_id,admin_id,customer_id,`type`,token,secret,verifier,callback_url,revoked,authorized,user_type,created_at,partner_id) VALUES (NULL,NULL,@customer_id,'access',REPLACE(UUID(), '-', ''),REPLACE(UUID(), '-', ''),NULL,'',0,0,3,now(),NULL); select * from oauth_token where customer_id = @customer_id order by created_at desc limit 1;
Copyright ©2024 f2h2h1 | All Rights Reserved
{"operationName":"Login","variables":{"email":"test.test.test@test.com","password":"passw!1234"},"query":"mutation Login($email: String\u0021, $password: String\u0021) {\\n generateCustomerToken(email: $email, password: $password) {\\n token\\n __typename\\n }\\n}\\n"}' \ --compressed -k --no-progress-meter 可以用这句curl验证生成的token curl 'https://magento2.localhost.com/graphql?query=%20%20%20%20%20%20%20%20query%20\{%20%20%20%20%20%20%20%20%20%20customer%20\{%20%20%20%20%20%20%20%20%20%20%20%20email%20%20%20%20%20%20%20%20%20%20%20%20firstname%20%20%20%20%20%20%20%20%20%20%20%20lastname%20%20%20%20%20%20%20%20%20%20\}%20%20%20%20%20%20%20%20\}%20%20%20%20%20%20%20%20' \ -H 'authorization: Bearer 10ed30a93cdc11ee9d3b0e4ba1986f92' \ -H 'accept: */*' \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ --cookie "XDEBUG_SESSION=vscode" \ --compressed -k --no-progress-meter 可以用 json_pp 来格式化 curl 的输出,前提是 curl 里的输出是 json 字符串 像这样 curl 'https://magento2.localhost.com/graphql?query=%20%20%20%20%20%20%20%20query%20\{%20%20%20%20%20%20%20%20%20%20customer%20\{%20%20%20%20%20%20%20%20%20%20%20%20email%20%20%20%20%20%20%20%20%20%20%20%20firstname%20%20%20%20%20%20%20%20%20%20%20%20lastname%20%20%20%20%20%20%20%20%20%20\}%20%20%20%20%20%20%20%20\}%20%20%20%20%20%20%20%20' \ -H 'authorization: Bearer 10ed30a93cdc11ee9d3b0e4ba1986f92' \ -H 'accept: */*' \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ --cookie "XDEBUG_SESSION=vscode" \ --compressed -k --no-progress-meter | json_pp 如果在网页端已经登录的前提下,可以在网页的 console 里用这样的代码发送 graphql 请求 graphqlquery=$(cat <<- EOF query { customer { email firstname lastname } } EOF ); graphqlquery=$(echo -n $graphqlquery | php -r 'print(http_build_query(["query"=>file_get_contents("php://stdin")]));'); curl -v -L -k 'https://magento2.localhost.com/graphql?'$graphqlquery \ -H 'authorization: Bearer 214dee33a45a11eeae4800e04c947949' \ -H 'accept: */*' \ -H 'cache-control: no-cache' \ -H 'accept: application/json' \ --cookie "XDEBUG_SESSION=vscode" \ --resolve magento2.localhost.com:80:127.0.0.1 \ --resolve magento2.localhost.com:443:127.0.0.1 \ --no-progress-meter 拦截这个方法就能拿到 graphql 的输入和输出 Magento\Framework\GraphQl\Query\ResolverInterface resolve -->浏览器可以安装这个拓展 https://github.com/altair-graphql/altair
这是 graphql 的中文文档 https://graphql.cn/
参考 https://devdocs.magento.com/guides/v2.4/graphql/index.html
新建索引器
magento 索引的运行原理
magento 的索引器有两种类型
- update on save
- 原始数据更新时会直接调用 indexer 的对象,更新缓存
- update by schedule
- 原始数据更新会通过触发器更新 view_id_cl 表,然后再通过定时任务来更新缓存
- view_id_cl 表和触发器都是 magento 自动生成的
- 触发器会根据表名成三个,只要表有更新,触发器就会运行
- trg_表名_after_insert
- trg_表名_after_update
- trg_表名_after_delete
- 触发器运行时会更新 view_id_cl 表
- 这些触发器里可能会关联多个索引器,也就是说 indexer 的触发器是共用的
- 触发器会根据表名成三个,只要表有更新,触发器就会运行
- view_id_cl 的表的 view_id 就是 mview.xml 中的 id
- view_id_cl 这类表只有两个字段 version_id 和 entity_id
- version_id 是自动递增的
- 只要和 mview_state 的 version_id 不一致,就会被认为时有索引需要更新
两个和索引器相关的表
- indexer_state
- state_id
- indexer_id
- status
- valid 有效的
- invalid 无效的,原数据有修改,索引应该更新
- working 工作中
- updated
- hash_config
- mview_state
- state_id
- view_id
- mode
- enabled 启用,表示这个索引器的类型是 update on schedule
- disabled 禁用,表示这个索引器的类型是 update on save
- status
- idle 空闲的
- suspended 暂停
- working 工作中
- updated
- version_id
新建索引的步骤
在模块目录 etc 新建 inderx.xml
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Indexer/etc/indexer.xsd"> <indexer id="test_indexer" view_id="test_indexer" class="Vendor\Extension\Model\Indexer\Test" > <title translate="true">test_indexer</title> <description translate="true">Test Indexer</description> </indexer> </config>在模块目录 etc 新建 mview.xml
<?xml version="1.0" encoding="UTF-8"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Mview/etc/mview.xsd"> <view id="test_indexer" class="Vendor\Extension\Model\Indexer\Test" group="indexer" > <subscriptions> <table name="sales_order" entity_column="entity_id"/> </subscriptions> </view> </config>- view 节点的 id 对应 indexer.xml 里 indexer 节点的 view_id
- view 节点的 class 和 indexer.xml 里 indexer 节点的 class 是一致的
- subscriptions 是传递给 indexer class 的参数,是某一个表的某一列,可以是多个表
- mview 是 materialized view 的缩写
在模块目录 model/indexer 新建 TestIndexer.php
<?php namespace Vendor\Extension\Model\Indexer; class Test implements \Magento\Framework\Indexer\ActionInterface, \Magento\Framework\Mview\ActionInterface { /** * @inheritdoc */ public function executeFull() { $this->reindex(); } /** * @inheritdoc */ public function executeList(array $ids) { $this->execute($ids); } /** * @inheritdoc */ public function executeRow($id) { $this->execute([$id]); } /** * @inheritdoc */ public function execute($ids) { $this->reindex($ids); } /** * @param int[] $ids * @return void */ protected function reindex($ids = null) { if ($ids === null) { // 更新全部索引 } else { // 根据传入的 id 更新索引 } } }运行这句命令重建索引
php bin/magento indexer:reindex test_indexer可以在后台里查看索引的状态
后台 -> SYSTEM -> Index Management
相关命令
在命令行里重建索引
php bin/magento indexer:reindex php bin/magento indexer:reindex [indexer]索引器的信息,就是显示 title 和 desc
php bin/magento indexer:info索引器的状态,就是显示 Ready Reindex Processing
php bin/magento indexer:status php bin/magento indexer:status [indexer]索引器的模式,就是显示 Update on save 或 Update by schedule
php bin/magento indexer:show-mode php bin/magento indexer:show-mode [indexer] php bin/magento indexer:set-mode {realtime|schedule} [indexer]使特定或全部索引起失效
php bin/magento indexer:reset php bin/magento indexer:reset [indexer]上面的命令提及到的 [indexer] 是 inderx.xml 文件里的 indexer 节点的 id 属性
在定时任务中运行的 indexer
多数情况下 indexer 是以定时任务的形式运行的 (虽然也可以使用其它方式运行,但文档里的里的例子就是用定时任务的)
* * * * * php bin/magento cron:run --group=index
定时任务的配置文件在这个位置
vendor\magento\module-indexer\etc\crontab.xml
这个 crontab.xml 文件里有三个任务
- indexer_reindex_all_invalid
- 重建索引,类似于 php bin/magento indexer:reindex 的效果
- indexer_update_all_views
- 更新索引,根据 view_id_cl 表中的记录,更新索引
- indexer_clean_all_changelogs
- 删除 view_id_cl 表中过时的记录
因为是定时任务,所以可以用这样的 sql 观察到 indexer 的运行记录
SELECT * from cron_schedule
WHERE job_code in ('indexer_reindex_all_invalid', 'indexer_update_all_views', 'indexer_clean_all_changelogs')
order by schedule_id desc;
也可以往 cron_schedule 插入记录,让定时任务中的 indexer 尽快运行。定时任务有可能会 miss ,所以可以多插入几条记录。
INSERT INTO cron_schedule (job_code,status,created_at,scheduled_at)
VALUES
('indexer_update_all_views','pending',CURRENT_TIMESTAMP(), date_add(CURRENT_TIMESTAMP(), interval 1 minute)),
('indexer_update_all_views','pending',CURRENT_TIMESTAMP(), date_add(CURRENT_TIMESTAMP(), interval 2 minute)),
('indexer_update_all_views','pending',CURRENT_TIMESTAMP(), date_add(CURRENT_TIMESTAMP(), interval 3 minute)),
('indexer_reindex_all_invalid','pending',CURRENT_TIMESTAMP(), date_add(CURRENT_TIMESTAMP(), interval 5 minute)),
('indexer_clean_all_changelogs','pending',CURRENT_TIMESTAMP(), date_add(CURRENT_TIMESTAMP(), interval 10 minute));
可以用这样的 sql 来观察 indexer 的状态。直接运行 sql 语句比运行 命令行会快不少
select * from indexer_state where indexer_id = 'example_indexer';
select * from mview_state where view_id = 'example_indexer';
select * from view_id_cl; -- view_id 就是 mview.xml 中的 id
笔者在本地开发时,会用这样的命令确保定时任务一直在运行, 然后再往 cron_schedule 插入记录,让对应的 indexer 尽快执行。
php -r "while(true){exec('php bin/magento cron:run --group=index');sleep(3);}"
参考
https://developer.adobe.com/commerce/php/development/components/indexing/custom-indexer/
http://aqrun.oicnp.com/2019/11/10/12.magento2-indexing-reindex.html
新建定时任务
新建定时任务的步骤
- 在模块目录 etc 新建 crontab.xml
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd"> <group id="default"> <job name="order_complete_fulfillment_end_date_expire" instance="Vendor\Extension\Cron\Order\FulfillmentEndDateExpireCron" method="execute"> <schedule>0 2 * * *</schedule> </job> </group> </config> - 在模块目录里新建文件夹 cron
- 在 cron 文件夹里新建一个普通的类,并在这个类里实现一个没有参数的 execute 方法
任务组
在模块目录 etc 新建 cron_groups.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/cron_groups.xsd">
<group id="token_expired">
<schedule_generate_every>1</schedule_generate_every>
<schedule_ahead_for>4</schedule_ahead_for>
<schedule_lifetime>15</schedule_lifetime>
<history_cleanup_every>1440</history_cleanup_every>
<history_success_lifetime>60</history_success_lifetime>
<history_failure_lifetime>600</history_failure_lifetime>
<use_separate_process>1</use_separate_process>
</group>
</config>
group 节点的 id 对应 crontab.xml 里 config group 的 id
在后台的这个位置可以查看任务组
Stores > Settings > Configuration > ADVANCED > System -> Cron (Scheduled Tasks)
运行定时任务
修改过 cron 和 cron_groups 需要重新编译并清空缓存才会生效
php bin/magento setup:di:compile
php bin/magento cache:clean
运行全部任务组
php bin/magento cron:run
运行 default 任务组,一般的定时任务都在 default
php bin/magento cron:run --group=default
运行 index 任务组,这是索引器的任务组,就是 by schedule 类型的索引器
php bin/magento cron:run --group=index
运行其他任务组修改 --group 参数就可以了
然后让 cron:run 一直运行就可以的了,官方文档提供了使用 crontab 的例子,默认情况下队列好像也是用 crontab 运行。
* * * * * php bin/magento cron:run
magneto 还提供了自动生成 crontab 配置的命令
php bin/magento cron:install # 加上 magento 的 cron ,不影响其他配置
php bin/magento cron:install --force # 加上 magento 的 cron ,清除其他配置
php bin/magento cron:remove # 移除 magento 的 cron
运行了 cron:install 后,可以用 crontab -l 来查看
crontab -l
#~ MAGENTO START c5f9e5ed71cceaabc4d4fd9b3e827a2b
* * * * * /usr/bin/php /var/www/html/magento2/bin/magento cron:run 2>&1 | grep -v "Ran jobs by schedule" >> /var/www/html/magento2/var/log/magento.cron.log
#~ MAGENTO END c5f9e5ed71cceaabc4d4fd9b3e827a2b
不同的 group 可以使用不同的 cron 表达式
* * * * * /usr/bin/php /var/www/html/magento2/bin/magento cron:run --group=default 2>&1 | grep -v "Ran jobs by schedule" >> /var/www/html/magento2/var/log/magento.cron.log
*/10 * * * * /usr/bin/php /var/www/html/magento2/bin/magento cron:run --group=index 2>&1 | grep -v "Ran jobs by schedule" >> /var/www/html/magento2/var/log/magento.cron.log
这是 crontab 配置的解释
- 2>&1 是把标准错误重定向到标准输出
- grep -v "Ran jobs by schedule" 是忽略执行成功的日志
- /var/www/html/var/log/magento.cron.log 是 cron 的日志文件
自己写 crontab 配置或用其它方式(例如 supervisor )让 cron:run 一直运行也是可以的了
队列
新建一个插件 Plugins (Interceptors)
新建 Plugins 类
- 通常在模块里的 Plugins 文件下新建
- 拦截器就是一个普通的类
- 拦截器的方法就是被拦截的方法前面加上 before around after 这三个关键词
- 拦截器的方法名始终以小驼峰命名
- 在原本的类里,只有 public 方法才可以被拦截
修改模块的 etc 文件夹下的 di.xml
- 例子
<config> <type name="需要拦截的类名(要填完整的类名)"> <plugin name="拦截器名称" type="拦截器的类名(要填完整的类名)" sortOrder="排序" disabled="false" /> </type> </config> - 如果要禁用拦截器 disabled 填 true 就可以了
- sortOrder 和 disabled 都不是必填的
- sortOrder 是升序排序
- sortOrder 未指定时会按加载顺序排序,先加载的在前面执行
- 例子
运行 php bin/magento setup:di:compile 或 php bin/magento setup:upgrade
- 拦截器必须通过编译才能生效
- 编译后的拦截器会在 generate 文件夹里生成一个对应的 Interceptor 类
- 在开发者模式时可以不通过编译,拦截器在运行时生成
- 生成的 Interceptor 类,通过 use 的方式继承 \Magento\Framework\Interception\Interceptor
- \Magento\Framework\Interception\Interceptor 的 ___callPlugins 方法是拦截器实现的核心
拦截器的运行顺序
- before -> around -> after
- 会先统一执行完一类拦截器再执行下一类拦截器
- 拦截器的顺序,就是配置文件里的那个 sortOrder 参数是用在同类拦截器的排序的
三种方法的入参和出参
- before
- 入参
- 原本的对象 $subject
- 原本的入参(这是一个可变长参数 ...array_values($arguments))
- 出参
- null 或 一个数组
- 如果是 null 那么 原本的入参不会变
- 如果是一个数组,那么数组会替代原本的入参
- 入参
- around
- 入参
- 原本的对象 $subject
- 匿名函数proceed(拦截器运行的匿名方法)
- proceed 在拦截器的around方法里运行
- 原本的入参(这是一个可变长参数 ...array_values($arguments))
- 出参
- 和执行结果类型一样的$result
- 入参
- after
- 入参
- 原本的对象 $subject
- 执行的结果 $result
- 原本的入参(这是一个可变长参数 ...array_values($arguments))
- 出参
- 和执行结果类型一样的 $result
- 入参
- before
参考 https://developer.adobe.com/commerce/php/development/components/plugins/
替换其它模块里的类 (preference)
事件和观察者 (Events and Observers)
- 在配置文件里声明一个事件
- 在模块的 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="customer_save_after_data_object"> <observer name="upgrade_order_customer_email" instance="Magento\Customer\Observer\UpgradeOrderCustomerEmailObserver"/> <observer name="upgrade_quote_customer_email" instance="Magento\Customer\Observer\UpgradeQuoteCustomerEmailObserver"/> </event> </config>
- 在模块的 etc 文件夹下的 events.xml ,加上类似于这样的一段
- 新建一个观察者类
- 新建的观察者类的完整类名需要和配置文件里的对应
- 需要实现这个接口
Magento\Framework\Event\ObserverInterface
- 在需要的位置触发事件,类似于这样
// 第一个参数是事件名;第二个参数是一个数组,用于传递参数给观察者 // $this->_eventManager 的类型 \Magento\Framework\Event\ManagerInterface $this->_eventManager->dispatch( 'admin_user_authenticate_after', ['username' => $username, 'password' => $password, 'user' => $this, 'result' => $result] ); - 参考 https://developer.adobe.com/commerce/php/development/components/events-and-observers/
直接触发事件(event)
try {
// 引入 magento2 的引导文件
require __DIR__ . '/app/bootstrap.php';
// 创建一个应用对象
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
// 获取一个对象管理器
$objectManager = $bootstrap->getObjectManager();
// 如果出现这种错误 area code is not set ,则加上这两句, area 的值可以根据实际场景修改
$areaCode = \Magento\Framework\App\Area::AREA_FRONTEND;
$objectManager->get(\Magento\Framework\App\State::class)->setAreaCode($areaCode);
$objectManager->configure(
$objectManager
->get(\Magento\Framework\App\ObjectManager\ConfigLoader::class)
->load($areaCode)
);
$order = '';
$invoice = '';
/** @var \Magento\Framework\Event\ManagerInterface */
$eventManager = $objectManager->get(\Magento\Framework\Event\ManagerInterface::class);
$eventManager->dispatch(
'sales_model_service_quote_submit_success',
['order' => $order, 'invoice' => $invoice]
);
} catch (\Throwable $e) {
echo $e->getFile() . ':' . $e->getLine() . PHP_EOL;
echo $e->getMessage() . PHP_EOL . $e->getTraceAsString();
}
直接运行观察者(observer)
try {
// 引入 magento2 的引导文件
require __DIR__ . '/app/bootstrap.php';
// 创建一个应用对象
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
// 获取一个对象管理器
$objectManager = $bootstrap->getObjectManager();
// 如果出现这种错误 area code is not set ,则加上这两句, area 的值可以根据实际场景修改
$areaCode = \Magento\Framework\App\Area::AREA_FRONTEND;
$objectManager->get(\Magento\Framework\App\State::class)->setAreaCode($areaCode);
$objectManager->configure(
$objectManager
->get(\Magento\Framework\App\ObjectManager\ConfigLoader::class)
->load($areaCode)
);
$order = '';
$invoice = '';
$data = ['order' => $order, 'invoice' => $invoice];
$eventName = 'sales_model_service_quote_submit_success';
$observerConfig = [ // 这里对应的是 events.xml 里 observer 字段的值
'instance' => 'Magento\CatalogInventory\Observer\ReindexQuoteInventoryObserver',
'name' => 'inventory',
];
/** @var \Magento\Framework\Event\InvokerInterface */
$invoker = $objectManager->get(\Magento\Framework\Event\InvokerInterface::class);
$event = new \Magento\Framework\Event($data);
$event->setName($eventName);
$wrapper = new \Magento\Framework\Event\Observer();
$wrapper->setData(array_merge(['event' => $event], $data));
$invoker->dispatch($observerConfig, $wrapper);
} catch (\Throwable $e) {
echo $e->getFile() . ':' . $e->getLine() . PHP_EOL;
echo $e->getMessage() . PHP_EOL . $e->getTraceAsString();
}
新建一个后台视图
- 新建路由
- 在 etc 文件夹下新建 adminhtml 文件夹,在 adminhtml 下新建 routes.xml 并写入以下内容
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> <router id="admin"> <route id="partnercode" frontName="partnercode"> <module name="LocalDev_HelloModule" /> </route> </router> </config>
- 在 etc 文件夹下新建 adminhtml 文件夹,在 adminhtml 下新建 routes.xml 并写入以下内容
- 视图是一个 xml 文件
- 视图的命名是根据路由来的
- 例如
- 视图名是 partnercode_couponquota_index.xml 对应的路由是 partnercode/couponquota/index
- 路由是 partnercode/couponquota/index 对应的控制器是 partnercode/Controller/Adminhtml/Couponquota/Index.php
- 视图文件一般放在这几个位置
- 后台的视图 模块/view/adminhtml/layout/视图名.xml
- 前台的视图 模块/view/frontend/layout/视图名.xml
- 通用的视图 模块/view/base/layout/视图名.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\Backend\Block\Template" name="extension.coupon_quota.grid.container" template="Vendor_Extension::coupon_quota/index.phtml"/> </referenceContainer> </body> </page> - block 节点参数的解释
- block 要填完整的类名,如果不是自定义的 block ,就填 \Magento\Backend\Block\Template 或 \Magento\Framework\View\Element\Template
- name 可以随便填,但最好全局唯一
- template 填模板的路径,是 模块名::模板的相对路径,可以参考下面的例子
- 如果视图的绝对路径是 app\code\Vendor\Extension\view\adminhtml\layout\partnercode_couponquota_index.xml
- 如果模板的绝对路径是 app\code\Vendor\Extension\view\adminhtml\templates\coupon_quota\index.phtml
- 那么在 template 里的值就填 Vendor_Extension::coupon_quota/index.phtml
- 视图由 block 组成
- blcok 是 php 对象
- 自定义的 block 一般放在 模块/block 这个文件夹里
- 后台的 block 就要继承这个类 \Magento\Backend\Block\Template
- 前台的 block 就要继承这个类 \Magento\Framework\View\Element\Template
- 每个 block 会有一个模板对应,也就是 phtml 后缀的文件。
- 这是一个模板的例子
<?php /** @var \Magento\Framework\View\Element\Template $block */ ?> <p><?=$block->getBaseUrl()?></p>
- 这是一个模板的例子
在后台视图里新建一个表格
新建必要的文件
app/code/Vendor/Module/
├── Controller/
│ └── Adminhtml/
│ └── Customer/
│ └── Index.php
├── Model/
│ ├── Customer.php
│ └── ResourceModel/
│ ├── Customer.php
│ └── Customer/
│ └── Collection.php
├── Ui/
│ └── Component/
│ └── DataProvider.php
├── etc/
│ ├── adminhtml/
│ │ ├── acl.xml
│ │ ├── menu.xml
│ │ └── routes.xml
│ ├── di.xml
│ └── module.xml
├── view/
│ └── adminhtml/
│ ├── layout/
│ │ └── vendor_module_customer_index.xml
│ └── ui_component/
│ └── vendor_module_customer_listing.xml
└── registration.php
app/code/Vendor/Module/
├── Controller/
│ └── Adminhtml/
│ └── Page/
│ └── Index.php
├── etc/
│ ├── adminhtml/
│ │ └── menu.xml
│ └── di.xml
├── view/
│ └── adminhtml/
│ ├── layout/
│ │ └── vendor_module_page_index.xml
│ └── ui_component/
│ └── vendor_module_page_listing.xml
Vendor/Module/Controller/Adminhtml/Page/Index.php
<?php
namespace Vendor\Module\Controller\Adminhtml\Page;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class Index extends Action
{
protected $resultPageFactory;
public function __construct(Context $context, PageFactory $resultPageFactory)
{
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
}
public function execute()
{
$resultPage = $this->resultPageFactory->create();
$resultPage->setActiveMenu('Vendor_Module::page_list');
$resultPage->getConfig()->getTitle()->prepend(__('page List'));
return $resultPage;
}
// protected function _isAllowed()
// {
// return $this->_authorization->isAllowed('Vendor_Module::menu');
// }
}
Vendor/Module/etc/adminhtml/menu.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
<menu>
<add id="Vendor_Module::page_list" title="page Grid" module="Vendor_Module" parent="Magento_Backend::system_tools" action="vendor_module/page/index" sortOrder="10" resource="Magento_Backend::admin"/>
</menu>
</config>
Vendor/Module/etc/di.xml
<config>
...
<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
<arguments>
<argument name="collections" xsi:type="array">
<item name="vendor_module_page_listing_data_source" xsi:type="string">Magento\Cms\Model\ResourceModel\Page\Grid\Collection</item>
</argument>
</arguments>
</type>
...
</config>
Vendor/Module/view/adminhtml/layout/vendor_module_page_index.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">
<update handle="styles"/>
<body>
<referenceContainer name="content">
<uiComponent name="vendor_module_page_listing"/>
</referenceContainer>
</body>
</page>
Vendor/Module/view/adminhtml/layout/vendor_module_page_listing.xml
<?xml version="1.0"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<argument name="data" xsi:type="array">
<item name="js_config" xsi:type="array">
<item name="provider" xsi:type="string">vendor_module_page_listing.vendor_module_page_listing_data_source</item>
<item name="deps" xsi:type="string">vendor_module_page_listing.vendor_module_page_listing_data_source</item>
</item>
<item name="spinner" xsi:type="string">vendor_module_page_columns</item>
</argument>
<dataSource name="vendor_module_page_listing_data_source">
<argument name="dataProvider" xsi:type="configurableObject">
<argument name="class" xsi:type="string">Magento\Cms\Ui\Component\DataProvider</argument>
<argument name="name" xsi:type="string">ccc_qwe_page_listing_data_source</argument>
<argument name="primaryFieldName" xsi:type="string">page_id</argument>
<argument name="requestFieldName" xsi:type="string">page_id</argument>
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
<item name="update_url" xsi:type="url" path="mui/index/render"/>
<item name="storageConfig" xsi:type="array">
<item name="indexField" xsi:type="string">page_id</item>
</item>
</item>
</argument>
</argument>
<argument name="data" xsi:type="array">
<item name="js_config" xsi:type="array">
<item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
</item>
</argument>
</dataSource>
<columns name="vendor_module_page_columns">
<column name="page_id">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="filter" xsi:type="string">text</item>
<item name="label" xsi:type="string" translate="true">page_id</item>
</item>
</argument>
</column>
<column name="title">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="filter" xsi:type="string">text</item>
<item name="label" xsi:type="string" translate="true">title</item>
</item>
</argument>
</column>
</columns>
</listing>
添加后台日志
在模块的 etc 文件夹下的 logging.xml 里加上类似这样的一段
<group name="order_retrievepayment">
<label translate="true">Order Retrieve Payment</label>
<expected_models>
<expected_model class="Magento\Sales\Model\Order"></expected_model>
</expected_models>
<events>
<event controller_action="adminportal_order_retrievepayment" action_alias="save" />
</events>
</group>
如果是 post 请求,那么需要在 event 节点里再加一个属性 post_dispatch="postDispatchSimpleSave"
<event controller_action="adminportal_order_retrievepayment" action_alias="save" post_dispatch="postDispatchSimpleSave"/>
controller_action 是 模块名_控制器名_方法名 可以在这两个位置加断点,然后再运行一次请求,就知道具体的 controller_action 是什么了
vendor\magento\module-logging\Observer\ControllerPostdispatchObserver.php:52
vendor\magento\module-logging\Model\Processor.php:363
然后在后台里勾选对应的选项,按着这样的路径寻找
Stores
Settings
Configuration
Advanced
Admin
Admin Actions Logging
在配置文件里的 label
可以在后台里的这个位置查看日志
system -> action logs -> report
日志会插入到这个表里 magento_logging_event
后台 acl
- 修改在模块的 etc 文件夹下的 acl.xml
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> <acl> <resources> <resource id="Magento_Backend::admin"> <resource id="Magento_Sales::sales"> <resource id="Magento_Sales::sales_operation"> <resource id="Magento_Sales::sales_order"> <resource id="Vendor_Extension_AdminPortal::cs_portal" title="CS Portal" sortOrder="10" /> <resource id="Magento_Sales::create_new_order" title="Create New Order" sortOrder="20" /> <resource id="Magento_Sales::view_order" title="View Order" sortOrder="30" /> <resource id="Magento_Sales::order_actions" title="Order Actions" sortOrder="40" /> <resource id="Magento_Sales::go_to_archive" title="Go To Order Archive" sortOrder="50" /> </resource> </resource> </resource> </resources> </acl> </config>- resource 可以嵌套
- resource id(模块::操作),这个 resource id 要和控制器定义的 ADMIN_RESOURCE 一致
- 控制器里有一个常量
class Save extends Action { public const ADMIN_RESOURCE = 'Magento_Customer::save'; }
- 修改完后要清除缓存才能生效 php bin/magento cache:clean
- 权限的调整在这个位置 System > Permissions > User Roles
- 参考 https://developer.adobe.com/commerce/php/best-practices/tutorials/create-access-control-list-rule/
新建一个后台菜单
- 在模块目录下的 etc 文件里新建一个文件
module/etc/adminhtml/menu.xml - 在 menu.xml 里加入一段
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> <add id="Silk_Test::job_head" title="Test" module="Silk_Test" sortOrder="100" parent="Magento_Backend::stores" resource="Silk_Test::job_head" /> <add id="Silk_Test::job" title="Test" module="Silk_Test" sortOrder="20" parent="Silk_Test::job_head" action="test/job" resource="Silk_Test::job" /> </menu> </config> - 一些参数的解释
- parent 上级的id
- title 菜单名称
- id 唯一识别的id
- action 转跳的 action ,不填这个就是菜单里的一个分类
- resource 用于 acl 的
- module 模块名
- 参考 https://developer.adobe.com/commerce/php/best-practices/tutorials/create-access-control-list-rule/
一些自定义配置
写在模块的 etc/config.xml 文件里
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<general>
<file>
<bunch_size>1000</bunch_size>
</file>
</general>
</default>
</config>
写在 core_config_data 表里
INSERT INTO core_config_data (`scope`,scope_id,`path`,value,updated_at) VALUES ('default',0,'general/file/bunch_size','1000', NOW());
UPDATE core_config_data SET `value` = '500' WHERE `path` = 'pgeneral/file/bunch_size' and `scope` = 'default' and `scope_id` = 0;
UPDATE core_config_data
SET value = '0'
WHERE path = 'product_custom_attributes/club_protection/enable_mox_card';
上面两种写法效果是一样的, 可以这样获取配置的值
/** @var \Magento\Framework\App\Config\ScopeConfigInterface */
$scopeConfig = \Magento\Framework\App\ObjectManager::getInstance()->get(Magento\Framework\App\Config\ScopeConfigInterface::class);
$scopeConfig->getValue('general/file/bunch_size');
用于查询配置的sql
select * from core_config_data where path like '%general/file/bunch_size%' limit 10
还可以用命令行来修改配置,这种修改会保存在数据库里 https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configuration-management/set-configuration-values.html
# 设置某个配置
php bin/magento config:set path value
# 查看某个配置
php bin/magento config:show path
数据库的优先级会更高。
修改过配置项的值后,需要清空或刷新缓存才会生效(不论是 config.xml 的配置还是数据库里的配置)。
在后台加上配置项
通常是写在模块的 etc/adminhtml/system.xml 文件里
后台的配置也是用上面额方法获取配置的值,后台配置的默认值也是写在 etc/config.xml 文件里
一个例子
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="test_section" showInDefault="1" showInWebsite="1" showInStore="1">
<group id="test_group" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="11">
<label>test group</label>
<field id="test_field" translate="label" type="textarea" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
<label>test field</label>
<comment>test comment</comment>
</field>
</group>
</section>
</system>
</config>
配置里的加密字段
加密字段,就是在后台页面里不会直接显示内容,而是显示 ***** ,即使用命令行获取字段内容也是返回 *****
etc/system.xml 里的配置,注意 type 需要设成 obscure
<field id="password" translate="label" type="obscure" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Password</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypte</backend_model>
<validate>required-entry</validate>
</field>
在代码里这样获取
$scopeConfig = $objectManager->get(\Magento\Framework\App\Config\ScopeConfigInterface::class);
$secret = $scopeConfig->getValue('sales_email/general/teste_ncrypted_2'); // 这是加密后的字符串
var_dump($secret);
$encryptor = $objectManager->get(\Magento\Framework\Encryption\Encryptor::class); // 这才是有效的内容
$secret = $encryptor->decrypt($secret);
var_dump($secret);
前端
缓存
发送邮件
使用模板
- 在 etc 下新建 email_templages.xml 文件
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd"> <template id="self_check_order_confirmation_email" label="Self Check Order Confirmation Eamil" file="self_check_order_confirmation_email.html" type="html" module="LocalDev_HelloModule" area="adminhtml"/> </config> - 在 view/area代码/email 下新建模板文件,文件名和area代码要和 email_templages.xml 里的对应
<!--@subject {{var subject}} @--> <p>{{var mail_content}}</p> - 在 php 的代码里这样调用
try { require __DIR__ . '/app/bootstrap.php'; $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); $objectManager = $bootstrap->getObjectManager(); $areaCode = \Magento\Framework\App\Area::AREA_CRONTAB; $objectManager->get(\Magento\Framework\App\State::class)->setAreaCode($areaCode); $objectManager->configure( $objectManager ->get(\Magento\Framework\App\ObjectManager\ConfigLoader::class) ->load($areaCode) ); $appConfig = $objectManager->get(\Magento\Framework\App\Config\ScopeConfigInterface::class); $transportBuilder = $objectManager->get(\Magento\Framework\Mail\Template\TransportBuilder::class); $templateIdentifier = 'self_check_order_confirmation_email'; // 要和 email_templages.xml 里的 id 对应 $templateVars = [ 'subject' => '123', 'mail_content' => '321' ]; $templateOptions = [ 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, // 要和 email_templages.xml 里的 area代码 对应 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, ]; $sender = [ 'name' => $appConfig->getValue('trans_email/ident_general/name'), 'email' => $appConfig->getValue('trans_email/ident_general/email'), ]; $transportBuilder ->setTemplateIdentifier($templateIdentifier) ->setTemplateVars($templateVars) ->setTemplateOptions($templateOptions) ->setFrom($sender); $to = [ '001@example.com', '002@example.com', '003@example.com', ]; foreach ($to as $item) { $transportBuilder->addTo($item); } $transportBuilder->getTransport()->sendMessage(); } catch (\Throwable $e) { echo $e->getFile() . ':' . $e->getLine() . PHP_EOL; echo $e->getMessage() . PHP_EOL . $e->getTraceAsString(); }
不使用模板
try {
require __DIR__ . '/app/bootstrap.php';
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
$objectManager = $bootstrap->getObjectManager();
$areaCode = \Magento\Framework\App\Area::AREA_CRONTAB;
$objectManager->get(\Magento\Framework\App\State::class)->setAreaCode($areaCode);
$objectManager->configure(
$objectManager
->get(\Magento\Framework\App\ObjectManager\ConfigLoader::class)
->load($areaCode)
);
$message = $objectManager->get(\Magento\Framework\Mail\Message::class);
$appConfig = $objectManager->get(\Magento\Framework\App\Config\ScopeConfigInterface::class);
$message->setSubject('Hello from Bing');
$message->setBodyHtml('<p>This is a test email sent by Bing using PHP mail function.</p>');
// $message->setBodyText('This is a test email sent by Bing using PHP mail function.');
$message->setFromAddress(
$appConfig->getValue('trans_email/ident_general/email'),
$appConfig->getValue('trans_email/ident_general/name')
);
$to = [
'001@example.com',
'002@example.com',
'003@example.com',
];
foreach ($to as $item) {
$message->addTo($item);
}
(new \Laminas\Mail\Transport\Sendmail())->send(
\Laminas\Mail\Message::fromString($message->getRawMessage())
);
} catch (\Throwable $e) {
echo $e->getFile() . ':' . $e->getLine() . PHP_EOL;
echo $e->getMessage() . PHP_EOL . $e->getTraceAsString();
}
Transport
magento2 在默认情况下 使用 PHP 的 Sendmail 函数来发送邮件,就是调用系统里的 sendmail ,只能设置 host 和 port
magento2.3 之后也支持 smtp 了,在这个位置设置,可以选择 sendmail 和 smtp
Stores > Settings > Configuration > Advanced > System > Mail Sending Settings > Transport
magento2.3 之前的版本可以用这个模块来实现 SMTP 发送邮件 https://www.mageplaza.com/magento-2-smtp/
magento2 使用这个库来发送邮件的 https://github.com/laminas/laminas-mail
在本地测试邮件可以参考这篇文章《在Windows下配置PHP服务器》的这个章节 mailpit
sendmail 和 smtp 两种方式都可以用 mailpit 来测试, mailpit 可以忽略 smtp 的账号密码
一些调试技巧
获取某一个对象
// 从已存在的对象中获取
$logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
// 新建一个
$logger = \Magento\Framework\App\ObjectManager::getInstance()->create(\Psr\Log\LoggerInterface::class);
// 获取一个普通的对象
/** @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory */
$orderCollectionFactory = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class);
$orderId = 3068;
$orderCollection = $orderCollectionFactory->create();
$orderCollection->addFieldToFilter('entity_id', $orderId); // 可以修改条件
/** @var \Magento\Sales\Model\Order */
$order = $orderCollection->getFirstItem(); // $orderCollection->getItems(); // 获取集合
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
// 根据 customer id 或 email 获取 customer 对象
/** @var \Magento\Customer\Model\CustomerFactory */
$customerFactory = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Customer\Model\CustomerFactory::class);
$customer = $customerFactory->create()->load($customerID);
$customer = $customerFactory->create()->loadByEmail($email);
// 获取某个 customer 的购物车
$quote = $customer->getQuote();
// 获取某个 customer 最近成功支付的订单
/** @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory */
$orderCollectionFactory = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class);
$orderCollection = $orderCollectionFactory->create();
$orderCollection->addFieldToFilter('customer_id', $customer->getId());
$orderCollection->addFieldToFilter('state', ['in' => [
\Magento\Sales\Model\Order::STATE_PROCESSING,
\Magento\Sales\Model\Order::STATE_COMPLETE
]]);
$orderCollection->setOrder('created_at');
$orderCollection->setPageSize(1);
$order = $orderCollection->getFirstItem();
// 根据 productId 获取 product 对象
/** @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory */
$productCollectionFactory = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class);
$productCollection = $productCollectionFactory->create();
$productCollection->addFieldToFilter(
'entity_id', ['in' => $productId]
// 'sku', ['eq' => $sku]
);
$productCollection->setPageSize(1);
$product = $productCollection->getFirstItem();
/** @var \Magento\Catalog\Api\ProductRepositoryInterface */
$productRepository = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
$product = $productRepository->getById($productId);
$product = $productRepository->get($sku);
$product->getExtensionAttributes();
在某一个位置写日志
/** @var \Psr\Log\LoggerInterface */
$logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
$logger->warning('=======flg debug=======', ['trace' => $a]);
$logger->warning('=======flg debug=======', ['trace' => $exception->getTrace(), 'msg' => $exception->getMessage()]);
$logger->warning('=======flg debug=======', ['trace' => debug_backtrace()]);
$logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
$logger->warning('=======flg debug=======' . PHP_EOL . __FILE__ . ':' . __LINE__ . PHP_EOL, ['trace' => $a]);
$logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
$logger->error('debugger ' . __FILE__ .':'.__LINE__, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
try {
if (isset($_COOKIE) && isset($_COOKIE['PHPSESSID']) && $_COOKIE['PHPSESSID'] =='k3o5vd1ggjo6k5rfd47') {
$logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
$logger->error('debugger ' . __FILE__ .':'.__LINE__, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
}
} catch (\Throwable $e) {
$logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
$logger->error('debugger ' . __FILE__ .':'.__LINE__, [$e->getFile(), $e->getLine(), $e->getMessage()]);
}
在某一个位置通过拼接的 sql 查询数据库
/**
* @var \Magento\Framework\App\ResourceConnection
*/
$conn = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\App\ResourceConnection::class);
$conn = $conn->getConnection();
$select = $conn->select()
->from(['so' => $conn->getTableName('sales_order')], [
'so.entity_id',
'so.customer_id',
'soi.fulfilment_end_at',
])
->joinLeft(
['soi' => $conn->getTableName('sales_order_item')],
'so.entity_id=soi.order_id',
);
$select->where("so.status = ?", \Magento\Sales\Model\Order::STATE_PROCESSING)
->where("soi.qty_fulfilled + soi.qty_disabled + soi.qty_markoff < soi.qty_invoiced")
->where("soi.fulfilment_start_at <= ? ", time());
$result = $conn->fetchAll($select);
// 直接运行 sql 语句
$conn = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\App\ResourceConnection::class);
$result = $conn->getConnection()->query('SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP);')->fetchAll();
$result = $conn->getConnection()->query("update sales_order set status = 'complete', state = 'complete' where entity_id = 123456;")->execute();
通过某一个模型的 collection 对象
/** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */
$collection = $collectionFactory->create();
$collection->addFieldToSelect(
'*'
)->addFieldToFilter('customer_id', $customer->getId());
输出原始的 sql 语句
/** @var \Magento\Framework\DB\Select $select */
echo $select->__toString();
/** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection $collection */
echo $collection->getSelect()->__toString();
echo $collection->getSelectSql(true);
sql 的执行记录
加在这个文件里 app/etc/env.php 加上这段
'db_logger' => [
'output' => 'file',
'log_everything' => 1,
'query_time_threshold' => '0.001',
'include_stacktrace' => 0 // 改成1可以记录代码调用栈
],
日志会输出到这个文件里 var/debug/db.log
sql 语句最终的执行位置
通过 composer 安装的
vendor\magento\zendframework1\library\Zend\Db\Adapter\Abstract.php query
通过 github 安装的
vendor\magento\zend-db\library\Zend\Db\Adapter\Abstract.php query
写日志,并记录调用栈堆
# region logsql
$logOpen = false;
// $logOpen = true;
$trace = debug_backtrace();
$basePath = BP . DIRECTORY_SEPARATOR;
if (!defined('DEBUG_TRACE_LOG')) {
$logpath = $basePath . 'var' . DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR . 'debug_trace_sql';
if (!is_dir($logpath)) {
mkdir($logpath, 0755, true);
}
define('DEBUG_TRACE_LOG', $logpath . DIRECTORY_SEPARATOR . date('ymdHis') . '.log');
$data = [
'_POST' => $_POST ?? null,
'_GET' => $_GET ?? null,
'_FILES' => $_FILES ?? null,
'_SERVER' => $_SERVER ?? null,
'_SESSION' => $_SESSION ?? null,
'_input' => file_get_contents("php://input"),
// '_stdin' => file_get_contents("php://stdin") // 这一句在命令行里会等待输入
];
$msg = print_r($data, true) . '========' . PHP_EOL;
if ($logOpen) {
file_put_contents(
DEBUG_TRACE_LOG,
$msg,
FILE_APPEND
);
}
}
$ignore = [ // 忽略 ObjectManager 的文件, Interceptor 的文件, Factory 的文件, Event 的文件
'vendor' . DIRECTORY_SEPARATOR . 'magento' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'Interception' . DIRECTORY_SEPARATOR . 'Interceptor.php',
'generated',
'vendor' . DIRECTORY_SEPARATOR . 'magento' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'ObjectManager' . DIRECTORY_SEPARATOR . 'Factory',
'vendor' . DIRECTORY_SEPARATOR . 'magento' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'ObjectManager' . DIRECTORY_SEPARATOR . 'ObjectManager.php',
'vendor' . DIRECTORY_SEPARATOR . 'magento' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'Event' . DIRECTORY_SEPARATOR . 'Manager.php',
'vendor' . DIRECTORY_SEPARATOR . 'magento' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'Event' . DIRECTORY_SEPARATOR . 'Invoker' . DIRECTORY_SEPARATOR . 'InvokerDefault.php',
'vendor' . DIRECTORY_SEPARATOR . 'magento' . DIRECTORY_SEPARATOR . 'module-staging' . DIRECTORY_SEPARATOR . 'Model' . DIRECTORY_SEPARATOR . 'Event' . DIRECTORY_SEPARATOR . 'Manager.php',
];
$pattern = array_map(function($item) use ($basePath) {
return '(' . preg_quote($basePath . $item, '/') . ')';
}, $ignore);
$pattern = '/' . implode('|', $pattern) . '/im';
$max = 200;
$traceRecord = [];
// $traceRecord[] = __FILE__ . ':' . __LINE__;
for ($i = 0, $len = count($trace); $i < $max && $i < $len; $i++) {
if (isset($trace[$i]['file'])) {
if (!preg_match($pattern, $trace[$i]['file'])) {
$file = $trace[$i]['file'];
$line = $trace[$i]['line'] ?? '1';
$class = $trace[$i]['class'] ?? '';
$func = $trace[$i]['function'] ?? '';
$record = $file . ':' . $line . ' ' . $class . ' ' . $func;
$traceRecord[] = $record;
}
}
}
$msg = print_r([
$sql,
count($bind) < 1 ? null : $bind,
$traceRecord,
], true) . '========' . PHP_EOL;
if ($logOpen) {
$filer = [ // 通过正则表达式只记录某些语句
// '`customer_entity`',
// '`customer_address_entity`',
// '`quote_address`',
// '`salesrule`',
// '`salesrule_coupon`',
// '`salesrule_customer`',
// '^SELECT'
// 'customer_is_guest',
];
$regexp = '';
if (is_array($filer) && count($filer) > 0) {
$filer = implode('|', $filer);
$regexp = '/' . $filer . '/';
}
if (empty($regexp) || filter_var($sql, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => $regexp)))) {
file_put_contents(
DEBUG_TRACE_LOG,
$msg,
FILE_APPEND
);
}
}
# endregion logsql
这一段是硬写在这个方法里的,也可以硬写到其它方法里
vendor\magento\zendframework1\library\Zend\Db\Adapter\Abstract.php query
使用 Debug 类来输出调用栈
lib\internal\Magento\Framework\Debug.php
/**
* Prints or returns a backtrace
*
* @param bool $return return or print
* @param bool $html output in HTML format
* @param bool $withArgs add short arguments of methods
* @return string|bool
*/
public static function backtrace($return = false, $html = true, $withArgs = true)
可以像这样调用
\Magento\Framework\Debug::backtrace();
\Magento\Framework\Debug::backtrace(false, false, false);
echo \Magento\Framework\Debug::backtrace(true, false, false);
文件搜索
通过正则表达式搜索某个接口的实现类或某个对象的继承类
implements(?:.*)ObjectManagerInterface\n
extends(?:.*)AbstractResource\n
搜索时的排除选项
.js,.css,.md,.txt,.json,.csv,.html,.less,.phtml,**/tests,**/test,**/Test,**/setup,**/view,**/magento2-functional-testing-framework,.wsdl,**/module-signifyd,**/Block,pub,generated,var,dev
app/code/Vendor/**/*.php
app/**/*Test.php
magento/**/*.php
通过命令行运行一些测试的代码
修改这个文件的 execute 方法,用 exit(0); 来结束
vendor/magento/module-indexer/Console/Command/IndexerInfoCommand.php
例子
protected function execute(InputInterface $input, OutputInterface $output)
{
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
/** @var \Magento\Framework\App\State */
$appState = $objectManager->get(\Magento\Framework\App\State::class);
try { // 没有这句很容易会出现 Area code is not set 的错误
$appState->setAreaCode(\Magento\Framework\App\Area::AREA_ADMINHTML);
} catch (\Exception $e) {
}
// 可以尝试这样更改 store view
// /** @var \Magento\Store\Model\StoreManagerInterface */
// $storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class);
// $storeManager->setCurrentStore('zh_Hans_CN');
/** @var \Magento\Framework\App\ResourceConnection */
$connection = $objectManager->get(\Magento\Framework\App\ResourceConnection::class);
$conn = $connection->getConnection();
/** @var \Mageplaza\SocialLogin\Model\Social */
$social = $objectManager->get(\Mageplaza\SocialLogin\Model\Social::class);
$customer = $social->getCustomerByEmail('qwe@asd.com');
/** @var \Magento\Quote\Model\QuoteFactory */
$quoteFactory = $objectManager->get(\Magento\Quote\Model\QuoteFactory::class);
$quote = $quoteFactory->create();
$quote->setCustomer($customer->getDataModel());
$address = $quote->getShippingAddress();
var_dump($address->getCity());
exit(0);
$indexers = $this->getAllIndexers();
foreach ($indexers as $indexer) {
$output->writeln(sprintf('%-40s %s', $indexer->getId(), $indexer->getTitle()));
}
}
运行命令
php bin/magento indexer:info
php -d xdebug.remote_autostart=on bin/magento indexer:info
php -d xdebug.start_with_request=yes bin/magento indexer:info
通过命令行运行测试代码,可以不加载前端资源,反馈的速度更快。 修改原本的命令行是为了不运行构建的命令就能生效。 一些对象可以通过 \Magento\Framework\App\ObjectManager::getInstance()->get() 的方法获得。 indexer:status 的输出就包含了 indexer:info 的输出。
直接运行测试代码,要在项目的根目录里运行,但这种方式无法调试,这种运行方式很容易忽略一些模块的 plugin 或 event
php -a <<- 'EOF'
try {
// 引入 magento2 的引导文件
require __DIR__ . '/app/bootstrap.php';
// 创建一个应用对象
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
// 获取一个对象管理器
$objectManager = $bootstrap->getObjectManager();
// 如果出现这种错误 area code is not set ,则加上这两句, area 的值可以根据实际场景修改
$areaCode = \Magento\Framework\App\Area::AREA_FRONTEND;
$objectManager->get(\Magento\Framework\App\State::class)->setAreaCode($areaCode);
$objectManager->configure(
$objectManager
->get(\Magento\Framework\App\ObjectManager\ConfigLoader::class)
->load($areaCode)
);
// 获取一个文件系统对象
$fileSystem = $objectManager->get(\Magento\Framework\Filesystem::class);
// 获取临时目录的路径
$tempDir = $fileSystem->getDirectoryRead(\Magento\Framework\App\Filesystem\DirectoryList::TMP)->getAbsolutePath();
// 输出路径
echo $tempDir;
} catch (\Throwable $e) {
echo $e->getFile() . ':' . $e->getLine() . PHP_EOL;
echo $e->getMessage() . PHP_EOL . $e->getTraceAsString();
}
EOF
try {
// 引入 magento2 的引导文件
require __DIR__ . '/app/bootstrap.php';
// 创建一个应用对象
$application = new \Magento\Framework\Console\Cli('Magento CLI');
// 获取一个对象管理器
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$areaCode = \Magento\Framework\App\Area::AREA_CRONTAB;
$objectManager->get(\Magento\Framework\App\State::class)->setAreaCode($areaCode);
$objectManager->configure(
$objectManager
->get(\Magento\Framework\App\ObjectManager\ConfigLoader::class)
->load($areaCode)
);
// 获取一个文件系统对象
$fileSystem = $objectManager->get(\Magento\Framework\Filesystem::class);
// 获取临时目录的路径
$tempDir = $fileSystem->getDirectoryRead(\Magento\Framework\App\Filesystem\DirectoryList::TMP)->getAbsolutePath();
// 输出路径
echo $tempDir;
} catch (\Throwable $e) {
echo $e->getFile() . ':' . $e->getLine() . PHP_EOL;
echo $e->getMessage() . PHP_EOL . $e->getTraceAsString();
}
try {
// 引入 magento2 的引导文件
require __DIR__ . '/app/bootstrap.php';
// 创建一个应用对象
$application = new \Magento\Framework\Console\Cli('Magento CLI');
// 获取一个对象管理器
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
// 这两句主要用在 git for windows 的环境下,主要用在 php -a
putenv('COLUMNS=80');
putenv('LINES=50');
// 允许执行多个命令,不然会只执行一个命令然后直接 exit
$application->setAutoExit(false);
$command = 'magento indexer:status catalogrule_product';
$application->run(new \Symfony\Component\Console\Input\ArgvInput(explode(' ', $command)));
$command = 'magento indexer:status catalog_product_price';
$application->run(new \Symfony\Component\Console\Input\ArgvInput(explode(' ', $command)));
} catch (\Throwable $e) {
echo join(PHP_EOL, [
$e->getFile() . ':' . $e->getLine(),
$e->getMessage(),
$e->getTraceAsString(),
]);
}
前端的调试
可以这样在浏览器查看前端模块的数据
require('Magento_Checkout/js/model/quote');通过浏览器的断点来实现前端的调试
一些情况下可以使用使用鼠标点击的事件作为断点,如果是火狐浏览器可以直接查看节点绑定的事件
忽略一些 js 文件,把这些 js 文件标记为库文件
使用浏览器的覆盖功能,直接在前端修改 js 和 css 代码,这样做的好处是不用运行 setup:static-content:deploy 这种命令
修改后台的帐号密码
笔者在二次开发 magento2 的过程中,登录后台时总是失败, magento2 似乎有一套很 混乱 很 复杂 的规则来限制后台的登录。
这里记录一下通过修改数据库里对应的表来完成登录。
这些记录可能会随着magento的更新而失效
和后台登录相关的表
admin_passwords
admin_user
admin_user_expiration
顺利登录时各个字段的状态
- admin_user
- is_active 设为 1
- failures_num 设为 0
- lock_expires 设为 小于当前的时间,至少是 2 天前,避免受到时区的影响
date_add(now(), interval -2 day) - first_failure 设为 NULL
- admin_passwords
- last_updated 设为当前的时间
- expires 设为 0
- admin_user_expiration
- 这个表里不能有对应的记录
- 这两个字段的值需要一致
- admin_user.password
- admin_passwords.password_hash
用于观察的 sql
select
admin_user.user_id,
admin_user.firstname,
admin_user.lastname,
admin_user.email,
admin_user.username,
admin_user.is_active,
admin_user.lognum,
admin_user.failures_num,
admin_user.first_failure,
admin_user.lock_expires,
admin_user.password,
admin_passwords.password_id,
admin_passwords.password_hash,
admin_passwords.expires,
FROM_UNIXTIME(admin_passwords.expires),
admin_passwords.last_updated,
FROM_UNIXTIME(admin_passwords.last_updated)
from admin_user
left join admin_passwords on admin_user.user_id = admin_passwords.user_id
WHERE admin_user.email = 'admin@example.com'
order by admin_passwords.password_id desc limit 1;
用于更新的 sql
-- 更新 admin_user
UPDATE admin_user
SET
is_active=1,
failures_num=0,
first_failure=NULL,
-- lock_expires=NULL,
lock_expires=date_add(now(), interval -3 day),
modified=current_timestamp()
where admin_user.email = 'admin@example.com';
-- 更新 admin_passwords
UPDATE admin_passwords
SET
expires=0,
last_updated=unix_timestamp(now())
where password_id = (
select * from (
select password_id
from admin_passwords
left join admin_user on admin_user.user_id = admin_passwords.user_id
where admin_user.email = 'admin@example.com'
order by admin_passwords.password_id desc
limit 1
) as t
);
-- 删除 admin_user_expiration 里对应的记录
DELETE FROM admin_user_expiration
WHERE user_id = (
select user_id
from admin_user
where email = 'admin@example.com'
limit 1
);
生成新的密码
// 直接生成一个密码,在命令行里是用,只运行一次,因为重置了key,可能会使其他逻辑混乱
// 输出的值,填到 admin_user.password 和 admin_passwords.password_hash
/** @var \Magento\Framework\App\ObjectManager */
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
/** @var \Magento\Framework\Encryption\Encryptor */
$encryptor = $objectManager->get(\Magento\Framework\Encryption\Encryptor::class);
/** @var \Magento\Framework\App\DeploymentConfig */
$deploymentConfig = $objectManager->get(\Magento\Framework\App\DeploymentConfig::class);
$cryptkey = preg_split('/\s+/s', trim((string)$deploymentConfig->get('crypt/key')))[0]; // 本地的 key
$cryptkey = '4oyi2yvpl8kx3sh9e4u05vnql41kn8fa'; // crypt/key ,其它的 key ,可能会在本地生成用于线上环境的 password
$encryptor->setNewKey($cryptkey);
$password = 'password#12345678'; // 新的密码
echo $encryptor->getHash($password, true, $encryptor::HASH_VERSION_ARGON2ID13_AGNOSTIC);
exit(0);
通过命令行新建管理员
php bin/magento admin:user:create --admin-user="360magento" --admin-password="Admin@123" --admin-email="admin@360magento.com" --admin-firstname="MyFirstName" --admin-lastname="MyLastName"
分配角色给刚刚新建的用户
INSERT INTO magento_preprod.authorization_role
(parent_id,tree_level,sort_order,role_type,user_id,user_type,role_name,gws_is_all,gws_websites,gws_store_groups)
select
1,2,0,'U',user_id,'2',username,1,NULL,NULL
from admin_user
where admin_user.username = '360magento';
通过在数据库里插入记录来新建管理员
其实就是在这三表表插入对应的记录
admin_user
admin_passwords
authorization_role
在后台新建客户(customer)
和权限相关的表
authorization_role
authorization_rule
sales_order 表的两个状态
- state 是 magento 内部的状态
- 写死在文件
- vendor\magento\module-sales\Model\Order.php
- status 可以是二次开发时自定义的状态
- 写在数据库里的表
- vendor\magento\module-sales\Model\ResourceModel\Order\Status.php
- sales_order_status
- status 和 state 的对应关系在这个表里 sales_order_status_state
打补丁
- 遇到问题,可以先搜索一下 github 的 iusses ,同样的问题可能已经出了补丁,不用自己修改。
- 可以在这个站点里找到对应的补丁
- 关于 Quality Patches Tool (QPT) 工具的使用
- 也可以把下载的 patch 文件单独复制出来,然后用 cweagans/composer-patches 打补丁
单元测试
从文档里看至少有三种命令可以运行测试
直接用 phpunit 运行测试
php ./vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist
用 magento 的命令运行测试
php bin/magento dev:tests:run
用 mftf 运行测试
php ./vendor/bin/mftf run
magento2 也支持使用 codecept 测试,只是不推荐 https://developer.adobe.com/commerce/testing/functional-testing-framework/commands/codeception/
MFTF(Magento Functional Testing Framework, Magento 功能 测试 框架) MFTF 一般是要单独安装的, MFTF 是一个类似于 codecept 的测试框架,但只能用在 magento2
magento2 似乎有一套很完善很复杂的测试 https://developer.adobe.com/commerce/testing/
单独地运行一个测试用例
php vendor/phpunit/phpunit/phpunit -c dev/tests/unit/phpunit.xml.dist app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php
php -d xdebug.remote_autostart=on vendor/phpunit/phpunit/phpunit -c dev/tests/unit/phpunit.xml.dist app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php
composer exec -v -- phpunit -c dev/tests/unit/phpunit.xml.dist app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php
从 marketplace.magento.com 下载和安装拓展
- 登录
- 购买
- 获取包名和版本
- 修改 composer.json 加上 仓库地址和帐号密码
- 运行 composer require
- 修改 app/etc/config.php
- 运行 bin/magento setup:upgrade
- 参考 https://devdocs.magento.com/extensions/install
常见的 magento 扩展供应商
- magento https://commercemarketplace.adobe.com/extensions.html
- mageplaz https://www.mageplaza.com/
- magefan https://magefan.com/
- amasty https://amasty.com/
- vnecoms https://www.vnecoms.com/
- webkul https://webkul.com/
- MGT-Commerce https://www.mgt-commerce.com/
备份源码和数据库
magento2 有备份功能,但已经弃用 https://experienceleague.adobe.com/en/docs/commerce-operations/installation-guide/tutorials/backup
- 备份源码
- 在 composer.json 里加上这一段
"archive": { "exclude": ["var", "generated", "pub/static", ".git", ".vscode"] }- 然后在根目录运行这句命令
composer archive --format=zip --file=magento2 composer archive --format=zip --file=magento2-$(date +%Y%m%d)
- 备份数据库
mysqldump -u root -p magento2 > backup_database.sql
mysqldump -u root -p magento2 --no-create-info > backup_database.sql
参考
中文文档 https://experienceleague.adobe.com/docs/commerce.html?lang=zh-Hans
github 里 magento2 的模块例子
- https://github.com/magento/magento2-samples
- sample-module-minimal 是最简单的模块例子
https://developer.adobe.com/commerce/php/architecture/
生成 magento 模块 https://cedcommerce.com/magento-2-module-creator/
https://devdocs.magento.com/guides/v2.4/extension-dev-guide/module-development.html



















