Minicli

Se você já usou o Pest ou o artisan do Laravel você já usou um aplicativo em linha de comando em PHP. Embora não seja a primeira opção quando pensamos em aplicativos de console, é possível criar facilmente aplicativos com PHP, ainda mais utilizando algum framework como o Minicli. Ele é um framework sem dependências externas que fornece uma boa estrutura para organizar os comandos e helpers para receber as entradas da linha de comando e realizar a saídas formatada de dados no console.

Para começar vamos fazer um comando simples, e iniciamos fazendo a instalação via composer:

composer require minicli/minicli

Então criamos nosso arquivo que será executado, vamos chamá-lo de minicli.

#!/usr/bin/env php
<?php

use Minicli\App;
use Minicli\Command\CommandCall;

if(php_sapi_name() !== 'cli'){
    exit;
}
require_once __DIR__.'/../vendor/autoload.php';

$app = new App();

$app->registerCommand('test', function(CommandCall $input) use($app){

    $param1 = $input->getParam('param1');
    $param2 = $input->getParam('param2');

    $umaFlag = $input->hasFlag('--umaFlag') ? 'Sim': 'Não';
    $outraFlag = $input->hasFlag('--outraFlag') ? 'Sim': 'Não';

    $app->getPrinter()->info("Parâmetro param1: " . $param1);
    $app->getPrinter()->info("Parâmetro param2: " . $param2);

    $app->getPrinter()->info("Flag umaFlag: " . $umaFlag);
    $app->getPrinter()->info("Flag outraFlag : " . $outraFlag );
});

$app->runCommand($argv);

Na linha 1 adicionamos o shebang para sistemas linux saberem qual programa deve executar o script. Na linha 7 testamos se o script não está sendo executado pelo CLI, se não o encerramos.

Na linha 12 criamos um objeto App e na linha 14 adicionamos um comando a ele através do método registerCommand, que recebe como argumento o nome do comando propriamente dito e uma função que será executada pelo comando. Esta função recebe um objeto CommandCall de parâmetro, que é um objeto que permite o acesso às entradas da linha de comando de uma forma bastante simples. Nas linha 16 e 17 pegamos os valores de parâmetro param1 e param2 passados para o comando. Nas linha 19 e 20 testamos se as flags umaFlag e outraFlag foram passadas para o comando.

Para executar a saída de dados utilizamos o objeto OutputHandler obtido através do método getPrinter do App, que nos fornece métodos de saída avançada com estilos definidos por temas e até tabelas. Neste exemplo inicial simplesmente utilizamos a saída de informação com o método info().

Então podemos dar permissão de execução, executar o comando e visualizar a saída.

> chmod +x minicli
> ./minicli test param1=valor1 param2=valor2 --umaFlag --outraFlag
Saída do comando no Oh My Zsh

Usando a estrutura do Minicli

O esquema acima atende bem se nosso aplicativo tiver apenas um comando, mas para projetos mais complexos é necessário um pouco mais de estrutura. Podemos criar um projeto com um template já fornecido pelo Minicli.

composer create-project --prefer-dist minicli/application meu-app

Ao término da instalação teremos a seguinte estrutura

.
├── app
│   └── Command
│       └── Demo
│           ├── DefaultController.php
│           ├── TableController.php
│           └── TestController.php
├── composer.json
├── composer.lock
├── LICENSE
├── minicli
├── phpunit.xml
├── README.md
├── tests
└── vendor

Já temos o arquivo executável minicli:

$app = new App([
    'app_path' => [
        __DIR__ . '/app/Command',
        '@minicli/command-help'
    ],
    'debug' => true
]);

try {
    $app->runCommand($argv);
} catch (CommandNotFoundException $notFoundException) {
    $app->getPrinter()->error("Command Not Found.");
    return 1;
} catch (Exception $exception) {
    if ($app->config->debug) {
        $app->getPrinter()->error("An error occurred:");
        $app->getPrinter()->error($exception->getMessage());
    }
    return 1;
}

Note que é passado um array de opções para a aplicação, nele temos a opção app_path que é o caminho onde as classes de controller, que serão os nossos comandos, estarão localizadas. Também temos o tratamento das exceções de comandos não encontrados e de exceções genéricas para mostrar mensagens de error mais “apresentáveis”.

Para criar um comando nesta estrutura criamos uma classe que deve estender a classe abstrata CommandController e implementar o método public function handle(): void.

O namespace que utilizamos para classe de controller será o nome do comando, se olharmos o exemplo que já vem com o template do projeto temos a pasta Demo dentro da app/Command , então temos o comando ./minicli demo disponível que irá executar o controller App\Command\Demo\DefaultController, se executarmos ./minicli demo test irá executar o controller App\Command\Demo\TestController, como um subcomando de demo. Simplificando cada pasta dentro de Command é um comando, se nenhum subcomando for chamado ele executa o DefaultController, se for passado ele executa o <Subcomando>Controller.

Veja o exemplo do TestController

namespace App\Command\Demo;

use Minicli\Command\CommandController;

class TestController extends CommandController
{
    public function handle(): void
    {
        $name = $this->hasParam('user') ? $this->getParam('user') : 'World';
        $this->getPrinter()->display(sprintf("Hello, %s!", $name));

        print_r($this->getParams());
    }
}

