Commands That Need an Area
A console command starts with no area set, so emulate one before touching anything that expects a web request.
public function emulateAreaCode($areaCode, $callback, $params = [])An area is a named application context: frontend, adminhtml, crontab, webapi_rest, graphql, global. It decides which di.xml overrides apply, which translations load and which theme is active. A web request sets the area before any of your code runs. bin/magento does not set one at all.
The error
The first thing that asks for the area throws. Magento\Framework\App\State::getAreaCode() throws a LocalizedException whose message is Area code is not set. Anything that starts a session reports Area code not set: Area code must be set before starting a session. Both mean the same thing: you reached code written for a web context.
Setting one
Two methods on State do it. setAreaCode(string $code) fixes the area for the rest of the process. emulateAreaCode(string $areaCode, callable $callback, array $params = []) sets it, runs the callback, returns its value, and restores the previous area in a finally block. Prefer emulation: it is scoped, and it cannot collide with an area something else already set.
An area is not a store
Store-scoped values — the store name, currency, locale, the active theme — come from a store, and the command line has no current store either. Read a value for a named store with ScopeConfigInterface::getValue($path, ScopeInterface::SCOPE_STORE, $storeCode), or switch the whole environment with Magento\Store\Model\App\Emulation. Neither needs a cache flush; code changes do, so run bin/magento cache:flush after editing.
Example
<?php
// app/code/Soundscode/Ops/Console/Command/StoreInfoCommand.php
declare(strict_types=1);
namespace Soundscode\Ops\Console\Command;
use Magento\Framework\App\Area;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\State;
use Magento\Framework\Console\Cli;
use Magento\Store\Model\ScopeInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'soundscode:store:info', description: 'Shows a store name under the frontend area')]
class StoreInfoCommand extends Command
{
public function __construct(
private readonly State $appState,
private readonly ScopeConfigInterface $scopeConfig
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('store', InputArgument::REQUIRED, 'Store code');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln($this->appState->emulateAreaCode(
Area::AREA_FRONTEND,
$this->describe(...),
[(string) $input->getArgument('store')]
));
return Cli::RETURN_SUCCESS;
}
private function describe(string $storeCode): string
{
$name = (string) $this->scopeConfig->getValue(
'general/store_information/name',
ScopeInterface::SCOPE_STORE,
$storeCode
);
return sprintf('%s [area: %s]', $name, $this->appState->getAreaCode());
}
}
// $ bin/magento soundscode:store:info default
// Soundscode Audio [area: frontend]When to use it
- Render a transactional email from the command line to check a template change.
- Reindex or export per store, reading each store's own configuration values.
- Fix a command that dies with "Area code is not set" the moment it calls into core code.
More examples
The callable, shorthand and longhand
emulateAreaCode() takes any PHP callable and invokes it with call_user_func_array(), so these three forms are interchangeable. $this->describe(...) is the short form of [$this, 'describe'] and both resolve to the same bound method; a static analyser understands the first and often cannot check the second, which is the practical reason to prefer it. Note that the third argument is the parameter list for the callback, not for emulateAreaCode itself, so a closure that captures its data with use needs no third argument.
<?php
// Both forms pass the same callable to emulateAreaCode().
declare(strict_types=1);
// PHP 8.1 and later: first-class callable syntax.
$result = $this->appState->emulateAreaCode(
Area::AREA_FRONTEND,
$this->describe(...),
['default']
);
// The longhand it desugars to: an array callable.
$result = $this->appState->emulateAreaCode(
Area::AREA_FRONTEND,
[$this, 'describe'],
['default']
);
// A closure works too, and is handy for two or three lines of work.
$result = $this->appState->emulateAreaCode(
Area::AREA_FRONTEND,
function () use ($storeCode): string {
return $this->describe($storeCode);
}
);Emulating a whole store environment
Reach for this when the area alone is not enough: generating a transactional email body, rendering a price, or resolving a media URL all read the store's locale, translations and theme. startEnvironmentEmulation($storeId, $area, $force) takes the store id, not the code, and the third argument forces emulation even when the target store already looks like the current one. Two details bite people. The method allows only one level of nesting and returns silently if emulation is already active, so never call it twice without stopping. And always stop in a finally block, or every later line of the command keeps the emulated locale.
<?php
// app/code/Soundscode/Ops/Console/Command/PreviewCommand.php
declare(strict_types=1);
namespace Soundscode\Ops\Console\Command;
use Magento\Framework\App\Area;
use Magento\Framework\Console\Cli;
use Magento\Store\Model\App\Emulation;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class PreviewCommand extends Command
{
public function __construct(
private readonly Emulation $emulation
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->emulation->startEnvironmentEmulation(
(int) $input->getArgument('store_id'),
Area::AREA_FRONTEND,
true
);
try {
// Locale, translations, theme and current store now match that store.
$output->writeln('<info>rendered</info>');
} finally {
$this->emulation->stopEnvironmentEmulation();
}
return Cli::RETURN_SUCCESS;
}
}
Discussion