Hoje venho falar sobre um elemento muito bacana do Zend Framework, o Console. Com ele você pode criar poderosas aplicações de linha de comando de forma organizada e tirando proveito do melhor que o ZF pode lhe oferecer. Iniciaremos um projeto com o Zend Skeleton Application para que você veja como é simples começar com o Console.
$ composer create-project -sdev zendframework/skeleton-application path/to/installAlgumas perguntas serão feitas, pode seguir com as respostas da mesma forma que apresento aqui. Do you want a minimal install (no optional packages)? Y/n - responda n. Would you like to install the developer toolbar? y/N - pode ser N, somente utilizaremos a linha de comando. Would you like to install caching support? y/N - pode responder N. Would you like to install database support (installs zend-db)? y/N - pode ser N. Would you like to install forms support? y/N - indiferente, eu respondi N. Would you like to install JSON de/serialization support? y/N - mais uma vez N. Would you like to install logging support? y/N - não precisaremos para este exemplo, N. Would you like to install MVC-based console support? (We recommend migrating to zf-console, symfony/console, or Aura.CLI) y/N - responda y. Would you like to install the official MVC plugins, including PRG support, identity, and flash messages? y/N - pode ser N também. Would you like to use the PSR-7 middleware dispatcher? y/N - de momento responda N. Would you like to install sessions support? y/N - N de novo. Would you like to install MVC testing support? y/N - N, não serão abordados testes aqui. Would you like to install the zend-di integration for zend-servicemanager? y/N - pode responder y. Please select which config file you wish to inject 'Zend\Mvc\Console' into: [0] Do not inject [1] config/modules.config.php [2] config/development.config.php.dist Make your selection (default is 0): - responda 1. Remember this option for other packages of the same type? (y/N) - responda y para que outros módulos semelhantes sejam injetados no mesmo arquivo. Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]? - responda Y e pronto! O Zend já está instalado e pronto para uso.
'view_manager' => [/* ... */],
'console' => [
'router' => [
'routes' => [
'hello-world' => [
'options' => [
'route' => 'hello-world',
'defaults' => [
'controller' => HelloWorldConsole::class,
'action' => 'index'
]
]
]
]
]
]
Note que temos em controller, uma classe que 1) ainda não foi mapeada nos controllers e, 2) sequer existe. Vamos resolver isso.
Nas configurações de controllers, ainda no arquivomodule/Application/config/module.config.php, mapeie o HelloWorldConsole.
'controllers' => [
'factories' => [
Controller\IndexController::class => InvokableFactory::class,
HelloWorldConsole::class => InvokableFactory::class
]
],
E nos imports, isso:
use Application\Console\HelloWorldConsole;
<?php
namespace Application\Console;
use Zend\Mvc\Console\Controller\AbstractConsoleController;
/**
* Class HelloWorldConsole
* @package Application\Console
*/
class HelloWorldConsole extends AbstractConsoleController
{
public function indexAction()
{
return 'hello world!' . PHP_EOL;
}
}
Simples não?! Se você rodar pelo terminal o comando php public/index.php hello-world, verá o resultado do que acabamos de criar.
Isso porque a rota foi definida desta forma e já é seguro garantir que a mesma funcione somente via linha de comando. No entanto podemos deixar as coisas mais seguras ainda.
<?php
namespace Application\Console;
// importando o Request de Console e o apelidando de ConsoleRequest
use Zend\Console\Request as ConsoleRequest;
use Zend\Mvc\Console\Controller\AbstractConsoleController;
/**
* Class HelloWorldConsole
* @package Application\Console
*/
class HelloWorldConsole extends AbstractConsoleController
{
public function indexAction()
{
// obtendo o objeto da requisição
$request = $this->getRequest();
// e garantindo que o acesso será somente via linha de comando
if (! $request instanceof ConsoleRequest) {
throw new \RuntimeException('Rota válida somente para linha de comando');
}
return 'hello world!' . PHP_EOL;
}
}
Isso não modifica em nada o que já temos funcionando, mas impede que por engano este controller seja utilizado em rotas web.
'console' => [
'router' => [
'routes' => [
'hello-world' => [
'options' => [
'route' => 'hello-world <nome>',
'defaults' => [
'controller' => HelloWorldConsole::class,
'action' => 'index'
]
]
]
]
]
]
'console' => [
'router' => [
'routes' => [
'hello-world' => [
'options' => [
'route' => 'hello-world <nome> [<sobrenome>]',
'defaults' => [
'controller' => HelloWorldConsole::class,
'action' => 'index'
]
]
]
]
]
]
'console' => [
'router' => [
'routes' => [
'hello-world' => [
'options' => [
'route' => 'hello-world <nome> [<sobrenome>] [--show_date]',
'defaults' => [
'controller' => HelloWorldConsole::class,
'action' => 'index'
]
]
]
]
]
]
public function indexAction()
{
// obtendo o objeto da requisição
$request = $this->getRequest();
// e garantindo que o acesso será somente via linha de comando
if (!$request instanceof ConsoleRequest) {
throw new \RuntimeException('Rota válida somente para linha de comando');
}
$nome = $request->getParam('nome');
$sobrenome = $request->getParam('sobrenome');
$showDate = (bool)$request->getParam('show_date', false);
$outputDate = '';
if ($showDate) {
$outputDate = ' ' . date('Y-m-d H:i:s');
}
return 'hello ' . $nome . ' ' . $sobrenome . $outputDate . PHP_EOL;
}
Shortcutmkdir bin touch bin/app chmod +x bin/appNo arquivo app, adicione o seguinte conteúdo:
#!/usr/bin/env php <?php include __DIR__ . '/../public/index.php';A partir de agora, ao invés de rodar o comando php public/index.php, basta rodar ./bin/app. Deixa muito mais intuitivo para uma aplicação via linha de comando.
Note que não existe nada, agora pense em uma configuração de rotas como esta da imagem a seguir. É uma pequena parte das dezenas de rotas de uma aplicação real que desenvolvi recentemente.
Pra nossa sorte que o ZF pensou em tudo e nos disponibilizou um meio de indicar a forma de utilização de rotas do Console, a ConsoleUsageProviderInterface. No arquivo module/Application/src/Module.php implemente a interface.
<?php
namespace Application;
use Zend\Console\Adapter\AdapterInterface;
use Zend\ModuleManager\Feature\ConsoleUsageProviderInterface;
class Module implements ConsoleUsageProviderInterface
{
const VERSION = '3.0.3-dev';
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
public function getConsoleUsage(AdapterInterface $console)
{
return [
'hello-world <nome> [<sobrenome>] [--show_date]' => 'Apresenta uma saudacao ao usuario'
];
}
}
Agora se rodar novamente o comando ./bin/app, temos novidades.
Melhorou né? Mas sabia que podemos ir além?
'console' => [
'router' => [
'routes' => [
'hello-world' => [
'options' => [
'route' => 'hello-world <nome> [<sobrenome>] [--show_date]',
'defaults' => [
'controller' => HelloWorldConsole::class,
'action' => 'index'
]
],
'usage_info' => 'Apresenta uma saudacao ao usuario. Obrigatorio: <nome>. Opcionais: [<sobrenome>] e [--show_date]'
]
]
]
]
E pra que nossa aplicação reconheça automaticamente a rota e os detalhes de uso, criei uma lógica no Module.php.
public function getConsoleUsage(AdapterInterface $console)
{
// carrega as configurações do módulo
$config = $this->getConfig();
// obtém as rotas do console
$consoleRoutes = $config['console']['router']['routes'];
$usages = [];
foreach ($consoleRoutes as $route) {
// adiciona a rota juntamente com sua forma de uso
$usages[$route['options']['route']] = $route['usage_info'];
}
// retorna todas as configurações adicionadas dinamicamente
return $usages;
}
E o resultado é esse:
Agora podemos adicionar mais rotas para testar o carregamento dinâmico.
'console' => [
'router' => [
'routes' => [
'hello-world' => [
'options' => [
'route' => 'hello-world <nome> [<sobrenome>] [--show_date]',
'defaults' => [
'controller' => HelloWorldConsole::class,
'action' => 'index'
]
],
'usage_info' => 'Apresenta uma saudacao ao usuario. Obrigatorio: <nome>. Opcionais: [<sobrenome>] e [--show_date]'
],
'status' => [
'options' => [
'route' => 'status',
'defaults' => [
'controller' => HelloWorldConsole::class,
'action' => 'status'
]
],
'usage_info' => 'Apresenta um status atualizado da aplicacao'
]
]
]
]
Pode parecer algo dispensável, mas num cenário real, pode ser extremamente útil, veja só.