PHP fora da Web

Utilizando nossa linguagem preferida para criar scripts CLI e robôs

Roteiro

  • Scripts CLI

    Lidando com argumentos, streams e roteamento de comandos

  • Robôs

    Gerenciando início e término de robôs, usando pcntl ou pthreads

  • 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

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

Zend\Console

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

Zend\Console

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

Symfony Console

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

Symfony Console

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

Symfony Console

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

Symfony Console

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

Extensão Process Control

Manual do PHP: php.net/pcntl

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 e wait)
  • Nem toda tarefa ganha performance ao ser dividida em threads
  • Não se esqueça de aguardar as threads terminarem (join)

Demonstração

Reutilização

Criando códigos que rodem em mais de um ambiente

Configurações

Como armazenar parâmetros, credenciais e outras opções?

// config.php
return [
    'db' => [
        'dsn'  => 'mysql:dbname=mydb;host=localhost',
        'user' => 'user',
        'pass' => '[email protected]'
    ]
];
// 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

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)
);