自定义命令里调用客户、税率或订单服务时,常见异常是 Area code is not set。把 $state->setAreaCode('adminhtml') 塞进构造函数,当前命令也许能跑,但随后执行任意 bin/magento 命令又可能变成 Area code is already set。原因是 Magento 启动 CLI 时会实例化命令列表,构造函数不是“只有执行这条命令时才运行”。

先看是哪条依赖在构造阶段启动了Session

从异常栈往上找第一个自定义类。如果命令构造函数直接注入了重量级服务,该服务的依赖链可能创建 customer session,而 session 必须知道当前 area。此时问题不只是少一行 setAreaCode,而是对象创建得太早。

容易触发的写法类似:

public function __construct(
    private TaxCalculationInterface $taxCalculation,
    AppState $appState
) {
    $appState->setAreaCode(Area::AREA_ADMINHTML);
    parent::__construct();
}

把area设置移到execute,构造函数只保存依赖

protected function execute(InputInterface $input, OutputInterface $output): int
{
    $this->appState->setAreaCode(Area::AREA_ADMINHTML);
    $this->runImport();
    return Command::SUCCESS;
}

area 要按实际业务选择。读取前台价格、客户组上下文时未必应该用 adminhtml;纯全局处理也不要为了消掉异常随便选一个 area。

这段逻辑可能被别处调用,就用emulateAreaCode包住

return $this->appState->emulateAreaCode(
    Area::AREA_ADMINHTML,
    fn () => $this->runImport()
);

emulation 的好处是执行完成后恢复原状态,适合共享服务或测试。但如果依赖在构造阶段已经启动 session,emulation 仍然来不及,这时要把该依赖改成 Proxy 或 Factory,让真实对象到 execute() 里才创建。

修完要跑的不只是这条命令

先执行目标命令,再跑:

php bin/magento list
php bin/magento cache:status
php bin/magento setup:di:compile

list 很重要,因为它会加载全部命令,最容易暴露“构造函数里抢先设置 area”的问题。真正稳定的修复是控制初始化时机,而不是捕获 Area code is already set 后假装没事。