Promises no ReactPHP

No último artigo – Introdução à programação assíncrona em PHP usando o ReactPHP – tivemos uma introdução à programação assíncrona com PHP.

No artigo de hoje vamos comentar sobre Promises, que no ReactPHP trata-se de um componente. Promises no ReactPHP são uma implementação da API CommonJS Promises/A. Se você trabalha com JavaScript, vai notar muita similaridade com o que veremos a seguir.

Afinal, o que é uma Promise?

Uma Promise é uma abstração que encapsula um resultado de uma execução assíncrona para ser utilizado quando ele estiver disponível. Promises desempenham a mesma tarefa que os Callbacks dos quais já estamos acostumados, só que de maneira bem mais elegante (principalmente quando lidamos com múltiplos callbacks de forma encadeada).

Dois conceitos importantes que temos que conhecer: Promise e Deferred. Já vimos que uma Promise representa o resultado de um código assíncrono, já Deferred representa a computação que vai gerar esse resultado em algum momento futuro. Um objeto Deferred sempre vai ter uma Promise associada que vai representar (encapsular) o resultado. Usar um objeto Deferred é uma forma de separar a Promise de quem vai resolvê-la ou rejeitá-la (em algum momento futuro).

Uma Promise possui três estados possíveis:

  • unfulfilled: É o estado inicial, pois o valor retornado de Deferred ainda é desconhecido.
  • fulfilled: Esse estado indica que a Promise está devidamente “alimentada” do resultado da computação de Deferred.
  • failed: Houve uma exceção durante a execução do Deferred (o executor pode ter chamado reject()).

Já um objeto Deferred possui dois métodos para mudar o estado da sua Promise:

  • resolve(string $result): Quando o código é executado com sucesso, esse método altera o estado da Promise para fulfilled (enviando para ela o resultado que ela encapsulára);
  • reject(string $reason): A execução do código falhou, altera o estado da Promise para failed.

Uma Promise também possui métodos para que definamos handlers (callbacks a serem executados) para as mudanças de estados dela, são esses: then(), done(), otherwise() e always().

Antes de iniciarmos, certifique-se de instalar o componente React Promise no seu projeto:

$ composer require react/promise

Vamos então ao primeiro exemplo do uso de um objeto Deferred e de sua Promise:

<?php

require './vendor/autoload.php';

use React\Promise\Deferred;

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

$promise->done(function ($data) {
    echo 'Resultado: ' . $data;
});

$deferred->resolve('Olá mundo!'); // Resultado: Olá mundo!

Criamos um objeto Deferred e definimos um handler para o método done() da Promise associada a ele. Por fim, resolvemos a operação usando o método resolve(), método este que “altera” o estado da Promise (para fulfilled) e fez o nosso handler (a função anônima que definimos no primeiro argumento do método done()) ser executado.

No caso de o objeto Deferred explicitamente rejeitar uma operação alterando o estado da Promise para failed, podemos usar o segundo argumento do método done() para definir um handler a ser executado em caso de rejeição:

O método done() aceita como argumento handlers tanto para o estado fulfilled quanto para o failed:

public function done(callable $onFulfilled = null, callable $onRejected = null)

No primeiro argumento informamos o handler que será executado quando o estado da Promise mudar para fulfilled, no segundo argumento, um handler para ser executado quando o estado da Promise for alterado para failed.

Por exemplo, vamos informar um handler pro segundo argumento da Promise e então vamos executar o método reject() do Deferred:

<?php

require './vendor/autoload.php';

use React\Promise\Deferred;

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

$promise->done(function ($data) {
    echo 'Resultado: ' . $data;
}, function($reason) {
    echo 'Motivo da falha: ' . $reason;
});

$deferred->reject('Erro interno'); // Motivo da falha: Erro interno

Portanto, temos a seguinte relação:

Promises podem ser encadeadas, ou seja, o valor de uma promise resolvida pode ser encaminhado para a próxima da sequência. Isso pode ser atingido usando os seguintes métodos: then() e otherwise(). O método then() é parecido com o done(), com a diferença que ele retorna uma nova promise, enquanto o done() sempre retorna null. E o método otherwise() é uma forma de definir um handler para quando o estado da Promise for failed (e ele retorna uma nova Promise).

Poderíamos, então, trocar o done() por then() no exemplo anterior:

<?php

require './vendor/autoload.php';