Para pegarmos os valores passados pela linha de comando podemos utilizar diversos métodos dentro do controller, eles simplesmente repassam a chamada para o objeto CommandCall que está disponível como atributo em $this->input.

Veja os métodos de entrada disponíveis no controller:

  • getParam(string $param): retorna o valor do parâmetro que o nome for passado para o método. Se não foi passado um parâmetro com este nome retorna null. Os parâmetros são passados na linha de comando no formato <nome-parametro>=<valor-parametro>.
  • hasParam(string $param) : retorna true se o parâmetro com o nome passado existe e false se não.
  • hasFlag(string $flag): retorna true se a flag com o nome passado existe e false se não. As flags são passadas via linha de comando prefixadas com “–” . Ex.: --import
  • getParams(): retorna todos os parâmetros passados na linha de comando em um array associativo onde a chave é o nome do parâmetro e o valor é (ohhh) o valor.
  • getArgs(): retorna um array com os argumentos do comando. Por exemplo, para o comando ./minicli demo test retorna ["./minicli","demo","teste"]

Saída de Dados

Para realizar a saída de dados no console, como já vimos no primeiro exemplo, utilizamos o objeto OutputHandler que pode ser obtido através do método getPrinter() do controller, ele fornece vários métodos de saída para o console com diversos estilos

  • display($message, $alt = false): Exibe uma mensagem com uma linha em branco antes e depois com o estilo default. Se alt for true usa o estilo invert, que deve inverter a cor do fundo com o da letra.
  • info($message, $alt = false): Exibe uma mensagem com uma linha em branco antes e depois com o estilo info. Se alt for true usa o estilo info_alt, que deve inverter a cor do fundo com o da letra.
  • success($message, $alt = false): Exibe uma mensagem com uma linha em branco antes e depois com o estilo success. Se alt for true usa o estilo success_alt, que deve inverter a cor do fundo com o da letra.
  • error($message, $alt = false): Exibe uma mensagem com uma linha em branco antes e depois com o estilo error. Se alt for true usa o estilo error_alt, que deve inverter a cor do fundo com o da letra.
  • out($message, $style = null): Exibe uma mensagem e de forma opcional aplica um estilo. Os estilos disponíveis são: bold, italic, underline, invert. Não inclui linhas em branco.
  • newline(): Exibe uma linha em branco.
  • rawOutput($message): Exibe uma mensagem sem formatação ou linhas em branco.
  • printTable(array $table): Exibe um array em formato de tabela.
Exemplos de saídas no console Oh My Zsh

Também podemos formatar uma tabela utilizando o helper TableHelper, chamando o método addHeader para definir o cabeçalho da tabela e addRow para adicionar linhas.

$table = new TableHelper();
$table->addHeader(['Header 1', 'Header 2', 'Header 3']);

for($i = 1; $i <= 10; $i++) {
    $table->addRow(["Linha $i", "Linha $i Colunha 2", "Linha $i Colunha 3"]);
}
$this->getPrinter()->out($table->getFormattedTable(new ColorOutputFilter()));
Exemplo de tabela no Oh My Zsh

Solicitando valores do console

Para solicitar valores do console utilizamos a classe Input através do método read() que encapsula a função do PHP readline. O Input aceita um de prompt no construtor para a ser exibido antes de realizar a leitura dos dados. A classe também fornece um método para recuperar todos valores lidos pelo objeto através do método getInputHistory().

Veja um exemplo de como realizar a leitura do console até o usuário digitar um valor específico, no caso :q

$input = new Input('> ');
do{
    $userInput = $input->read();
}while(trim($userInput) !== ':q');

$text = join("\n", array_slice($input->getInputHistory(), 0, -1) );
echo $text;

Temas de Cores

O minicli trabalha com temas de cores para a saída da linha de comando. Ele conta com três temas: Default, Unicorn e Dalton. Para alterar o tema alteramos o arquivo minicli passando como um dos valores do construtor do objeto App a entrada theme.

$app = new App([
    'app_path' => [
        __DIR__ . '/app/Command',
        '@minicli/command-help'
    ],
    'debug' => true,
    'theme' => '\Unicorn'
]);

Também podemos criar nosso próprio esquema de cores estendendo a classe DefaultTheme e implementando o método getThemeColors().

namespace App\Theme;

use Minicli\Output\Theme\DefaultTheme;
use Minicli\Output\CLIColors;

class MyTheme extends DefaultTheme
{
    public function getThemeColors(): array
    {
        return [
            'default'     => [ CLIColors::$FG_YELLOW ],
            'alt'         => [ CLIColors::$FG_WHITE, CLIColors::$BG_YELLOW ],
            'info'        => [ CLIColors::$FG_WHITE],
            'info_alt'    => [ CLIColors::$FG_WHITE, CLIColors::$BG_YELLOW ]
        ];
    }
}

E para adicioná-lo insira no array de configurações do App o nome canonico sem o Theme, exe.: 'theme' => 'App\Theme\My'.

Para exemplificar veja um exemplo bem simples de um redimensionador de imagens em linha de comando.

Bom era isso, o minicli é um ótimo framework para criar aplicações em linha de comando, sendo sua simplicidade um ponto muito forte, excelente para criar pequenas automações do dia a dia. Para mais informações veja a documentação que ele tem mais a oferecer. T++