Minicli – Aplicativos linha de comando em PHP
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
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.
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()));
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++