PHP SP

[React PHP] Promises e assincronicidade – Parte 1

Em sequência (mais que atrasada) da série sobre React PHP, iremos tratar sobre outra biblioteca do pacote: ReactPromise.

Desta vez, eu gostaria de falar um pouco mais sobre a estrutura interna da biblioteca por achar mais didático e menos abstrato. Se isto, na realidade, tornar o tópico mais complicado, não deixe de me notificar com suas dúvidas!

 

Visão Geral

Quase tudo o que vimos até o momento com React PHP trata de rotinas assíncronas. As principais chamadas que fazemos para abrir um arquivo, um socket ou mesmo trabalhar com timers não oferecem resultado imediato: nos permitem esperar por algo que ainda está por acontecer.

Conforme a definição referenciada na página do GitHub do pacote ReactPromise, uma promessa – ou promise – é “uma abstração que torna o trabalho com operações assíncronas muito mais agradável”. (clique aqui para ver a definição completa)

O pacote ReactPromise traz uma implementação em PHP para o modelo Promises/A . E o primeiro item interessante a ser notado é que este pacote React é um dos poucos que não utiliza o Event Loop.

Os principais conceitos que precisamos trabalhar aqui são os de deferred e promise. Um deferred representa alguma rotina que ainda não terminou de ser executada (assíncrona ou não). E uma promise é a representação do resultado desta rotina. Não que uma promise seja o resultado ao completar um deferred, mas sim a representação encapsulada deste.

 

Instalação da Biblioteca

O pacote ReactPromise pode ser instalado através do Composer, basta requerer o pacote react/promise:

$ composer require react/promise

Após o download do pacote, adicione em seu arquivo a chamada para o autoload.php e estamos prontos para começar!

 

Utilização Básica de Promises

Eu sei… tá estranho… não contextualizei! Mas desta vez é mais fácil ver o código e depois partir pro conceito, olha aí!

A princípio, quando formos trabalhar com promises, a instanciação ocorre através da classe ReactPromiseDeferred e não instanciando manualmente (mais tarde veremos outras opções). A utilização mais básica ficaria mais ou menos assim:

<?php
// 01-promises.php
require_once 'vendor/autoload.php';

$deferred = new ReactPromiseDeferred();
$promise = $deferred->promise();

// Esta função é chamada quando a promise é resolvida (sucesso)
$callbackSucesso = function ($resultado) {
    var_dump($resultado);
};

// Esta função é chamada quando a promise é rejeitada (falha)
$callbackErro = function ($motivo) {
    var_dump($motivo);
};
$promise->then($callbackSucesso)
        ->otherwise($callbackErro);

$deferred->resolve('Deu certo');
$deferred->reject('Não deu certo...');

A saída deste código deverá ser algo como…

$ php 01-promises.php
string(9) "Deu certo"

Agora sim! Vamos analisar o que foi feito!

Primeiramente instanciamos nossa unidade de trabalho e armazenamos numa variavel $deferred. A partir de então utilizamos de seu método promise() para gerar um novo objeto do tipo ReactPromisePromise (não acredite em mim, faz um get_class() aí!). Este objeto é a representação do nosso resultado e nele indicaremos o que deverá ser feito em caso de sucesso e o que deverá ser feito em caso de falha.

No fim de tudo (linhas 20-21), é feita a chamada ao $deferred->resolve('Deu certo')$deferred->reject('Não deu certo...'). Mas por que só o primeiro executou o callback?

 

Estados de uma Promessa

O motivo de somente um callback ter sido executado está contido nos diversos estados que uma promessa pode atingir. Basicamente uma promessa assume somente um estado por vez dentre quatro estados possíveis: não iniciada (LazyPromise), em andamento (Promise), resolvida, com sucesso (FulfilledPromise) e rejeitada, com falha (RejectedPromise).

Ao criarmos uma promessa com $deferred->promise() pode-se assumir que seu estado é “em andamento”, pois a nossa unidade de trabalho ($deferred) foi quem criou o objeto. Ao executar $deferred->resolve($resultadoOK) estamos dizendo que nossa unidade de trabalho terminou de executar e obteve sucesso, neste momento nossa promessa assume o estado “resolvida, com sucesso” e este estado é irreversível! O mesmo ocorre em $deferred->reject($motivoFalha), porém a promessa assume o estado “rejeitada, com falha” e não muda a partir de então.

Exatamente por atingir este estado imutável de “resolvida, com sucesso” a nossa promessa não muda mais de estado ao executarmos $deferred->reject() logo após. Pois uma promessa resolvida (fulfilled) ou rejeitada (rejected) não muda de estado nunca mais!

Como é que este controle de estados é feito? Meu objeto $promise mudou de classe em tempo de execução quando eu a resolvi?

 

Transição de Estados de uma Promessa

O PHP até oferece gambiarras ferramentas que permitem alterar a classe de uma variável em tempo de execução, mas não é o caso aqui. Conforme pode-se notar aqui, a classe Promise possui um objeto interno $result: quando tem valor NULL a promise está em andamento, quando possui um objeto FulfilledPromise a promise está resolvida, e quando possui um objeto RejectedPromise a promessa está rejeitada. Esta organização permite que ao resolver uma promise, esta não seja nunca mais rejeitada e vice-versa. Como? Olha só esse trecho do código fonte da biblioteca:

 