use React\Promise\Deferred;

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

$promise->then(function ($data) {
    echo 'Resultado: ' . $data;
}, function($reason) {
    echo 'Motivo da falha: ' . $reason;
});

$deferred->reject('Erro interno'); // Motivo da falha: Erro interno

Ou, uma vez que then() retorna uma nova Promise, poderíamos usar o otherwise() de forma encadeada:

<?php

require './vendor/autoload.php';

use React\Promise\Deferred;

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

$promise
    ->then(function ($data) {
        echo 'Resultado: ' . $data;
    })
    ->otherwise(function($reason) {
        echo 'Motivo da falha: ' . $reason;
    });

$deferred->reject('Erro interno'); // Motivo da falha: Erro interno

Outro exemplo com o encadeamento de Promises:

<?php

require './vendor/autoload.php';

use React\Promise\Deferred;

$deferred = new Deferred();

$deferred->promise()
    ->then(function ($data) {
        return "Olá, {$data}";
    })->then(function($data) {
        return "{$data}Web";
    })->then(function($data) {
        return strtoupper($data);
    })->then(function($data) {
        echo "{$data}!";
    });

$deferred->resolve('Treina'); // OLÁ, TREINAWEB!

Na programação assíncrona faz muito sentido uma Promise retornar outra Promise, pois não temos o resultado da operação de imediato (quando envolve I/O), o que temos é uma “promessa” de que em algum momento ele poderá vir a estar disponível. Nesse sentido, then() é um mecanismo para aplicar uma transformação em uma Promise e gerar uma nova Promise a partir dessa transformação.

Outro exemplo de encadeamento de then() com otherwise():

<?php

require './vendor/autoload.php';

use React\Promise\Deferred;

$deferred = new Deferred();

$deferred->promise()
    ->then(function ($data) {
        return "Olá, {$data}";
    })->then(function($data) {
        throw new InvalidArgumentException("{$data}Web");
    })->otherwise(function(InvalidArgumentException $exception) {
        return strtoupper($exception->getMessage());
    })->done(function($data) {
        echo "{$data}!";
    });

$deferred->resolve('Treina'); // OLÁ, TREINAWEB!

Nesse exemplo o nosso handler em otherwise() só vai ser invocado se ele receber uma exceção do tipo InvalidArgumentException (e fizemos ele receber).

Outro exemplo:

<?php

require './vendor/autoload.php';

use React\Promise\Deferred;

$deferred = new Deferred();

$deferred->promise()
    ->then(function ($data) {
        return "Olá, {$data}";
    })->then(function($data) {
        throw new RuntimeException("{$data}Web");
    })->otherwise(function(InvalidArgumentException $exception) {
        return strtoupper($exception->getMessage());
    })->otherwise(function(Exception $exception) {
        return strtolower($exception->getMessage());
    })->done(function($data) {
        echo "{$data}!";
    });

$deferred->resolve('Treina'); // olá, treinaweb!

Nesse exemplo, como a promise lança uma exceção do tipo RuntimeException, apenas o handler que está esperando por uma exceção genérica (do tipo Exception) será invocado:

...
})->otherwise(function(Exception $exception) {
  return strtolower($exception->getMessage());
})
...

E, claro, se nenhuma promise passar pelo estado failed os nossos handlers que lidam com as falhas não serão invocados:

<?php

require './vendor/autoload.php';

use React\Promise\Deferred;

$deferred = new Deferred();

$deferred->promise()
    ->then(function ($data) {
        return "Olá, {$data}";
    })->then(function($data) {
        // throw new RuntimeException("{$data}Web");
        return "{$data}Web";
    })->otherwise(function(InvalidArgumentException $exception) {
        return strtoupper($exception->getMessage());
    })->otherwise(function(Exception $exception) {
        return strtolower($exception->getMessage());
    })->done(function($data) {
        echo "{$data}!";
    });

$deferred->resolve('Treina'); // Olá, TreinaWeb!

Agora que já entendemos o funcionamento do objeto Deferred e como as Promises funcionam, podemos verificar como seria resolver (ou rejeitar) uma Promise sem um objeto Deferred:

<?php

require './vendor/autoload.php';

use React\Promise\Promise;

$promise = new Promise(function(Closure $resolve, Closure $reject) {
    if (\random_int(1, 1000000) % 2 === 0) {
        $resolve('Gerou um número par.');
    } else {
        $reject('Gerou um número ímpar.');
    }
});

