
Compartilhar:
Sou ator formado, com uma dissertação sobre stand-up comedy, e comecei a me dedicar ao desenvolvimento em PHP por meio dos encontros da comunidade. Você pode me encontrar dando palestras e escrevendo sobre tecnologia, ou ouvindo e comprando discos curiosos da minha coleção de vinil.
PHP assíncrono com Revoltphp e a Voice API da Vonage
Tempo de leitura: 9 minutos
Talvez alguns leitores se surpreendam ao saber que o PHP assíncrono não é novidade. O PHP 5.5 introduziu os geradores já em 2014, o que nos colocou nesse caminho, e, desde então, temos visto a criação de amphp, ReactPhpe OpenSwoole.
Olá, fibras!
Os desenvolvedores de PHP tendem a não pensar em termos de programação assíncrona devido à natureza do ciclo de vida de solicitação/resposta (com o estado encapsulado) com o qual estamos acostumados a trabalhar. No entanto, ocorreu algo que pode muito bem mudar isso: a introdução de fibras nativas no PHP 8.1. Embora as fibras possam não ser uma execução assíncrona “verdadeira”, enquanto ambientes de execução como Node.js e Go , elas certamente podem proporcionar um aumento significativo de desempenho se executadas sem nenhuma E/S bloqueante.
Olá, RevoltPhp!
Um novo projeto foi criado a partir do lançamento do PHP 8.1, RevoltPhp, que é uma colaboração entre os criadores do amphp e do ReactPhp, com o objetivo de aplicar sua experiência em corrotinas para aproveitar o novo recurso de fibras. Embora seja melhor pensar nisso mais como uma “biblioteca subjacente” para um framework usar sobre ela (Concepts como callbacks de fluxos legíveis/graváveis podem ser bem difíceis de entender), vou mostrar a vocês uma pequena amostra de como vocês podem aprender esse conceito.
Emergência! Ativo fora do confinamento!

