模块的路径是这样的,开发商名称和模块名称都使用 大驼峰 的形式命名
app/code/开发商名称/模块名称
默认路由是这样的
routeid/controller/action
app/code/LocalDev/HelloModule
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'LocalDev_HelloModule',
__DIR__
);
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="LocalDev_HelloModule" setup_version="1.0.9"></module>
</config>
<?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>
新建 Controller 和 action
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;
}
}
localdev_hello_world.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>
container.phtml
的文件,这个文件名要和 xml 文件里的 template 属性对应 <?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
<?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
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>
在模块目录里新建一个文件夹 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');
}
}
}
php bin/magento example:sayhello
运行这句命令 php bin/magento setup:upgrade
更新数据
php bin/magento list
,看看能不能找到新加的命令php bin/magento example:sayhello
参考 https://developer.adobe.com/commerce/php/development/cli-commands/custom/
新建 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>
<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}'
在模块目录 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
}
在模块目录 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 ;
}
}
运行这句命令 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
浏览器可以安装这个拓展 https://github.com/altair-graphql/altair
这是 graphql 的中文文档 https://graphql.cn/
参考 https://devdocs.magento.com/guides/v2.4/graphql/index.html
magento 的索引器有两种类型
两个和索引器相关的表
在模块目录 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>
在模块目录 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 是以定时任务的形式运行的 (虽然也可以使用其它方式运行,但文档里的里的例子就是用定时任务的)
* * * * * php bin/magento cron:run --group=index
定时任务的配置文件在这个位置
vendor\magento\module-indexer\etc\crontab.xml
这个 crontab.xml 文件里有三个任务
因为是定时任务,所以可以用这样的 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
<?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>
在模块目录 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 配置的解释
自己写 crontab 配置或用其它方式(例如 supervisor )让 cron:run 一直运行也是可以的了
<!-- 模块名 magento/module-cron 模型文件 vendor\magento\module-cron\Model\Schedule.php 定时任务可能的状态 pending 计划中 running 运行中 success 运行成功 missed 错过 error 运行失败 插入的语句如果执行的时间太迟,可能会被删掉? 在这个位置,会把schedule_at不符合cron表达式的记录删掉,再执行符合cron表达式的任务 vendor\magento\module-cron\Observer\ProcessCronQueueObserver.php _generateJobs saveSchedule cleanupScheduleMismatches schedule->trySchedule 之前没发现这个问题,是因为我是用 index 去测试的,index的cron表达式全是 * 或 每十分钟 执行一次,很容易遇到正确的时间,所以就没有发现这个问题 所以,强行向数据库插记录,也不是总是能成功执行到对应的定时任务 输入 magento cron:run 命令两三次。 第一次输入命令时,它会将作业排入队列;随后,将运行cron作业。 必须输入命令 至少 两次。 cron 命令的这个参数应该是用来标识 父进程 和 子进程 的 bootstrap= vendor\magento\module-cron\Console\Command\CronCommand.php vendor\magento\framework\App\Cron.php vendor\magento\module-cron\Observer\ProcessCronQueueObserver.php 父进程为一个组开启一个进程 子进程中 先设置一个锁 获得锁后 清除过期的任务 删除 cron_schedule 表的记录 新建任务 在 cron_schedule 表中插入新记录 执行任务 根据 cron_schedule 表中的记录执行任务 在数据库里修改 cron 的 cron expr path crontab/group_id/jobs/job_id/schedule/cron_expr 这一段其实是自定义的 这一段需要现在 crontab.xml 里配置 通常这一段会覆盖 xml 文件中 schedule 的值 这个修改好像生效了。。。 crontab/reports/jobs/promotion_group_attribute/schedule/cron_expr 这个可能是要重新部署才能生效,即使是改数据库,可能是一次缓存了 输出全部的 cronjob /** @var \Magento\Cron\Model\Config\Data */ $configData = $objectManager->get(\Magento\Cron\Model\Config\Data::class); var_dump($configData->getJobs()); php -a <<- 'EOF' > cron.json 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) ); /** @var \Magento\Cron\Model\Config\Data */ $configData = $objectManager->get(\Magento\Cron\Model\Config\Data::class); echo json_encode($configData->getJobs(), JSON_PRETTY_PRINT); } catch (\Throwable $e) { echo $e->getFile() . ':' . $e->getLine() . PHP_EOL; echo $e->getMessage() . PHP_EOL . $e->getTraceAsString(); } EOF SELECT * from cron_schedule order by schedule_id desc limit 10; SELECT * from cron_schedule WHERE job_code in ('promotion_group_attribute') order by schedule_id desc; INSERT INTO cron_schedule (job_code,status,created_at,scheduled_at) VALUES ('promotion_group_attribute','pending',CURRENT_TIMESTAMP(), date_add(CURRENT_TIMESTAMP(), interval 1 minute)); INSERT INTO cron_schedule (job_code,status,created_at,scheduled_at) VALUES ('promotion_group_attribute','pending',CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP()); UPDATE cron_schedule SET scheduled_at=DATE_FORMAT('2023-05-18T16:51:00+08:00', "%Y-%m-%d %H:%i") WHERE schedule_id=54999675; DELETE FROM cron_schedule WHERE schedule_id=55149474; INSERT INTO core_config_data (`scope`, scope_id, `path`, value, updated_at) VALUES ('default', 0, 'crontab/reports/jobs/promotion_group_attribute/schedule/cron_expr', '0 13,17 * * *', CURRENT_TIMESTAMP()); SELECT x.* FROM core_config_data x WHERE `path` LIKE 'crontab%' ORDER BY x.config_id DESC DELETE FROM core_config_data WHERE config_id=2578; 直接在命令行里运行 cronjob ,要在项目的根目录里运行,数据库里就没有运行记录了 php -a <<- 'EOF' 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) ); $instance = \Magento\Sales\Cron\CleanExpiredQuotes::class; $method = 'execute'; $cronJob = $objectManager->get($instance); call_user_func([$cronJob, $method]); } catch (\Throwable $e) { echo $e->getFile() . ':' . $e->getLine() . PHP_EOL; echo $e->getMessage() . PHP_EOL . $e->getTraceAsString(); } EOF --> <config>
<type name="需要拦截的类名(要填完整的类名)">
<plugin name="拦截器名称" type="拦截器的类名(要填完整的类名)" sortOrder="排序" disabled="false" />
</type>
</config>
三种方法的入参和出参
参考 https://developer.adobe.com/commerce/php/development/components/plugins/
<?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>
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]
);
<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>
<?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>
<?php
/** @var \Magento\Framework\View\Element\Template $block */
?>
<p><?=$block->getBaseUrl()?></p>
在模块的 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
<?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>
class Save extends Action
{
public const ADMIN_RESOURCE = 'Magento_Customer::save';
}
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="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>
写在模块的 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());
上面两种写法效果是一样的, 可以这样获取配置的值
/** @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');
<!--
php -a <<- 'EOF'
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)
);
/** @var \Magento\Framework\App\Config\ScopeConfigInterface */
$scopeConfig = $objectManager->get(Magento\Framework\App\Config\ScopeConfigInterface::class);
var_dump($scopeConfig->getValue('general/file/bunch_size'));
} catch (\Throwable $e) {
echo $e->getFile() . ':' . $e->getLine() . PHP_EOL;
echo $e->getMessage() . PHP_EOL . $e->getTraceAsString();
}
EOF
-->
还可以用命令行来修改配置,这种修改会保存在数据库里 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>
<!-- select * from core_config_data where path like '%promo/promotion_group/email_address%' limit 10 -->
<!--
block -> ui_component -> system.xml
ui_component的文档
https://developer.adobe.com/commerce/frontend-core/ui-components/
后台配置的文档
https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/files/config-reference-systemxml.html
后台配置页的Controller
vendor\magento\module-config\Controller\Adminhtml\System\Config\Edit.php
后台配置页的 layout 文件
vendor\magento\module-config\view\adminhtml\layout\adminhtml_system_config_edit.xml
后台配置页的block文件
vendor\magento\module-config\Block\System\Config\Edit.php
vendor\magento\module-config\Block\System\Config\Form.php
后台配置页的phtml文件
vendor\magento\module-config\view\adminhtml\templates\system\config\edit.phtml
vendor\magento\module-backend\view\adminhtml\templates\widget\form.phtml
-->
<?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>
<!--@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();
}
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 的账号密码
<!-- 可以用这个仓库来测试邮件的发送 - https://github.com/axllent/mailpit - 启动命令 ``` mailpit --listen 127.0.0.1:8025 --smtp 127.0.0.1:25 --smtp-auth-accept-any --smtp-auth-allow-insecure ``` - 启动完后用浏览器访问 listen 的地址 - sendmail 和 smtp 两种方式都可以用 mailpit 来测试, mailpit 可以忽略 smtp 的账号密码 mailpit 的版本是 v1.20 邮件里如何加上附件? -->// 从已存在的对象中获取
$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();
<!--
常用的对象
\Magento\Sales\Model\ResourceModel\Order\CollectionFactory
\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory
\Magento\Customer\Model\CustomerFactory
\Magento\Quote\Model\QuoteFactory
shipment
-->
/** @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]);
/**
* @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());
/** @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);
加在这个文件里 app/etc/env.php 加上这段
'db_logger' => [
'output' => 'file',
'log_everything' => 1,
'query_time_threshold' => '0.001',
'include_stacktrace' => 0 // 改成1可以记录代码调用栈
],
日志会输出到这个文件里 var/debug/db.log
通过 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
通过正则表达式搜索某个接口的实现类或某个对象的继承类
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');
通过浏览器的断点来实现前端的调试
笔者在二次开发 magento2 的过程中,登录后台时总是失败, magento2 似乎有一套很 混乱 很 复杂 的规则来限制后台的登录。
这里记录一下通过修改数据库里对应的表来完成登录。
这些记录可能会随着magento的更新而失效
和后台登录相关的表
admin_passwords
admin_user
admin_user_expiration
顺利登录时各个字段的状态
date_add(now(), interval -2 day)
用于观察的 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
中文文档 https://experienceleague.adobe.com/docs/commerce.html?lang=zh-Hans
github 里 magento2 的模块例子
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
<!-- 这是一个收费的文档 https://www.kancloud.cn/zouhongzhao/magento2-in-action 在这个位置加上 WHERE vendor\magento\zendframework1\library\Zend\Db\Select.php _where Filter vendor\magento\framework\Api\Filter.php vendor\magento\framework\Api\AbstractSimpleObject.php FilterGroup vendor\magento\framework\Api\Search\FilterGroup.php vendor\magento\framework\Api\AbstractSimpleObject.php filter_groups -> FilterGroup的数组 FilterGroup -> \Magento\Framework\Api\Search\FilterGroup filters -> Filter的数组 Filter -> \Magento\Framework\Api\Search\Filter 搜索通常是使用这两种对象 SearchCriteria Collection 搜索通常是把 Filter 转换为 sql 或 es 的 where 语句 getData 和 setData 一般的对象 vendor\magento\framework\DataObject.php 模型的对象 vendor\magento\framework\Model\AbstractModel.php 除了对应的方法还要留意构造函数 子类的方法有可能覆盖父类的方法 magento2 里用于执行单个定时任务的工具 https://github.com/netz98/n98-magerun2 安装这个工具可以直接跑某个指定的cron job n98-magerun2.phar sys:cron:run sales_clean_quotes 其实这个工具还有很多其它功能的 curl -O https://files.magerun.net/n98-magerun2.phar && chmod +x ./n98-magerun2.phar; su www-data -c "./n98-magerun2.phar sys:cron:run sales_clean_quotes" 配置文件修改后,要清除一次缓存 php bin/magento c:c php bin/magento setup:upgrade --keep-generated 构建前端时忽略后台 php bin/magento setup:static-content:deploy -f --exclude-area=adminhtml 构建前端时忽略前台 php bin/magento setup:static-content:deploy -f --exclude-area=frontend -j 参数 使用多进程的方式构建前台 -j 参数 windows 用不了,因为依赖了 pcntl_fork php bin/magento setup:static-content:deploy -f -j 8 --exclude-area=frontend php bin/magento setup:static-content:deploy -f -j 8 --exclude-area=adminhtml 只构建英语的前台 php bin/magento setup:static-content:deploy -f --area=frontend --language=en_US 只构建英语的后台 php bin/magento setup:static-content:deploy -f --area=adminhtml --language=en_US php bin/magento setup:static-content:deploy --help 还有一些技巧 查看一个类的 Preference 和 Plugins php bin/magento dev:di:info "Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor" magento/catalog 后台设置位置 Catalog -> Products -> product setting -> Related Products, Up-sells, and Cross-sells 相关的索引器 catalogrule_rule catalogrule_product magento/module-target-rule 这个不生效,应该是索引的问题 后台设置位置 Marketing -> Related Products Rules 相关的索引器 targetrule_rule_product targetrule_product_rule 相关的表 magento_targetrule magento_targetrule_product magento_targetrule_customersegment amasty/mostviewed 后台设置位置 Catalog -> Related Product Rules 相关的索引器 amasty_mostviewed_rule_product amasty_mostviewed_product_rule magento/catalog 是 ce 版的功能 magento/module-target-rule 是 ee 版的功能 amasty/mostviewed 是第三方的模块 magento/module-target-rule 是通过替换 block 的方式加上对应的产品 vendor\magento\module-target-rule\view\frontend\layout\catalog_product_view.xml amasty/mostviewed 是通过 拦截 的方式加上对应的产品 vendor\amasty\mostviewed\etc\frontend\di.xml <type name="Magento\Catalog\Block\Product\ProductList\Related"> <plugin name="Amasty_Mostviewed::collectionRelated" type="Amasty\Mostviewed\Plugin\Community\Related"/> </type> <type name="Magento\TargetRule\Block\Product\AbstractProduct"> <plugin name="Amasty_Mostviewed::collection" type="Amasty\Mostviewed\Plugin\Enterprise\Product"/> </type> AdminPortal MARKETING Cart Price Rules 优惠券大概就两种 指定 code 的 自动生成 code 的 自动生成的优惠券是通过队列生成的 这是运行队列的命令 php bin/magento queue:consumers:start codegeneratorProcessor 优惠券和订单相关的表 salesrule_coupon_usage coupon_id 对应 salesrule_coupon 的 coupon_id customer_id times_used 同一个用户消耗同一个 coupon_code 的数量 salesrule_coupon \Magento\SalesRule\Model\Coupon coupon_id rule_id 对应 sequence_salesrule 的 sequence_value code 这个字段就是优惠码 usage_limit usage_per_customer times_used 同一个 coupon_code 消耗的数量 expiration_date 到期时间 salesrule_customer \Magento\SalesRule\Model\Rule\Customer rule_id 对应 sequence_salesrule 的 sequence_value customer_id times_used 同一个用户消耗同一个 rule 的数量 sequence_salesrule 这是一个奇怪的表,应该和队列有关 sequence_value salesrule 这个表的值对应 Cart Price Rules 页面的值 row_id 这个是主键,这个是版本 rule_id 对应 sequence_salesrule 的 sequence_value name times_used 只要 rule 的 coupon_code 有消耗就会加1 uses_per_customer uses_per_coupon conditions_serialized 生效的条件,这是一个 json 字符串 amasty_amrules_usage_limit salesrule_id 对应 salesrule 的 row_id limit 全局的数量限制? sales_order coupon_code 对应 salesrule_coupon 的 code coupon_rule_name 对应 salesrule 的 name applied_rule_ids 这个订单应用了哪些 rule quote coupon_code 对应 salesrule_coupon 的 code applied_rule_ids 这个购物车应用了哪些 rule applied_rule_ids 是一个字符串 多个值用逗号开个,单个值就是 salesrule 里的 rule_id 这个是对应的索引器 salesrule_rule salesrule salesrule_coupon 一对多 salesrule_coupon salesrule_coupon_usage 一对多 salesrule salesrule_customer 一对多 check per coupon usage limit salesrule_coupon salesrule_coupon.usage_limit 存在 且 salesrule_coupon.times_used 大于等于 salesrule_coupon.usage_limit 返回 flase salesrule_coupon_usage.times_used 大于等于 salesrule_coupon.usage_per_customer 返回 flase check per rule usage limit salesrule salesrule_customer.times_used 大于等于 salesrule.uses_per_customer 返回 false coupon_code 是否达到了数量上限 这个用户使用了同一个 coupon_code 多少次 这个用户使用了同一个 rule 多少次 在购物车使用了,不会更新优惠券的表 好像多个用户使用同一优惠券加入购物车都不会有影响 多个用户使用同一优惠券加入购物车 其中一个用户先结算 如果优惠券有数量限制,那么另一个会自动失效,但没有提示 coupon 新建界面里的 Uses per Coupon Uses per Customer 对应的是 salesrule uses_per_coupon uses_per_customer salesrule_coupon usage_limit usage_per_customer coupon 新建界面里的 Global Uses Limit 对应的是 amasty_amrules_usage_limit limit select row_id, rule_id, name, description, conditions_serialized from salesrule where rule_id = 5898; 可以通过 conditions_serialized 字段查看 rule 生效的代码 关键对象 和 相关的表 客户 customer_entity customer_id username email 产品 catalog_product_entity product_id 产品的状态 status visibility approval 这几个值都在 catalog_product_entity_int select @attr_product_status:=attribute_id from eav_attribute where attribute_code = 'status' and backend_type = 'int'; select @attr_approval:=attribute_id from eav_attribute where attribute_code = 'approval' and backend_type = 'int'; select @attr_visibility:=attribute_id from eav_attribute where attribute_code = 'visibility' and backend_type = 'int'; 产品的库存 cataloginventory_stock vendor\magento\module-catalog-inventory\Model\ResourceModel\Stock.php cataloginventory_stock_item vendor\magento\module-catalog-inventory\Model\ResourceModel\Stock\Item.php is_in_stock qty cataloginventory_stock_status vendor\magento\module-catalog-inventory\Model\ResourceModel\Stock\Status.php stock_status qty eav 里也有一个和库存相关的值 quantity_and_stock_status 这个值在 catalog_product_entity_int select @attr_quantity_and_stock_status:=attribute_id from eav_attribute where attribute_code = 'quantity_and_stock_status' and backend_type = 'int'; 但似乎已经弃用了 购物车 quote 购物车id quote.entity_id customer_id 相关的表 quote quote_item quote_item_option quote_address quote_shipping_rate union_shipping_quote_item 订单 sales_order order_id customer_id increment_id 相关的表 sales_order sales_order_item sales_order_status sales_order_status_history sales_order_payment sales_order_address sales_creditmemo sales_creditmemo_comment sales_order_tax sales_invoice sales_invoice_comment sales_shipment sales_shipment_item sales_shipment_comment sales_shipment_track 订单的送货 sales_shipment 订单的备忘录 sales_creditmemo 订单的发票 sales_invoice 地址 customer_address_entity quote_address quote_address_item sales_order_address service_center_address union_shipping_oto_store 分类 catalog_category_entity 管理员 admin_user 支付方式 sales_order_payment 销售规则 salesrule 配置 core_config_data 还有一些 _grid 结尾的表 eav 模型里还有一些表无法理解? **_eav_attribute 例如 customer_eav_attribute catalog_eav_attribute eav_attribute_group eav_attribute_label eav_attribute_option eav_attribute_option_switch eav_attribute_option_value 一些情况下 eav 里具体的值好像是存在这个表里的 eav_attribute_set eav_entity_attribute magento2的布局有两种类型 1. 页面布局(page layout) -> 在 page_layout 目录里的 xml 文件 2. 页面配置(page configuration) -> 在 layout 目录里的 xml 文件 页面布局 的 xml 只包含 容器 页面配置 的 文件名 就是 布局id 绝大多数情况下修改的是 页面配置 文件 这个就是 magento2 最基础的布局 vendor\magento\module-theme\view\base\page_layout\empty.xml 更完整的代码可以参考这个目录下的文件 vendor\magento\module-theme\view\base // magento2 的事务 /** @var \Magento\Framework\App\ResourceConnection */ $resourceConnection = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\App\ResourceConnection::class); $connection = $resourceConnection->getConnection(); $connection->beginTransaction(); try { // 一些数据库修改的操作 $connection->commit(); } catch (\Exception $e) { $connection->rollBack(); throw $e; } $timeZone = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); $currentTimezone = @date_default_timezone_get(); @date_default_timezone_set($timeZone->getConfigTimezone()); $strtime = strtotime($strtime); @date_default_timezone_set($currentTimezone); 在这个位置,也把时区设为 utc app\bootstrap.php 在这个位置里,连接完数据库后,时区会马上设置为 utc vendor\magento\framework\DB\Adapter\Pdo\Mysql.php _connect 在 magento2 里,一些位置能自动完成时区的转换,一些位置还是需要手动来转换 数据库里的类型 int string date datetime time 需要显示的格式 时间戳 格式化的字符串 格式化的字符串 $fmt = new \IntlDateFormatter($storeCode); $fmt->setTimeZone($timezone); $fmt->setPattern('yyyy年 M月 dd日, hh:mm a'); return $fmt->format($timestamp); https://www.php.net/manual/en/class.intldateformatter.php https://unicode-org.github.io/icu/userguide/format_parse/datetime/ magento2 的时区保存在core_config_data表的这个位置 general/locale/timezone select * from core_config_data where path like '%timezone%' 在php的代码里这样获取 $timezone = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); var_dump($timezone->getConfigTimezone()); 在后台里这样设置 Stores -> Configuration -> General -> General -> Locale Options Magento2 有三种运行模式,按性能由低到高, 依次为:developer < default < production magento2 有三种运行模式,分别是: developer:这是开发者模式,适合开发和调试 magento2 应用。在这个模式下,错误信息和日志会更详细,静态文件不会缓存,代码修改会立即生效。 default:这是默认模式,适合一般的使用场景。在这个模式下,错误信息和日志会比较简洁,静态文件会缓存,代码修改需要重新部署才能生效。 production:这是生产模式,适合正式的运营环境。在这个模式下,错误信息和日志会最少,静态文件会压缩和合并,代码修改需要重新编译和部署才能生效。 你可以使用以下命令来查看或设置 magento2 的运行模式: bin/magento deploy:mode:show:查看当前的运行模式 bin/magento deploy:mode:set {mode}:设置运行模式为 developer, default 或 production bin/magento deploy:mode:set production --skip-compilation:设置运行模式为 production 但跳过编译步骤 magento2 的维护模式 php bin/magento maintenance:enable php bin/magento maintenance:disable php bin/magento maintenance:status magneto2 的维护模式是用在生产环境里的, magento2 查看当前开发模式 php bin/magento deploy:mode:show magento2 把开发模式切换成 开发者模式 php bin/magento deploy:mode:set developer magento2 安装示例数据,安装示例数据需要切换到开发者模式 php bin/magento sampledata:deploy php bin/magento setup:upgrade magento2 是如何加载对象的? Plugins 是如何实现的? preference 是如何实现的? Events 是如何实现的? 有没有什么办法可以手动更新 generated 里的文件? 一个 界面 的显示是被哪些数据所影响的? 数据 赋值在哪里 保存在哪里 显示在哪里 从获取到显示之间经过了哪些位置? magento2 是如何加载对象的? 这个要先了解 composer 是如何自动加载对象的 spl_autoload_register 这个要先了解 原生的php 是如何加载对象的 include include_once require require_once 命名空间 几乎所有的类都是通过 \Magento\Framework\App\ObjectManager 的 create 方法创建的 create 方法 之后才是 composer 的 loadClass 方法 \Magento\Framework\App\ObjectManager create -> \Magento\Framework\ObjectManager\ObjectManager create -> \Magento\Framework\ObjectManager\Factory\AbstractFactory createObject 模块下的这几个文件是什么时候加载的? etc/module.xml composer.json 这个是给 composer 用的,只有 composer 的命令会用到 registration.php 也是通过 composer 的文件来加载的 根目录下的 composer.json 里有声明 引用这个文件 app/etc/NonComposerComponentRegistration.php NonComposerComponentRegistration.php 会加载全部模块下的 registration.php 其实 registration.php 也只是运行一次 \Magento\Framework\Component\ComponentRegistrar::register magento2 是如何读取配置的? 没有缓存的 有缓存的 配置文件是通过这里读取的 \Magento\Framework\Config\FileResolverInterface \Magento\Framework\Config\FileResolver 获取配置文件路径 vendor\magento\framework\Module\Dir\Reader.php 读取配置文件内容 vendor\magento\framework\Filesystem\File\Read.php 这个是用于读取配置的 \Magento\Framework\App\Config\ScopeConfigInterface \Magento\Framework\App\Config \Magento\Framework\App\Config\ScopeCodeResolver \Magento\Framework\App\Config\ConfigTypeInterface \Magento\Framework\App\Config\ConfigSourceInterface Magento\Config\App\Config\Type\System \Magento\Config\App\Config\Type\System\Reader \Magento\Framework\App\Config\ConfigSourceInterface 这个接口似乎有很多实现的类 这个是用于修改配置的,主要是针对数据库的 core_config_data 表 \Magento\Config\Model\Config 后台配置页的Controller vendor\magento\module-config\Controller\Adminhtml\System\Config\Edit.php 最早的配置是在 \Magento\Framework\App\ObjectManagerFactory create 里读取的 在这个方法中 \Magento\Framework\App\ObjectManagerFactory create 会创建一个新的 ObjectManager 对象, 会传入 \Magento\Framework\ObjectManager\FactoryInterface 对象, 最早的 di 文件, 最早的 $sharedInstances 这些对象是写死在代码里的 入口文件 require __DIR__ . '/app/bootstrap.php'; require_once __DIR__ . '/autoload.php'; // 在这个位置加载 composer ./vendor/autoload.php // 创建一个 Bootstrap 对象 $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); // 创建一个 Application 对象 $app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); // Application 对象 也是通过 objectManager 对象创建的 // $application = $this->objectManager->create($type, $arguments); // 通过 Bootstrap 对象 运行 Application 对象 $bootstrap->run($app); // 这两句是关键 $response = $application->launch(); // 处理请求 $response->sendResponse(); // 输出响应 vendor\magento\framework\App\Http.php launch vendor\magento\framework\App\FrontController.php dispatch vendor/magento/framework/App/Router/DefaultRouter.php match // 匹配路由 processRequest // 处理请求 vendor\magento\framework\App\Request\ValidatorInterface.php validate // 判断请求是否合法 getActionResponse // 处理请求 执行 $actionInstance->dispatch($request); 或 $actionInstance->execute(); $actionInstance 就是具体的控制器对象了 从这里返回的就是 result 了 renderResult magento2 的命令行是通过 \Magento\Framework\Console\Cli 继承 \Symfony\Component\Console\Application 后,直接调用 \Symfony\Component\Console\Application doRun 实现的 所以 magento2 的核心其实是 \Magento\Framework\App\Bootstrap \Magento\Framework\App\ObjectManagerFactory \Magento\Framework\App\ObjectManager 路由 配置文件中的 routes.xml 数据库中的表 url_rewrite catalog_url_rewrite_product_category view 下的 layout 下的 xml 文件的命名方式通常是,都是小写字母 routeid_controller_action 如果 routeid 或 controller 或 action 里有下滑线或大写字母时要怎么处理? action类的 execute 方法通常是返回一个 \Magento\Framework\View\Result\Page 对象 action类的 execute 方法大概就返回四种 result \Magento\Framework\View\Result\page \Magento\Framework\Controller\Result\Json \Magento\Framework\Controller\Result\Raw \Magento\Framework\Controller\Result\Forward 转发到其它 action \Magento\Framework\Controller\Result\Redirect http的重定向 这些类都继承自 \Magento\Framework\Controller\AbstractResult 如何加载 page_layout 和 layout 还是有一点模糊 那些可以迅速定位问题的文件?各种入口? http frontend backend rest graphql vendor\magento\module-graph-ql\etc\graphql\di.xml 在这个文件里,把 Magento\Framework\App\FrontControllerInterface 声明为 Magento\GraphQl\Controller\GraphQl vendor\magento\module-graph-ql\Controller\GraphQl.php Magento\Framework\GraphQl\Query\QueryProcessor vendor\magento\framework\GraphQl\Query\QueryProcessor.php ScandiPWA\PersistedQuery\Query\QueryProcessor vendor\scandipwa\persisted-query\src\Query\QueryProcessor.php "vendor/webonyx/graphql-php/src/GraphQL.php:94 GraphQL\\GraphQL promiseToExecute", "vendor/webonyx/graphql-php/src/GraphQL.php:162 GraphQL\\Executor\\Executor promiseToExecute", "vendor/webonyx/graphql-php/src/Executor/Executor.php:156 GraphQL\\Executor\\ReferenceExecutor doExecute", "vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php:215 GraphQL\\Executor\\ReferenceExecutor executeOperation", "vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php:264 GraphQL\\Executor\\ReferenceExecutor executeFields", "vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php:1195 GraphQL\\Executor\\ReferenceExecutor resolveField", "vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php:550 GraphQL\\Executor\\ReferenceExecutor resolveFieldValueOrError", vendor\webonyx\graphql-php\src\Executor\ExecutionContext.php vendor\webonyx\graphql-php\src\Type\Definition\FieldDefinition.php "vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php:623 Magento\\Framework\\GraphQl\\Query\\Resolver\\PromiseFactory Magento\\Framework\\GraphQl\\Query\\Resolver\\{closure}", vendor\magento\framework\GraphQl\Query\Resolver\PromiseFactory.php resolveType 和 resolve 有什么区别? 从源码来看 magento2 中的 graphql 的 contect 的第一个 fieldResolver 都是 defaultFieldResolver 从日志来看,似乎只有 fieldDef 和 exeContext 两种类型,又因为 exeContext 是 defaultFieldResolver ,所以只需要关注 fieldDef 就可以了 然后 fieldDef 还有三种类型 schemaMetaFieldDef typeMetaFieldDef typeNameMetaFieldDef vendor\magento\framework\GraphQl\Query\Resolver\PromiseFactory.php 这个文件是关键 graphql 是如何加载到这个文件的? 加在那个匿名函数里,可以通过 $resolver的类型 或 fieldName 或 path 来区分不同的请求 get_class($resolver), $info->fieldName, join(PHP_EOL, $info->path) console console cron document.cookie="XDEBUG_SESSION=vscode" 从数据库里看,当前系统有这么多种支付方式 select sales_order_payment.method, count(sales_order_payment.method) as 'count' FROM sales_order_payment group by sales_order_payment.method order by count desc 从数据库里看,当前系统有这么多种送货方式 select shipping_method, count(shipping_method) as 'count' FROM sales_order order by count desc 查看订单的支付方式 select sales_order.entity_id, sales_order.increment_id, sales_order.shipping_method, sales_order_payment.method AS payment_method, sales_order_payment.additional_information AS payment_info FROM sales_order JOIN sales_order_payment ON sales_order.entity_id = sales_order_payment.parent_id WHERE sales_order.increment_id = 3100182449; order by sales_order.entity_id desc limit 100 查看一个订单下的产品 select sales_order.entity_id, sales_order.increment_id, sales_order.shipping_method, sales_order_item.product_id, sales_order_item.sku, sales_order_item.name, sales_order_item.product_type, sales_order_item.is_virtual FROM sales_order JOIN sales_order_item on sales_order.entity_id = sales_order_item.order_id WHERE sales_order.increment_id = 3100182449; WHERE sales_order.entity_id = 28546; 3100182449 如果 http 头里存在这个字段 X-Requested-With ,而且这个字段的值是 XMLHttpRequest 那么这就是一个 ajax 请求 在控制器里可以这样判断 $isAjax = $this->getRequest()->isXmlHttpRequest() vendor\laminas\laminas-http\src\Request.php $isAjax = $this->getRequest()->isAjax() lib\internal\Magento\Framework\App\Request\Http.php 除了 isXmlHttpRequest 还会判断参数里有没有 ajax 或 isAjax 所以 isAjax() 应该会准确一些 在控制器里可以像这样输出 json 字符串 public function execute() { /** @var \Magento\Framework\Controller\Result\Json $result */ $result = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_JSON); return $result->setData(['isAjax' => $this->getRequest()->isAjax()]); } select * FROM sales_order WHERE sales_order.increment_id = 3100182449 \G select sales_order_payment.* FROM sales_order JOIN sales_order_payment ON sales_order.entity_id = sales_order_payment.parent_id WHERE sales_order.increment_id = 3100182449 \G SELECT x.* FROM core_config_data x WHERE value like '%sales%'; select entity_id, is_virtual, customer_id, email_sent, send_email, increment_id, customer_email, club_member_id, area_code, created_at, updated_at FROM sales_order WHERE sales_order.increment_id = 3100182449 \G <?php /* sudo rm -rf app/code/Vendor sudo rm -rf app/code/Vendor/ModuleName php createModule.php */ $vendor = 'Vendor'; $moduleName = 'ModuleName'; $vendor = ucfirst($vendor); $moduleName = ucfirst($moduleName); $modulePath = 'app/code/' . $vendor . '/' . $moduleName; mkdir($modulePath . '/etc', 0755, true); $moduleContent = <<< EOF <?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="{$vendor}_{$moduleName}"/> </config> EOF; $composerName = strtolower($vendor) . '/' . strtolower($moduleName); $composerContent = <<< EOF { "name": "$composerName", "description": "N/A", "type": "magento2-module", "config": { "sort-packages": true }, "autoload": { "files": [ "registration.php" ], "psr-4": { "$vendor\\\\$moduleName\\\\": "" } } } EOF; $registrationContent = <<< EOF <?php use Magento\Framework\Component\ComponentRegistrar; ComponentRegistrar::register(ComponentRegistrar::MODULE, '{$vendor}_{$moduleName}', __DIR__); EOF; file_put_contents($modulePath . '/etc/module.xml', $moduleContent); file_put_contents($modulePath . '/composer.json', $composerContent); file_put_contents($modulePath . '/registration.php', $registrationContent); -->