PHP fora da Web
Utilizando nossa linguagem preferida ♥ para criar scripts CLI e robôs
Quem sou eu?
Roteiro
-
Scripts CLI
Lidando com argumentos, streams e roteamento de comandos
-
Robôs
Gerenciando início e término de robôs, usando
pcntl
oupthreads
-
Reutilização
Criando códigos que rodem em mais de um ambiente
É a melhor solução para o meu problema?
Primeiro, considere se você está usando as melhores ferramentas para cada tipo de trabalho
Scripts CLI
Criando utilitários para a linha de comando
Argumentos
As variáveis $argc
e $argv
guardam informações sobre os argumentos do script
$argv
é um array com os argumentos passados, sendo que o índice 0 contém o nome do script invocado
if ($argc == 1) {
echo "Uso: php {$argv[0]} <comando>" . PHP_EOL;
exit(2);
}
switch ($argv[1]) {
case 'run':
// ...
break;
default:
// ...
break;
}
getopt()
array getopt( string $options [, array $longopts [, int &$optind ]] )
As opções podem ser:
- Caracteres individuais: não aceitam valores
- Caracteres seguidos por um dois-pontos: valor obrigatório
- Caracteres seguidos por dois dois-pontos: valor opcional
$options = getopt(
'ab:c::',
['verbose', 'user:', 'password::']
);
// php getopt.php -a -b valor -c1 --verbose \
// --user root --password
array(6) {
["a"] => bool(false)
["b"] => string(5) "valor"
["c"] => string(1) "1"
["verbose"] => bool(false)
["user"] => string(4) "root"
["password"] => bool(false)
}
I/O Streams
Uso das funções fopen
, fgets
, fputs
, stream_get_line
e diversas outras
Disponibilização das constantes STDIN
, STDOUT
e STDERR
// Leitura do STDIN
echo "Qual seu nome? ";
$line = trim(fgets(STDIN));
echo "Bem-vindo, {$line}." . PHP_EOL;
// Saída para STDERR
fputs(STDERR, 'Erro no sistema');
Streams
Uso de funções como stream_context_create
, stream_copy_to_stream
, stream_filter_append
, entre outras
Referências
// Exemplo simples do poder das streams
stream_filter_append(STDERR, 'string.toupper');
stream_copy_to_stream(STDIN, STDERR);
Roteamento de comandos
Organize seu script para que ele seja modular
Crie estrutura de controllers para facilitar a manutenção
if ($argc != 3) {
echo "Uso: {$argv[0]} " . PHP_EOL;
exit(2);
}
include 'vendor/autoload.php';
array_shift($argv); // nome do script
$module = array_shift($argv); // ou $options['module']
$class = "MyCli\Controllers\\{$module}";
if (!class_exists($class)) {
throw new DomainException("Módulo {$module} não encontrado");
}
$command = array_shift($argv); // ou $options['command']
if (!method_exists($class, $command)) {
throw new DomainException("Comando {$command} não encontrado");
}
(new $class())->{$command}($argv); // ou $options
Bibliotecas
// config/autoload/*.php
return [
'console' => [
'router' => [
'routes' => [
'user-reset-password' => [
'options' => [
'route' => 'user resetpassword [--verbose|-v] <email>',
'defaults' => [
'controller' => Application\Controller\IndexController::class,
'action' => 'resetpassword'
]
]
]
]
]
]
];
Retirado da documentação oficial
Bibliotecas
use Zend\Mvc\Controller\AbstractActionController;
class IndexController extends AbstractActionController
{
public function resetpasswordAction()
{
$request = $this->getRequest();
if (! $request instanceof \Zend\Console\Request) {
throw new \RuntimeException('Requisição inválida');
}
$email = $request->getParam('email');
/* ... */
if ($request->getParam('verbose') || $request->getParam('v')) {
/* ... */
}
return 'Senha enviada com sucesso';
}
}
Adaptado da documentação oficial
Bibliotecas
// application.php
require __DIR__.'/vendor/autoload.php';
$application = new Symfony\Component\Console\Application();
$application->add(new App\Command\CreateUserCommand());
$application->run();
Adaptado da documentação oficial
Bibliotecas
// src/Command/CreateUserCommand.php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CreateUserCommand extends Command
{
protected function configure()
{
/* ... */
}
protected function execute(InputInterface $input, OutputInterface $output)
{
/* ... */
}
}
Adaptado da documentação oficial
Bibliotecas
protected function configure()
{
$this
// Nome do comando, executado pelo bin/console
->setName('app:create-user')
// Descrição do comando ao executar bin/console list
->setDescription('Cria um novo usuário.')
// Descrição completa ao invocar o --help
->setHelp('Esse comando permite você criar um novo usuário...')
// Argumento obrigatório
->addArgument(
'username',
\Symfony\Component\Console\Input\InputArgument::REQUIRED,
'Nome de usuário'
);
}
Adaptado da documentação oficial
Bibliotecas
protected function execute(InputInterface $input, OutputInterface $output)
{
// Imprime várias linhas (automaticamente adicionando \n)
$output->writeln([
'Criando usuário',
'===============',
'',
]);
// Imprime sem \n
$output->write('Nome de usuário: ');
$output->write($input->getArgument('username'));
}
Adaptado da documentação oficial
Bibliotecas
Outras opções
Robôs
Usando o PHP para criar daemons
Gerenciador de robôs
Para iniciar, terminar e acompanhar a execução
Ou você pode ter daemons "auto-executáveis" - por exemplo, diretamente via cron
interface DaemonManagerInterface
{
// Inicia todos os daemons
public function start();
// Inicia um daemon específico
public function startDaemon($class);
// Para todos os daemons
public function stop();
// Para um daemon específico
public function stopDaemon($class);
// Lista os daemons que devem ser iniciados
protected function getActive();
// Monitora o status de cada daemon
protected function watchStatus();
}
pcntl_fork(); // Faz um fork do processo atual
pcntl_signal_dispatch(); // Invoca os handlers para sinais pendentes
pcntl_signal(); // Instala um handler
pcntl_sigprocmask(); // Bloqueia/desbloqueia sinais
pcntl_sigtimedwait(); // Espera por um sinal, com timeout
pcntl_sigwaitinfo(); // Espera por um sinal
pcntl_wait(); // Aguarda/retorna o status de um filho
pcntl_waitpid(); // Aguarda/retorna o status de um filho
Fluxo simples de execução
Utilizando pcntl_fork()
Fazendo fork do processo atual
public function startDaemon($class)
{
$pid = pcntl_fork();
if ($pid == -1) {
throw new RuntimeException("Houve um erro no fork do robô {$class}");
}
if ($pid) {
// Processo pai
return $pid;
}
// Processo filho (robô)
$daemon = new $class();
$daemon->run();
die();
}
Fluxo simples de execução
Utilizando pcntl_waitpid()
int pcntl_waitpid(
int $pid ,
int &$status
[, int $options = 0
[, array &$rusage ]]
)
protected function watchStatus() {
$count = count($this->pool);
while ($count > 0) {
foreach ($this->pool as $index => $pid) {
// Retorna o PID do filho se ele estiver terminado
if (pcntl_waitpid($pid, $status, WNOHANG)) {
unset($this->pool[$index]);
echo "Filho {$index} morreu..." . PHP_EOL;
--$count;
}
}
sleep(1);
}
}
Fluxo simples de execução
Utilizando sinais
Para lidar com eventos externos
pcntl_signal(SIGINT, [$this, 'signalHandler']);
// Para SIGTERM, SIGINT, SIGHUP, SIGUSR1, etc
public function signalHandler($signal) {
switch ($signal) {
case SIGTERM:
case SIGINT:
case SIGHUP:
echo 'Terminando graciosamente...';
die();
case SIGUSR1:
echo "Capturado sinal SIGUSR1 " . PHP_EOL;
break;
/* ... */
}
}
Demonstração
pthreads
Biblioteca que implementa o padrão POSIX Threads
- A V3 foi totalmente reescrita para uso no PHP 7.2
- Necessita do PHP compilado com ZTS (Zend Thread Safety)
Classes disponíveis
Thread
Worker
Pool
Volatile
Classe Thread
Ela deve implementar o método run()
class Task extends \Thread
{
private $threadId;
public function __construct($threadId)
{
$this->threadId = (int) $threadId;
}
public function run()
{
echo "Iniciando a thread {$this->threadId}" . PHP_EOL;
sleep(rand(1, 5));
echo "Finalizando a thread {$this->threadId}" . PHP_EOL;
}
}
Classe Worker
Agrupa tarefas para serem executadas sequencialmente
$worker = new Worker();
$worker->start();
// Empilha 9 tarefas no worker
for ($i = 0; $i < 8; ++$i) {
$worker->stack(new Task($i));
}
// Aguarda o término das tarefas
while ($worker->collect());
// Desliga o worker
$worker->shutdown();
Classe Pool
Agrupa Workers
para serem executados concorrentemente
// Cria 3 workers que serão executados simultaneamente
$pool = new Pool(3);
// Submete 9 tarefas para o pool
for ($i = 0; $i < 8; ++$i) {
$pool->submit(new Task($i));
}
// Aguarda o término das tarefas
while ($pool->collect());
// Desliga todos os workers
$pool->shutdown();
Pontos de atenção
- Tenha cuidado ao realizar operações atômicas (métodos
synchronized
,notify
ewait
) - Nem toda tarefa ganha performance ao ser dividida em threads
- Não se esqueça de aguardar as threads terminarem (
join
)
Referências
- Tutorial para instalação
- Slides sobre ZTS e threads no PHP (@jpauli)
- Tutorial sobre pthreads (SitePoint)
- Tutorial sobre pthreads v2 x v3 (SitePoint)
- Manual do PHP: php.net/pthreads
Demonstração
Reutilização
Criando códigos que rodem em mais de um ambiente
Boas práticas
Deixe seu código limpo e organizado para facilitar o entendimento
-
PHP CodeSniffer
Detecta e corrige violações de acordo com um padrão de regras (por exemplo, PSR-2) -
PHP Mess Detector
Analisador diversos aspectos, como variáveis desnecessárias, códigos muito complexos, etc -
SOLID
Cinco princípios para deixar softwares mais entendíveis, flexíveis e de fácil manutenção -
Object Calisthenics
Conjunto de 9 regras para auxiliar na manutenção, legibilidade e facilidade de teste
Configurações
Como armazenar parâmetros, credenciais e outras opções?
- 12 Factor Apps: III. Config - Store config in the environment
- Arquivos PHP (com simples arrays, por exemplo)
// config.php
return [
'db' => [
'dsn' => 'mysql:dbname=mydb;host=localhost',
'user' => 'user',
'pass' => 'my@p4ssw0rd'
]
];
// index.php
$config = require 'config.php';
$dbh = new PDO(
$config['db']['dsn'],
$config['db']['user'],
$config['db']['pass']
);
Injeção de dependências
Aumentando a reusabilidade de seus códigos
-
Interfaces
- Dependa de Interfaces ao invés de classes (mesmo que abstratas)
-
Containers
- PSR-11 - Containers: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md
- The PHP League Container: https://github.com/thephpleague/container
class MyClass
{
protected $logger;
public function __construct(\Psr\Log\LoggerInterface $logger)
{
$this->logger = $logger;
}
public function run()
{
$this->logger->notice('...');
}
}
// Psr\Container\ContainerInterface
$container = require 'container.php';
$myClass = new MyClass(
$container->get(\Psr\Log\LoggerInterface::class)
);