Dev (Back & Front)ARTIGO

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.

symfony-1

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.phpPHP44 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:

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/

 

 

É um engenheiro de software nascido em 1987, mesmo ano em que a Nintendo lançou “Super Mario Bros” na Europa, que, por acaso, é seu jogo preferido! Ele é apaixonado por código, pela web, smart apps e tudo o que é criativo, como música, arte e design. Como desenvolvedor web, sua experiência tem sido principalmente com PHP e Symfony 2, mesmo tendo se apaixonado recentemente por JavaScript, NodeJS e Docker.

Ver perfil