// ReactPromisePromise::resolve()

private function resolve($value = null)
{
    if (null !== $this->result) {
        return;
    }

    $this->settle(resolve($value));
}

 

Se $result não for NULL a nossa função resolve() nem executa, faz um return e termina ali mesmo.

 

Promises na prática!

Até aqui deu pra sacar o que são as promises e como eles funcionam. Mas pra quê são úteis?

Ainda não vimos um cenário de aplicação das promises. E, realmente, está bem fora do que estamos acostumados a ver com PHP. O próprio ambiente que React PHP provê está um tanto fora da nossa zona de conforto. Mas para nossa alegria, há uma outra biblioteca React PHP que se utiliza de promises de um jeito muito simples. Dá uma olhada no que o ReactStomp faz:

 

$client
    ->connect()
    ->then(function ($client) use ($loop) {
        $client->subscribe('/topic/foo', function ($frame) {
            echo "Message received: {$frame->body}n";
        });

        $loop->addPeriodicTimer(1, function () use ($client) {
            $client->send('/topic/foo', 'le message');
        });
    });

 

Numa outra oportunidade lhe explico melhor o que este código está fazendo. Por enquanto, atente-se somente à ideia de que $client quer se conectar à um serviço remoto e esta comunicação é assíncrona. Adivinhe o tipo de retorno de $client->connect()… EXATO! ReactPromisePromise!

A maior ideia de usar promises é permitir que você diminua o acoplamento do seu código assíncrono e utilize menos callbacks. É natural que o número de callbacks cresça (muito) conforme a complexidade do seu programa e o código vai ficando cada vez mais difícil de ler e dar manutenção. Além disso, adota-se uma semântica muito mais agradável ao seu código, só de ler podemos inferir o que está acontecendo. Note a diferença:

 

$opener = new NawarianFileOpener($filename);
$opener ->open()
        ->then($lerArquivo) // sucesso
        ->otherwise($informarFalhaAoAbrirArquivo); // falha

// ...

$navigator = new NawarianHttpNavigator();
$navigator->navigateTo('http://www.phpsp.org.br')
        ->then($salvarPaginaEmPdf) // sucesso
        ->otherwise($tentarEntrarComHttps); // falha

 

Muito mais fácil de imaginar e prever o que acontecerá em vez de…

 

$fileHandle = $opener->open();
$fileHandle->on('open', $lerArquivo); // sucesso
$fileHandle->on('error', $informarFalhaAoAbrirArquivo); // falha

// ...

$page = $navigator->navigateTo();
$page->on('open', $salvarPaginaEmPdf); // sucesso
$page->on('fail', $tentarEntrarComHttps); // falha

 

Pois no último exemplo você tem de conhecer muito bem as classes NawarianFileOpenerNawarianHttpNavigator para saber que $fileHandle emite eventos com os nomes openerror, que $page emite os eventos openfail e, além disso, tem de saber o que cada evento quer dizer na realidade. Concorda?

 

Promessas como Interface de Programação

Não bastasse a utilidade natural das promessas, ganhamos ainda um bônus de desacoplamento mesmo se não utilizarmos código assíncrono. Imagine um cenário onde precisemos inserir um usuário no sistema. Como o faremos? Inserção no banco de dados? Webservice? Gravar num arquivo? Para quem utiliza o nosso código tanto faz a implementação se a gente usar promises como retorno, basta respeitar a interface. E, para nós, trocar a implementação é relativamente trivial:

 

class MantenedorDeUsuario
{

    public function inserir(ModelUsuario $usuario)
    {
        $deferred = new ReactPromiseDeferred();
        // Uma implementação qualquer de gravação, assíncrona ou não!

        try {
            // $this->inserirSoap($usuario);
            $this->inserirBD($usuario);
            // $this->inserirArquivo($usuario);

            $deferred->resolve($usuario);
        } catch (Exception $e) {
            $deferred->reject($e);
        }

        return $deferred->promise();
    }

    private function inserirSoap(ModelUsuario $usuario)
    {
        $this->getSoapClient()->inserirUsuario($usuario);
    }

    private function inserirBD(ModelUsuario $usuario)
    {
        $this->getPDO()->exec("INSERT INTO ...");
    }

    private function inserirArquivo(ModelUsuario $usuario)
    {
        $this->getFileHandler()->write("...");
    }

}

 

Conclusão

Vimos aqui o básico da biblioteca ReactPromise, como trabalhar com ela e também alguns cenários de utilização. Deve-se notar que por respeitar ao padrão Promises/A sua utilização não é muito diferente de o que vemos em bibliotecas JS como AngularJS ($q), kriskowal/q ou mesmo no jQuery.

Ainda há tópicos que não abordamos, como as Lazy Promises, encadeamento e funções utilitárias. Vou poupá-los (a você e aos tópicos) para um segundo texto, que este aqui já está literalmente de bom tamanho.

Powered by WPeMatico