OK, o que quero dizer é que vou apresentar nosso caso de uso, mas gosto de ser um pouquinho dramático às vezes. Digamos que tenhamos nosso parque de dinossauros do mundo real. A equipe precisa ser notificada quando um lagarto furioso e devorador de humanos foge de seu recinto. O problema é que o sistema de comunicações foi escrito em <insert your favourite PHP framework of choice>e, portanto, está tecnicamente em uma linguagem de E/S bloqueante. Você precisa usar o Vonage para ligar simultaneamente para 2.000 funcionários do parque com um aviso de texto para voz, certo? Vamos criar uma thread de código assíncrona.
Configuração: PHP 8.1, Composer, Slim, ngrok, Vonage, RevoltPhp
PHP 8.1
Para isso, você precisará do PHP 8.1, que ainda não foi lançado oficialmente. Os usuários de Mac podem encontrá-lo em repositório Homebrew do shivammathur, enquanto os usuários de Linux podem encontrá-lo no PPA do ondrej para o apt, e os usuários do Windows podem encontrá-lo na seção de controle de qualidade do PHP para Windows.
Compositor
Precisamos do Composer, o gerenciador de dependências de fato do PHP, portanto siga as instruções de instalação aqui , caso ainda não o tenha instalado.
Espaço de projetos
Os requisitos a seguir exigirão um espaço para o seu projeto; portanto, crie um novo diretório onde o código ficará armazenado e use o Composer para criar uma composer.json configuração. Para isso, execute o seguinte comando no seu diretório vazio:
composer init
Slim Framework
Para ter um ciclo de eventos verdadeiramente não bloqueante e ter o gerenciamento de solicitações HTTP, seria recomendável usar algo como cliente HTTP do ReactPhp. Para este exemplo, porém, precisamos de algumas rotas abertas para o tratamento da Voice API, e o Slim é uma maneira rápida de fazer isso. Para obtê-lo, usamos o Composer:
composer require slim/slim
Também precisamos de uma biblioteca compatível com PSR-7 para lidar com solicitações e respostas (optei pela do Guzzle, mas há várias opções disponíveis):
composer require guzzlehttp/psr7
ngrok
Se você ainda não conhece o o ngrok antes, saiba que é uma ferramenta superútil para criar túneis de URL seguros para o seu localhost. Vamos precisar disso para que os webhooks da Vonage funcionem. Confira as instruções de instalação aqui e crie um Account.
Voice API da Vonage
A Vonage oferece uma API completa para enviar e receber chamadas; por isso, vamos usar o SDK do PHP para fazer chamadas de saída. Instale-o com o Composer:
composer require vonage/client-core
RevoltPhp
Por fim, precisamos obter o Event Loop do RevoltPhp. No momento, ele ainda está em fase de pré-lançamento, então você precisará especificar o branch “dev”:
composer require revolt/event-loop:dev-main
Configuração das Applications e Numbers da Vonage
Para fazer chamadas para alertar os funcionários do parque, que vivem em feliz ignorância, sobre o perigo iminente, você precisará configurar sua Account da Vonage adequadamente.
Crie um novo aplicativo com a funcionalidade Voice ativada e baixe as chaves do aplicativo.
Ligue logo!
OK, vamos começar a trabalhar no aplicativo Slim. Crie um diretório na raiz do seu projeto chamado /public e crie um novo arquivo PHP nele chamado index.php. Nosso arquivo ficará assim:
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Vonage\Client;
use Vonage\Client\Credentials\Keypair;
use Vonage\Voice\Endpoint\Phone;
use Vonage\Voice\OutboundCall;
use Vonage\Voice\Webhook;
require __DIR__ . '/../vendor/autoload.php';
$keypair = new Keypair(
file_get_contents('../revolt_php_example.key'),
'940597b9-7f52-416f-8fd4-a19e0f689602'
);
$vonage = new Client($keypair);
$faker = Faker\Factory::create('en_GB');
$phoneNumbers = [];
for ($i = 1; $i < 1201; $i++) {
$phoneNumbers[] = $faker->phoneNumber();
}
$app = AppFactory::create();
$app->get('/code32', function (Request $request, Response $response) use ($phoneNumbers, $vonage) {
foreach ($phoneNumbers as $outboundNumber) {
$outboundCall = new OutboundCall(
new Phone($outboundNumber),
new Phone('999999')
);
$outboundCall
->setAnswerWebhook(
new Webhook('/webhook/answer', 'GET')
)
->setEventWebhook(
new Webhook('/webhook/event', 'GET')
);
$vonage->voice()->createOutboundCall($outboundCall);
}
$response->getBody()->write('Park employees notified.' . PHP_EOL);
return $response;
});
$app->run();
Há muita coisa para assimilar aqui, então vamos analisar passo a passo.
Primeiramente, vamos configurar nosso cliente Vonage com as credenciais do aplicativo que criamos anteriormente, usando um Keypair objeto e lendo a chave SSH que você baixou como primeiro argumento, com o ID do aplicativo como segundo:
$keypair = new Keypair(
file_get_contents('../my-example-app.key'), // <- SSH key downloaded from Vonage dashboard and put in the root directory
'9999999-7f52-416f-8fd4-a19e0f689602' // <- application key here
);
$vonage = new Client($keypair);
Em seguida, simulamos uma carga útil de números de telefone para ligar usando o biblioteca faker , atribuída a uma variável chamada $phoneNumbers.
$faker = Faker\Factory::create('en_GB');
$phoneNumbers = [];
for ($i = 1; $i < 2001; $i++) {
$phoneNumbers[] = $faker->phoneNumber();
}
O Faker permite definir uma localidade; nesse caso, optei pelos números do Reino Unido, configurando-o como 'en_GB'. Se você quiser definir uma localidade diferente, dê uma olhada na documentação do Faker aqui.
Estamos usando um for para inserir os números de telefone em uma matriz; agora temos 2.000 números de telefone prontos para receber seus alertas sobre dinossauros. Como fazemos isso? Com um foreach loop no endpoint:
$app->get('/code32', function (Request $request, Response $response) use ($phoneNumbers, $vonage) {
foreach ($phoneNumbers as $outboundNumber) {
$outboundCall = new OutboundCall(
new Phone($outboundNumber),
new Phone('MY_VIRTUAL_NUMBER') // <- this is a dummy phone number, make it your virtual number on your app
);
$outboundCall
->setAnswerWebhook(
new Webhook('/webhook/answer', 'GET')
)
->setEventWebhook(
new Webhook('/webhook/event', 'GET')
);
$vonage->voice()->createOutboundCall($outboundCall);
}
$response->getBody()->write('Park employees notified.' . PHP_EOL);
return $response;
});
Este tutorial está simulando um exemplo, portanto, não execute isso em ambiente real! O motivo é que serão gerados 2.000 números de telefone falsos, e a Vonage tentará ligar para todos eles!
Então, temos um endpoint a ser chamado em nosso aplicativo. Ele percorrerá todos os números de telefone para ligar, mas há duas coisas necessárias para concluir nossa aviso . Você percebeu aquele setAnswerWebhook() no código acima? Bem, assim que fizermos essa chamada, a Vonage precisa saber o que fazer com ela. É aí que entram o ngrok e nossos webhooks.
Conexão das chamadas
O Ngrok abrirá um túnel e fornecerá uma URL para o localhost quando você o iniciar. O PHP possui um servidor web integrado, então vamos usá-lo para o localhost e, em seguida, executar o Ngrok para abrir o túnel. Enquanto estivermos no public diretório que criamos, inicie o servidor web integrado do PHP:
php -S 0.0.0.0:8000 -t .A porta 8000 já está aberta em nossa máquina; portanto, digite o seguinte para que o ngrok crie um túnel para ela:
ngrok http 8000Se tudo correr bem, você receberá uma resposta como esta:

