Back-End

4 mar, 2016

Escreva um aplicativo de console usando Symfony e Pimple

Publicidade

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.php 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 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/