Escreva um aplicativo de console usando Symfony e Pimple
Escrever comandos do console para Symfony (full stack framework) é fácil e divertido.
Neste artigo, vou mostrar como configurar um aplicativo de linha de comando usando o componente Symfony e o Pimple.
Escrever comandos do console para Symfony (full stack framework) é fácil e divertido. Isso ajuda muito quando você precisa lidar com tarefas colaterais específicas que são necessárias, de uma forma ou de outra, para fazer o seu site funcionar (processamento e fragmentação de dados, solicitações assíncronas, criação de relatórios etc.).
Enfim, eu descobri que escrever aplicativos de linha de comando usando apenas o componente Symfony/Console é muito mais fácil e divertido, e um monte de aplicativos de linha de comando famosos os usam (Composer e Laravel/Artisan, apenas para citar alguns). Além disso, usando Symfony eu me tornei um grande fã dos design patterns Dependency Injection e Inversion of Control (IoC), e como minhas dependências começaram a crescer, eu quis colocar algum tipo de container em meus aplicativos de linha de comando. Eu decidi usar Pimple: um container de injeção de dependência muito simples escrito por Fabien Potencier, o notório líder por trás do framework Symfony e do Sensio.
Vamos começar
Vou demonstrar a minha abordagem por meio da criação de uma aplicação de linha de comando simples “hello $name“, que será capaz de contar quantas vezes você cumprimentou alguém. Você pode encontrar o código todo no repositório GitHub.
Então, vamos ser capaz de executar
app/console greet Alice
e ele irá imprimir
Hello Alice (First time!)
Sim, intencionalmente simples! 😉
Vamos começar criando nosso arquivo composer.json. Vamos precisar do console Symfony e dos pacotes Pimple. Nós também incluímos o componente Symfony Yaml, já que vamos armazenar os dados em um arquivo yaml (obviamente nós poderíamos ter usado json, mas acredito que yaml é mais legal :P).
{
"name": "lmammino/symfony-console-pimple",
"description": "A sample Symfony Console app using Pimple",
"require": {
"symfony/console": "dev-master",
"pimple/pimple": "dev-master",
"symfony/yaml": "dev-master"
},
"license": "MIT",
"authors": [
{
"name": "Luciano Mammino",
"email": "lmammino@oryzone.com"
}
],
"autoload": {
"psr-4": {
"LMammino\\ConsoleApp\\": "src/"
}
}
}
Sim, vamos executar composer update para baixar todas as bibliotecas.
Estrutura de pastas
Vamos estruturar nosso código. Queremos separar o aplicativo e o código de configuração do código-fonte principal. Então vamos acabar com a seguinte estrutura de pastas:
- app
- src
- vendors
A pasta app irá conter nosso arquivo console executável, um arquivo de inicialização e uma pasta de configuração. Nós vamos entrar em detalhes sobre isso daqui a pouco.
O serviço Greeter
Vamos definir o nosso core service escrevendo uma classe Greeter. Essa classe define a lógica de negócios do nosso aplicativo de saudação.
<?php
namespace LMammino\ConsoleApp;
use Symfony\Component\Yaml\Yaml;
class Greeter
{
/**
* @var string $file
*/
protected $file;
/**
* @var array $greetings
*/
protected $greetings;
/**
* Constructor
*
* @param string $file
*/
public function __construct($file)
{
$this->file = $file;
if (file_exists($file)) {
$this->greetings = Yaml::parse(file_get_contents($file));
} else {
$this->greetings = array();
}
}
/**
* Destructor
*/
public function __destruct()
{
file_put_contents($this->file, Yaml::dump($this->greetings));
}
/**
* Builds the greeting for someone (you can yell on it if you want!)
*
* @param string $name
* @param bool $yell wanna yell?
* @return string
*/
public function greet($name, $yell = false)
{
$output = sprintf('Hello %s', $name);
if ($yell) {
$output = strtoupper($output);
}
$name = strtolower($name);
if (!isset($this->greetings[$name])) {
$this->greetings[$name] = 1;
} else {
$this->greetings[$name]++;
}
return $output;
}
/**
* Will tell you how many times you greet someone
*
* @param string $name
* @return int
*/
public function countGreetings($name)
{
$name = strtolower($name);
if (!isset($this->greetings[$name])) {
return 0;
}
return $this->greetings[$name];
}
}
A classe é muito simples. Os principais métodos são greet e countGreetings, que permitem que você construa a string greet para alguém e conte quantas vezes você o cumprimentou.
Perceba que essa classe precisa saber na construção qual arquivo deve usado para ler e armazenar a contagem das saudações. Isso será algo que iremos configurar por meio do Pimple como um parâmetro no container.
O GreetCommand
Agora que temos um serviço com a principal lógica de negócios, vamos escrever um comando Symfony para executá-lo:
<?php
namespace LMammino\ConsoleApp\Command;
use LMammino\ConsoleApp\Greeter;
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;
class GreetCommand extends Command
{
/**
* @var \LMammino\ConsoleApp\Greeter $greeter
*/
protected $greeter;
/**
* Constructor
*
* @param Greeter $greeter
*/
public function __construct(Greeter $greeter)
{
parent::__construct();
$this->greeter = $greeter;
}
/**
* {@inheritDoc}
*/
protected function configure()
{
$this->setName('greet')
->setDescription('Greet someone')
->addArgument('name', InputArgument::OPTIONAL, 'The name of the one you want to greet', 'World')
->addOption('yell', 'Y', InputOption::VALUE_NONE, 'If set will scream out the greeting. Use with caution!');
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$name = $input->getArgument('name');
$yell = $input->getOption('yell');
$output->writeln($this->greeter->greet($name, $yell));
if (1 === ($count = $this->greeter->countGreetings($name))) {
$output->writeln('(First time!)');
} else {
$output->writeln(sprintf('(%d times)', $count));
}
}
}
O comando é totalmente autoexplicativo! Ele apenas define o comando greet oferecendo um argumento name e uma opção de yell (ambos opcionais). O ponto aqui é que o nosso comando tem uma dependência na classe Greeter que escrevemos antes. Então, precisamos passá-la na construção (ou precisamos configurar o nosso container Pimple para fazê-lo).
Senhoras e senhores, o container Pimple!
Finalmente chegou a hora de escrever nosso container Pimple. Antes de entrar no código, vamos recapitular um pouco as coisas.
Nós temos um parâmetro (o nome do arquivo que irá guardar a contagem) e dois serviços (o serviço Greeter e o GreetCommand).
Vamos criar um arquivo app/config/container.php↳PHP44 conteúdosPadronizando seu código com PHP CS FixerDev (Back & Front) · mai 2019Docker de imagem. Para rodar projetos PHP em menos de 30 segundosDev (Back & Front) · jan 2026PHP 7.3: conheça as novidades desta versãoDev (Back & Front) · abr 2019Ver tudo em Dev (Back & Front) → para definir nossos parâmetros e serviços com o Pimple:
<?php
$c = new Pimple();
$c['parameters'] = array(
'greetings.file' => 'greetings.yaml'
);
$c['greeter'] = function($c) {
return new \LMammino\ConsoleApp\Greeter($c['parameters']['greetings.file']);
};
$c['command.greet'] = function($c) {
return new \LMammino\ConsoleApp\Command\GreetCommand($c['greeter']);
};
$c['commands'] = function($c) {
return array(
$c['command.greet']
);
};
$c['application'] = function($c) {
$application = new \Symfony\Component\Console\Application();
$application->addCommands($c['commands']);
return $application;
};
return $c;
Se você não estava familiarizado com o Pimple, deve ter notado a sintaxe simples desse container. Nós só precisamos criar uma instância Pimple e ele age como um array. Nesse “array”, nós colocamos parâmetros como dados simples (valores escalares ou arrays) e definições de serviço como funções que retornam serviços instanciados. Vamos verificar as nossas definições uma a uma:
- $c[‘parameters’] contém os parâmetros da aplicação (em um aplicativo mais complexo, com um monte de parâmetros, você pode carregar os valores a partir de um arquivo de configuração externo).
- $c[‘greeter’] define a construção do nosso serviço Greeter.
- $c[‘command.greet’] define a construção de nosso comando greet.
- $c[‘commands’] é uma definição auxiliar que retorna um array com todos os comandos que desejamos adicionar à nossa aplicação.
- $c[‘application’] define a criação do nosso aplicativo de linha de comando.
Ok, estamos quase terminando. Apenas precisamos escrever nosso arquivo bootstrap e nosso arquivo de console executável.
O arquivo bootstrap
O arquivo app/bootstrap.php é usado para carregar a classe composer autoloader e nosso container:
<?php set_time_limit(0); require __DIR__ . '/../vendor/autoload.php'; $container = require(__DIR__ . '/config/container.php');
set_time_limit(0) assegura que o nosso script não será finalizado depois de uma certa quantidade de segundos (se o seu php.ini requerer isso). É quase inútil, nesse caso em particular (o nosso comando será executado em poucos milissegundos), mas adicioná-lo a aplicações de linha de comando do PHP é uma boa prática (especialmente quando você tem que lidar com tarefas de longa duração).
O arquivo de console executável
O último passo necessário para tornar a aplicação executável é escrever o arquivo app/console. Ele é um arquivo PHP que pode ser executado a partir da linha de comando (você precisa de fazer chmod+x nele).
#!/usr/bin/env php <?php require __DIR__ . '/bootstrap.php'; $application = $container['application']; $application->run();
Para usar um container, é só preciso carregar o nosso serviço de “aplicação” e chamar run() nele.
Perceba que a primeira linha “shebang” (#!/usr/bin/env php) nos permite executar esse arquivo chamando app/console (de modo que você pode evitar a chamada para o interpretador php explicitamente).
Conclusões
Este aplicativo é muito simples e será fácil de construir, mesmo sem a adoção de um container. De qualquer forma, eu acho que esta abordagem garante uma boa organização para o seu código e se tornará realmente útil quando seu aplicativo de linha de comando começar a crescer em termos de complexidade.
Recentemente, tive de construir um aplicativo de linha de comando que usa Doctrine e JMS/Serializer (além de várias outras dependências). Posso dizer que a adoção de um container como o Pimple me ajudou muito a manter as coisas organizadas e serviços desacoplados.
Apenas para fazer um resumo final, eu acho que esta abordagem garante vários benefícios:
- Escrever comandos “container agnósticos” (que não conhecem o container, mas têm apenas as dependências necessárias injetadas).
- Anexar novos comandos na configuração: você só precisa adicioná-los no array $container[‘commands’].
- É de grande ajuda escrever comandos pequenos (sim, eu acho que comandos agem como controladores, e eles devem ser “magros” também), porque você tem uma maneira simples de declarar serviços e suas dependências, e você é capaz de injetar apenas os necessários a cada comando.
- Permitir que você tenha parâmetros e configurações (útil quando você tem que estabilizar uma conexão com um banco de dados↳Banco de dados134 conteúdosSQL ou NoSQL: eis a questão!!Data · mar 2020Banco de dados: como organizar e dar segurança para milhões de dados de loteriasData · mai 20215 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → ou utilizar recursos externos que precisam de configuração, como uma API externa).
Isso deve ser tudo. Fique à vontade para comentar ou contribuir com o repositório do aplicativo de exemplo, se você sentir que esta abordagem pode ser melhorada.
ATUALIZAÇÃO:
Javier Egiluz, grande evangelista do Symfony, citou que um dos seus aplicativos de linha de comando, easybook, usa o componente Symfony Console em conjunto com Pimple. Então, se você quiser dar uma olhada em um caso mais completo e realista (e complexo :P), eu recomendo o código-fonte do easybook.
Tenha um ótimo dia!
***
Luciano Mammino faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://loige.co/write-a-console-application-using-symfony-and-pimple/