A URL fornecida precisará ser adicionada ao seu aplicativo Vonage. Acesse seu aplicativo Vonage no painel de controle e clique em “Editar”. No painel “Editar aplicativo”, você pode configurar os webhooks de Voice para chamadas recebidas; pegue a URL do ngrok e adicione os caminhos nos quais colocamos espaços reservados ao definir os webhooks em nosso código PHP. Por exemplo, se o ngrok criou a URL https://aef9-82-30-208-179.ngrok.io, alteraríamos nossas URLs de webhooks para
https://aef9-82-30-208-179.ngrok.io/webhooks/answer
https://aef9-82-30-208-179.ngrok.io/webhooks/event
É aqui que você pode editá-los no painel do Vonage:

Em seguida, alteramos nosso código PHP para que nossa rota fique assim ao configurar os webhooks:
$baseUrl = 'https://aef9-82-30-208-179.ngrok.io'
$outboundCall
->setAnswerWebhook(
new Webhook($baseUrl . '/webhook/answer', 'GET')
)
->setEventWebhook(
new Webhook($baseUrl . '/webhook/event', 'GET')
);
Configurando o aviso
Vamos emitir nosso alerta sobre dinossauros com uma nova rota para a qual o webhook de resposta está apontando. Para usar a conversão de texto em fala da Vonage, utilizamos o que é chamado de NCCO object, que é um termo sofisticado para um objeto JSON que controla o que fazer com a chamada. Adicione a seguinte rota ao seu index.php:
$app->get('/webhook/answer', function (Request $request, Response $response) {
$ncco = [
[
'action' => 'talk',
'language' => 'en-GB',
'style' => 1,
'text' => 'This is a code 32. Asset #784 is out of containment.'
]
];
$response->getBody()->write(json_encode($ncco));
return $response
->withHeader('Content-Type', 'application/json');
});
O objeto NCCO é fornecido como uma resposta JSON ao webhook, para que a Vonage saiba o que fazer com ele — neste caso, o language e style de sua escolha irá ler o text que você fornecer, da maneira que você escolher.
Voltar para “Assíncrono x Síncrono”
Temos um endpoint para nossas chamadas de saída e uma resposta pronta para dar quando as pessoas atenderem a chamada de emergência. Mas o foco deste artigo era o código assíncrono, certo? Nosso endpoint de emergência, quando acionado em tempo de execução, percorrerá sincronicamente cada número e ligará para ele; isso é PHP. Então, agora é hora das fibras.
Apresentando o RevoltPhp
O ciclo de eventos do RevoltPhp continuará executando qualquer tarefa até que não haja mais nada a ser feito e, então, devolverá o controle à thread pai (isso geralmente marca o encerramento do aplicativo, pois, para um aplicativo PHP com E/S não bloqueante, queremos que o EventLoop para nunca fique sem tarefas).
No nosso caso, nossas chamadas de saída são, atualmente, síncronas e bloqueantes dentro do foreach loop. Queremos notificar todos os 2.000 funcionários do parque de uma só vez, antes que o inevitável caos se instale.
O ciclo de eventos do RevoltPhp define seis callbacks principais que a EventLoop classe irá executar:
Adiar
A função de retorno é executada na próxima iteração do ciclo de eventos. Se houver defers agendados, o ciclo de eventos não aguardará entre as iterações.
Atraso
A função de retorno é executada após o número especificado de segundos. As frações de segundo podem ser expressas como números de ponto flutuante.
Repetir
A função de retorno é executada após o número especificado de segundos, repetidamente. As frações de segundo podem ser expressas como números de ponto flutuante.
Legível em fluxo
A função de retorno é executada quando há dados no fluxo para serem lidos ou quando a conexão é encerrada.
Fluxo gravável
A função de retorno é executada quando há espaço suficiente no buffer de gravação para aceitar novos dados a serem gravados.
Sinal
A função de retorno é executada quando o processo recebe um sinal específico do sistema operacional.
OK, então precisamos criar callbacks dentro da nossa rota. De acordo com nossos requisitos, vamos precisar do repeat callback. Veja como fica:
$app->get('/code32', function (Request $request, Response $response) use ($phoneNumbers, $vonage) {
EventLoop::repeat(0, function ($callbackId) use ($phoneNumbers, $vonage): void {
static $i = 0;
if (isset($phoneNumbers[$i])) {
$outboundCall = new OutboundCall(
new Phone($phoneNumbers[$i]),
new Phone('MY_VIRTUAL_NUMBER') // <- this is a dummy phone number, make it your virtual number on your app
);
$baseUrl = 'https://aef9-82-30-208-179.ngrok.io'
$outboundCall
->setAnswerWebhook(
new Webhook($baseUrl . '/webhook/answer', 'GET')
)
->setEventWebhook(
new Webhook($baseUrl . '/https://aef9-82-30-208-179.ngrok.io/webhook/event', 'GET')
);
$vonage->voice()->createOutboundCall($outboundCall);
$i++;
} else {
EventLoop::cancel($callbackId);
}
});
EventLoop::run();
$response->getBody()->write('Outbound calls sent.' . PHP_EOL);
return $response;
});
Uau! Então, o que é isso?
O ciclo de eventos
EventLoop::run(); continuará funcionando enquanto houver trabalho. Então, o que estamos fazendo é criar uma carga de trabalho com a criação de callbacks estáticos EventLoop::repeat(). Aqui estão as principais partes disso:
O primeiro argumento da função de retorno é 0, pois trata-se de um número de tipo float que representa o intervalo que queremos entre as iterações. Sem atrasos, por favor, temos dinossauros à solta!
O segundo é a geração de callbacks — obtemos a
callbackIDpara gerenciamento de fibra.A
$staticvariável mantém um contador do número de callbacks que estão sendo criados. Ela está sendo usada como um índice para o$phoneNumbers; portanto, assim que não houver mais dados,isset($phoneNumbers[$i])ela fica falsa e, portanto, cancelamos o ciclo de eventos com o ID da nossa callback para referência.
Essa é a parte do código, mas o que está acontecendo nos bastidores? Finalmente, chegamos a:
PHP assíncrono
Ao contrário das operações síncronas tradicionais do PHP, a partir do momento em que o Event Loop é executado, os repeat são distribuídas pelas fibras de tempo de execução do PHP. São 2.000 chamadas disparadas por meio de fibras, em vez de serem executadas de forma síncrona. O que é interessante do ponto de vista dos desenvolvedores de PHP é que isso foi feito sem recorrer a algumas das abordagens de engenharia comuns para distribuir a carga, como o uso de um Job/Queue ou uma arquitetura sem servidor com Bref vinculado ao Google Cloud Compute ou AWS Lambda. Todas essas são abordagens perfeitamente válidas, mas o ponto principal aqui é que nossa abordagem é em PHP puro.
Graças à Vonage e à RevoltPhp, todos nós poderemos estar seguros um pouco mais rápido, graças aos esforços incansáveis da equipe do parque para colocar esse incêndio sob controle o mais rápido possível.
Compartilhar:
Sou ator formado, com uma dissertação sobre stand-up comedy, e comecei a me dedicar ao desenvolvimento em PHP por meio dos encontros da comunidade. Você pode me encontrar dando palestras e escrevendo sobre tecnologia, ou ouvindo e comprando discos curiosos da minha coleção de vinil.