$promise->then(function($data) {
    echo 'Sucesso: ' . $data;
})->otherwise(function($reason) {
    echo 'Falha: ' . $reason;
});

Nesse exemplo instanciamos um objeto Promise passando para ele uma função anônima que vai cuidar da computação que precisamos realizar. Apenas para fins didáticos, estamos gerando um número inteiro aleatório e então verificamos se ele é par, se verdadeiro, resolvemos a Promise, caso contrário, a rejeitamos. Nisso que passamos a função anônima no construtor da classe Promise, ela é invocada e recebe duas novas funções anônimas como argumento: $resolve e $reject. São essas funções que invocamos no término da nossa operação para decidir o estado da Promise, se vai ser fulfilled ou failed. Por fim, apenas definimos handlers que serão executados em caso de sucesso ou falha e, para isso, usamos os métodos then() e otherwise().

Observe que a diferença dessa abordagem (de instanciar um objeto Promise diretamente) para a que usa um objeto Deferred, é que nessa última invertemos o controle de quem resolve a Promise, ou seja, essa responsabilidade fica com o objeto Deferred.

Agora que já vimos o essencial do funcionamento das promises, podemos fazer um paralelo de como seria um código que usa callbacks versus um que usa promises (usando JavaScript como linguagem de referência):

request('http://www.treinaweb.com.br', function (error, response) {
    if (error) {
        // Aqui a gente lida com o erro.
    } else {
        request('http://www.treinaweb.com.br/' + response.path, function (error, response) {
            if (serror) {
                // Aqui a gente lida com o erro.
            } else {
                // Aqui lidaríamos com o sucesso da requisição.
            }
        });
    }
});

Nesse modelo estamos sempre aninhando callbacks uns dentro dos outros, o que nos torna suscetíveis ao mal do callback hell. O mesmo exemplo acima usando a biblioteca axios que trabalha com Promises ficaria assim:

axios.get('http://www.treinaweb.com.br')
    .then(function (response) {
        return axios.get('http://www.treinaweb.com.br/' + response.path);
    })
    .then(function (response) {
        // Lida com a resposta da requisição anterior
    })
    .catch(function (error) {
        // Lida com alguma exceção, se existir.
    });

No artigo de introdução ao ReactPHP mostramos um exemplo que usava a library reactphp-buzz. Veremos outro exemplo com ela, pois ela faz uso intensivo de promises. Primeiro instale-a como dependência do projeto:

$ composer require clue/buzz-react:^2.6

O nosso exemplo vai de forma assíncrona imprimir o bairro de alguns CEPs usando a API pública do Postmon:

<?php

require './vendor/autoload.php';

use Clue\React\Buzz\Browser;
use React\EventLoop\Factory;
use Psr\Http\Message\ResponseInterface;

$browser = new Browser(
    $loop = Factory::create()
);

$ceps = [
    '01311200', // Bela Vista
    '70630904', // Setor Militar Urbano
    '70165900', // Zona Cívico-Administrativa
    '32685888', // Erro, cep não existe.
];

foreach ($ceps as $cep) {
    $browser->get("https://api.postmon.com.br/v1/cep/{$cep}")
        ->then(function (ResponseInterface $response) {
            $endereco = \json_decode($response->getBody());

            echo $endereco->bairro . PHP_EOL;
        })
        ->otherwise(function (\Exception $exception) use ($cep) {
            echo 'Erro no CEP: ' . $cep . PHP_EOL;
        });
}

$loop->run();

Esse exemplo demonstra que o método get() que realiza uma requisição HTTP retorna uma Promise. Se você executar esse exemplo várias vezes, verá que não terá uma ordem definida para a impressão dos resultados, ademais, um resultado só é impresso quando ele fica pronto, não seguindo uma ordem linear (que é o que estamos acostumados na programação síncrona).

Para finalizar, é importante pontuar que Promises por si só não fazem nossos códigos serem assíncronos, elas são apenas mecanismos para encapsular os resultados.

Até a próxima!

Deixe seu comentário

Head de desenvolvimento. Vasta experiência em desenvolvimento Web com foco em PHP. Graduado em Sistemas de Informação. Pós-graduando em Arquitetura de Software Distribuído pela PUC Minas. Zend Certified Engineer (ZCE) e Coffee Addicted Person (CAP). @KennedyTedesco

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES