Comandos de console
Nem toda tarefa passa pelo navegador. Importar uma planilha, mandar e-mails pendentes, limpar registros antigos: isso roda no terminal ou num agendador como o cron. No Yii3, esses comandos usam o mesmo container e a mesma configuração da aplicação web, então qualquer serviço que você já tem fica disponível neles.
Como o console funciona
O script yii, na raiz do projeto, cria um ConsoleApplicationRunner. Ele monta a configuração e o container do mesmo jeito que o public/index.php, só que com os grupos de console (params-console, di-console). A aplicação de console é o pacote yiisoft/yii-console, que por baixo usa o Symfony Console.
Isso quer dizer que um comando do Yii3 é um comando do Symfony: uma classe que estende Symfony\Component\Console\Command\Command. Argumentos, opções, tabelas e perguntas interativas seguem a documentação do Symfony.
O template já traz um exemplo, o src/Console/HelloCommand.php, que imprime Hello! quando você roda ./yii hello.
Criando um comando
Vamos criar um comando que lista os países da tabela pais, criada em Trabalhando com banco de dados. Ele aceita uma busca opcional pelo nome e a opção --limite. Crie src/Topicos/Console/ListarPaisesCommand.php:
<?php
declare(strict_types=1);
namespace App\Topicos\Console;
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\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Yii\Console\ExitCode;
#[AsCommand(
name: 'pais:listar',
description: 'Lista os países cadastrados',
)]
final class ListarPaisesCommand extends Command
{
public function __construct(
private readonly ConnectionInterface $db,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('busca', InputArgument::OPTIONAL, 'Parte do nome do país')
->addOption('limite', 'l', InputOption::VALUE_REQUIRED, 'Quantidade máxima de linhas', '5');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$limite = (int) $input->getOption('limite');
if ($limite < 1) {
$io->error('O limite precisa ser um número maior que zero.');
return ExitCode::USAGE;
}
$query = $this->db
->select(['codigo', 'nome', 'populacao'])
->from('pais')
->orderBy(['nome' => SORT_ASC])
->limit($limite);
/** @var string|null $busca */
$busca = $input->getArgument('busca');
if ($busca !== null) {
$query->where(['like', 'nome', $busca]);
}
/** @var list<array{codigo: string, nome: string, populacao: int|string}> $paises */
$paises = $query->all();
if ($paises === []) {
$io->warning('Nenhum país encontrado.');
return ExitCode::OK;
}
$linhas = [];
foreach ($paises as $pais) {
$linhas[] = [
$pais['codigo'],
$pais['nome'],
number_format((int) $pais['populacao'], 0, ',', '.'),
];
}
$io->table(['Código', 'Nome', 'População'], $linhas);
return ExitCode::OK;
}
}O que cada parte faz:
- O atributo
#[AsCommand]dá o nome e a descrição que aparecem no./yii list. - As dependências chegam pelo construtor, como numa action. Quem cria o comando é o container, então ele recebe a
ConnectionInterfacejá configurada. Lembre de chamarparent::__construct(). - O
configure()declara argumentos e opções. A opção--limitetem o atalho-le o valor padrão'5'. Valores vindos do terminal são sempre string, por isso o(int). - O
execute()faz o trabalho e devolve o código de saída. OSymfonyStyledá métodos prontos para tabela, aviso e erro. - O comentário
@varantes do$query->all()diz ao Psalm o formato das linhas. Sem ele, a análise estática reclama, como mostra o capítulo de Qualidade de código.
O template guarda os comandos em src/Console/. Aqui o comando fica em src/Topicos/Console/ só para separar os exemplos do guia. O lugar não importa para o Yii: o que liga o nome à classe é o registro, logo abaixo.
Registrando em config/console
Os comandos ficam listados em config/console/commands.php. O arquivo é um mapa de nome para classe:
<?php
declare(strict_types=1);
use App\Console;
use App\Topicos;
return [
'hello' => Console\HelloCommand::class,
'pais:listar|paises' => Topicos\Console\ListarPaisesCommand::class,
'email:boas-vindas' => Topicos\Console\EnviarBoasVindasCommand::class,
];O config/console/params.php do template lê esse arquivo e o entrega ao yiisoft/yii-console no parâmetro commands. Não é preciso rodar nada depois de editar: o arquivo é lido a cada execução.
Na chave, o | separa nomes alternativos. Com 'pais:listar|paises', o comando também atende por ./yii paises. Se a chave começa com |, o comando fica escondido da listagem. O próprio yii-console registra um comando assim, o |yii.
O carregamento é preguiçoso. Para montar a lista, o CommandLoader do yii-console lê só o atributo #[AsCommand]. A classe só é criada pelo container quando o comando é executado, então um comando que depende do banco não abre conexão quando você roda outro.
O segundo comando da lista, o email:boas-vindas, aparece no capítulo Envio de e-mails.
Executando
./yii list paisAvailable commands for the "pais" namespace:
pais:listar [paises] Lista os países cadastradosSem argumentos, o comando mostra os cinco primeiros países em ordem alfabética:
./yii pais:listar -------- ----------- ---------------
Código Nome População
-------- ----------- ---------------
DE Alemanha 83.000.000
AU Austrália 26.000.000
BR Brasil 212.000.000
CA Canadá 40.000.000
CN China 1.410.000.000
-------- ----------- ---------------Com uma busca e a opção curta:
./yii paises bra -l 3 -------- -------- -------------
Código Nome População
-------- -------- -------------
BR Brasil 212.000.000
-------- -------- -------------A condição ['like', 'nome', $busca] do query builder já envolve o valor com %, então bra encontra "Brasil". O ./yii pais:listar --help mostra a descrição, o argumento e as opções declaradas no configure().
No Windows, troque ./yii por yii.bat.
Códigos de saída
O número devolvido pelo execute() vira o código de saída do processo. É ele que o cron, o CI ou um script shell usam para saber se deu certo. A classe Yiisoft\Yii\Console\ExitCode traz constantes no padrão sysexits do BSD: OK (0), UNSPECIFIED_ERROR (1), USAGE (64), DATAERR (65), NOPERM (77) e outras.
./yii pais:listar --limite=0
echo $? [ERROR] O limite precisa ser um número maior que zero.
64Uma exceção não tratada dentro do comando também encerra com código diferente de zero. O yii-console registra um listener que escreve o erro no log da aplicação.
Testando o comando
Há dois caminhos. O primeiro é rodar o ./yii de verdade e conferir a saída, com a suíte Console do Codeception. Trecho de tests/Console/ListarPaisesCest.php:
public function filtraPeloNome(ConsoleTester $I): void
{
$I->runApp('pais:listar bra');
$I->seeResultCodeIs(0);
$I->seeInShellOutput('Brasil');
$I->dontSeeInShellOutput('Canadá');
}O segundo é criar o comando à mão, com um banco SQLite em memória, e executá-lo com o CommandTester do Symfony. Esse caminho aparece em Testes unitários.
Comparando com o Yii2
- No Yii2, um comando era um controller de console, com um método
actionXxx()por subcomando, e rodava como./yii pais/listar. No Yii3, cada comando é uma classe do Symfony Console, e o nome costuma usar:(migrate:up). - Os argumentos deixam de ser parâmetros do método da action e passam a ser declarados no
configure(). - O registro é explícito, em
config/console/commands.php, em vez docontrollerMapou da descoberta pelo namespace. - O
ExitCodecontinua existindo, agora emYiisoft\Yii\Console\ExitCode.