https://a.storyblok.com/f/270183/9867/ab5b27ca0b/revolt-php_voiceapi.png

PHP assíncrono com Revoltphp e a Voice API da Vonage

Publicado em November 8, 2021

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!

Dinosaurs roaming freely out of their pens!

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.

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 8000

Se tudo correr bem, você receberá uma resposta como esta:

Screenshot of ngrok running as a process

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:

Screenshot of the web voicehooks section in the Vonage dashboard

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 callbackID para gerenciamento de fibra.

  • A $static variá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:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondePromotor Sênior de Desenvolvimento em PHP

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.