https://d226lax1qjow5r.cloudfront.net/blog/blogposts/asynchronous-php-with-revoltphp-vonage-voice-api/revolt-php_voiceapi.png

Asynchrones PHP mit Revoltphp & Vonage Voice API

Zuletzt aktualisiert am November 8, 2021

Lesedauer: 8 Minuten

Es mag einige Leser überraschen, dass asynchrones PHP nichts Neues ist. Mit PHP5.5 wurden bereits 2014 Generatoren eingeführt, die uns auf diesen Weg brachten, und seither haben wir die Entwicklung von amphp, ReactPhpund OpenSwoole.

Hallo, Fasern!

PHP-Entwickler neigen nicht dazu, in Begriffen der asynchronen Programmierung zu denken, da wir mit der Art des Anfrage/Antwort-Lebenszyklus (mit dem gekapselten Zustand) vertraut sind. Es ist jedoch etwas passiert, was das ändern könnte: die Einführung von nativen Fasern in PHP8.1. Auch wenn Fibers keine "echte" asynchrone Ausführung sind, während Laufzeiten wie node.js und Go sind, können sie sicherlich einen massiven Leistungsschub bringen, wenn sie ohne blockierende E/A ausgeführt werden.

Hallo, RevoltPhp!

Im Zuge der Veröffentlichung von PHP8.1 wurde ein neues Projekt ins Leben gerufen, RevoltPhpist eine Zusammenarbeit zwischen den Entwicklern von amphp und ReactPhp mit dem Ziel, ihre Erfahrungen mit Co-Routinen in die Nutzung der neuen Fasern einfließen zu lassen. Obwohl es am besten ist, es als eine "zugrundeliegende Bibliothek" für ein darauf aufbauendes Framework zu betrachten (Concepts wie Read/Writeable Stream Callbacks können ziemlich schwierig zu navigieren sein), zeige ich Ihnen einen kleinen Vorgeschmack darauf, wie Sie dieses Konzept erlernen können.

Notfall! Anlage aus dem Sicherheitsbehälter!

Dinosaurs roaming freely out of their pens!

OK, was ich meine, ist, dass ich unseren Anwendungsfall vorstellen werde, aber ich mag es, manchmal ein wenig dramatisch zu sein. Nehmen wir an, wir haben unseren realen Dinosaurierpark. Die Belegschaft muss benachrichtigt werden, wenn eine wütende, menschenfressende Echse aus ihrem Gehege entkommt. Die Sache ist die, das Kommunikationssystem wurde in <insert your favourite PHP framework of choice>geschrieben und ist daher technisch gesehen eine blockierende E/A-Sprache. Sie müssen Vonage verwenden, um 2000 Parkarbeiter gleichzeitig mit einer Text-to-Voice-Warnung anzurufen, richtig? Machen wir uns daran, einen asynchronen Codefaden zu erstellen.

Einrichten: PHP 8.1, Composer, Slim, ngrok, Vonage, RevoltPhp

PHP 8.1

Sie benötigen dafür PHP 8.1, das noch nicht offiziell veröffentlicht wurde. Mac-Benutzer finden es unter shivammathur's homebrew repositoryLinux-Benutzer finden es unter ondrejs apt PPAund Windows-Benutzer finden es in der QA-Sektion von PHP für Windows.

Komponist

Wir brauchen Composer, den De-facto-Abhängigkeitsmanager von PHP, also folgen Sie den Installationsanweisungen für diesen hier falls Sie ihn noch nicht haben.

Projektraum

Die folgenden Anforderungen benötigen Ihren Projektspeicherplatz, also erstellen Sie ein neues Verzeichnis, in dem sich der Code befinden wird, und verwenden Sie composer, um eine composer.json Konfiguration zu erstellen. Führen Sie dazu den folgenden Befehl in Ihrem leeren Verzeichnis aus:

composer init

Schlanker Rahmen

Um eine wirklich nicht-blockierende Ereignisschleife zu haben und HTTP-Request-Handling zu haben, sollte man etwas wie ReactPhp's HTTP Klient. Für dieses Beispiel brauchen wir jedoch einige offene Routen für die Handhabung der Voice API, und Schlank ist ein schneller Weg um dies zu tun. Um es zu bekommen, verwenden wir composer:

composer require slim/slim

Wir brauchen auch eine PSR-7-konforme Bibliothek, um Anfragen/Antworten zu bearbeiten (ich habe mich für Guzzle entschieden, aber es gibt mehrere Optionen):

composer require guzzlehttp/psr7

ngrok

Falls Sie noch nicht mit ngrok noch nicht kennen, ist es ein sehr nützliches Tool zum Erstellen sicherer URL-Tunnel zu Ihrem Localhost. Das brauchen wir, damit die Webhooks von Vonage funktionieren. Schauen Sie sich die Installationsanweisungen hier und erstellen Sie sich einen Account.

Vonage Voice API

Vonage bietet eine voll funktionsfähige API für das Senden und Empfangen von Anrufen, daher werden wir das PHP SDK verwenden, um ausgehende Anrufe zu senden. Installieren Sie es mit Composer:

composer require vonage/client-core

RevoltePhp

Zum Schluss brauchen wir noch die Event Loop von RevoltPhp. Sie ist derzeit noch Pre-Release, also müssen Sie den Dev Branch angeben:

composer require revolt/event-loop:dev-main

Einrichten von Vonage Applications & Numbers

Um ausgehende Anrufe zu tätigen, um die ahnungslosen Parkarbeiter vor der Gefahr zu warnen, müssen Sie Ihren Vonage Account entsprechend einrichten.

Vonage API-Konto

Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.

In diesem Lernprogramm wird auch eine virtuelle Telefonnummer verwendet. Um eine zu erwerben, gehen Sie zu Rufnummern > Rufnummern kaufen und suchen Sie nach einer Nummer, die Ihren Anforderungen entspricht.

Erstellen Sie eine neue Anwendung mit aktivierter Voice-Funktion und laden Sie die Anwendungsschlüssel herunter.

Rufen Sie an!

OK, fangen wir mit der Slim-Anwendung an. Erstellen Sie ein Verzeichnis in Ihrer Projektroute mit dem Namen /public und erstellen Sie darin eine neue php-Datei mit dem Namen index.php. Unsere Datei wird wie folgt aussehen:

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

Hier gibt es eine Menge zu verdauen, also lassen Sie uns das Ganze aufschlüsseln.

Zunächst richten wir unseren Vonage-Client mit den Anmeldedaten ein, die wir zuvor mit einem Keypair Objekt und lesen den SSH-Schlüssel, den Sie heruntergeladen haben, als erstes Argument und die Anwendungs-ID als zweites Argument ein:

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

Als Nächstes simulieren wir eine Nutzlast von anzurufenden Telefonnummern, indem wir die faker Bibliothek, die auf eine Variable namens $phoneNumbers.

$faker = Faker\Factory::create('en_GB');

$phoneNumbers = [];

for ($i = 1; $i < 2001; $i++) {
    $phoneNumbers[] = $faker->phoneNumber();
}

In Faker können Sie ein Gebietsschema festlegen. In diesem Fall habe ich mich für UK Numbers entschieden, indem ich "en_GB" eingestellt habe. Wenn Sie ein anderes Gebietsschema einstellen möchten, werfen Sie einen Blick in die Faker-Dokumentation hier.

Wir verwenden eine klassische for Schleife, um die Telefonnummern in einem Array zu erstellen, so dass wir jetzt 2000 Telefonnummern haben, die bereit sind, ihre Dino-Warnungen zu erhalten. Wie machen wir das? Mit einer foreach Schleife im Endpunkt:

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

Dieses Tutorial ist eine Simulation eines Beispiels, also führen Sie es nicht live durch! Der Grund dafür ist, dass 2000 gefälschte Numbers generiert werden, und Vonage wird versuchen, sie alle anzurufen!

Wir haben also einen Endpunkt, den wir in unserer App ansteuern können. Er durchläuft alle anzurufenden Telefonnummern in einer Schleife, aber es sind noch zwei Dinge nötig, um unsere synchronen Warnung. Siehst du die setAnswerWebhook() Methode im obigen Code? Nun, sobald wir den ausgehenden Anruf tätigen, muss Vonage wissen, was damit zu tun ist. Hier kommen ngrok und unsere Webhooks ins Spiel.

Verkabelung der Anrufe

Ngrok öffnet einen Tunnel und gibt Ihnen eine URL zu localhost, wenn Sie es starten. PHP hat einen eingebauten Webserver, also werden wir diesen für localhost verwenden und dann ngrok starten, um den Tunnel zu öffnen. Während Sie sich in dem public Verzeichnis, das wir erstellt haben, starten Sie den eingebauten PHP-Webserver:

php -S 0.0.0.0:8000 -t .

Port 8000 ist nun auf unserem Rechner geöffnet, also geben Sie Folgendes ein, um ngrok zum Tunneln zu veranlassen:

ngrok http 8000

Wenn alles gut geht, werden Sie eine Antwort wie diese erhalten:

Screenshot of ngrok running as a process

Die URL, die Sie erhalten, muss zu Ihrer Vonage-Anwendung hinzugefügt werden. Navigieren Sie zu Ihrer Vonage-Anwendung auf Ihrem Dashboard und klicken Sie auf Bearbeiten. Nehmen Sie die ngrok-URL und fügen Sie die Pfade hinzu, die wir beim Einstellen der Webhooks in unserem PHP-Code mit Platzhaltern versehen haben. Wenn ngrok zum Beispiel die URL https://aef9-82-30-208-179.ngrok.ioerstellt hat, würden wir unsere Webhook-URLs ändern in

  • https://aef9-82-30-208-179.ngrok.io/webhooks/answer

  • https://aef9-82-30-208-179.ngrok.io/webhooks/event

Hier können Sie sie im Vonage Dashboard bearbeiten:

Screenshot of the web voicehooks section in the Vonage dashboard

Dann ändern wir unseren PHP-Code für unsere Route würde jetzt wie folgt aussehen, wenn die Webhooks gesetzt werden:

$baseUrl = 'https://aef9-82-30-208-179.ngrok.io'

$outboundCall
    ->setAnswerWebhook(
        new Webhook($baseUrl . '/webhook/answer', 'GET')
    )
    ->setEventWebhook(
        new Webhook($baseUrl . '/webhook/event', 'GET')
    );

Einstellung der Warnung

Wir werden unsere Dino-Warnung mit einer neuen Route ausgeben, auf die der Antwort-Webhook verweist. Um Vonage Text-to-Speech zu nutzen, verwenden wir ein sogenanntes NCCO objectDas ist ein schicker Begriff für ein JSON-Objekt, das steuert, was mit dem Anruf geschehen soll. Fügen Sie die folgende Route zu Ihrem 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');
});

Das NCCO-Objekt wird als JSON-Antwort an den Webhook übergeben, damit Vonage weiß, was damit zu tun ist - in diesem Fall die language und style Ihrer Wahl wird die text Sie geben es, wie Sie wollen.

Zurück zu Async vs. Sync

Wir haben einen Endpunkt für unsere ausgehenden Anrufe, wir haben eine Antwort zu geben, wenn Menschen den Notruf beantworten. Aber in diesem Artikel ging es doch um asynchronen Code, oder? Unser Notruf-Endpunkt durchläuft zur Laufzeit eine synchrone Schleife für jede Nummer und ruft sie an; das ist PHP. So, jetzt ist es Zeit für Fasern.

Einführung von RevoltPhp

Die Ereignisschleife von RevoltPhp führt die Arbeit so lange aus, bis keine Arbeit mehr zu tun ist, und übergibt die Kontrolle an den übergeordneten Thread zurück (dies ist normalerweise die Beendigung der Anwendung, da wir bei einer nicht-blockierenden I/O-PHP-Anwendung die EventLoop auf niemals keine Arbeit mehr haben).

In unserem Fall sind unsere ausgehenden Anrufe derzeit synchron und blockieren innerhalb der foreach Schleife. Wir wollen alle 2000 Parkmitarbeiter auf einmal benachrichtigen, bevor das unvermeidliche Chaos ausbricht.

Die Ereignisschleife von RevoltPhp definiert sechs Kern-Callbacks, die die EventLoop Klasse ausführen wird:

  • Aufschieben

Der Callback wird bei der nächsten Iteration der Ereignisschleife ausgeführt. Wenn Verzögerungen vorgesehen sind, wartet die Ereignisschleife nicht zwischen den Iterationen.

  • Verzögerung

Der Callback wird nach der angegebenen Anzahl von Sekunden ausgeführt. Sekundenbruchteile können als Gleitkommazahlen ausgedrückt werden.

  • Wiederholen Sie

Der Callback wird nach der angegebenen Anzahl von Sekunden wiederholt ausgeführt. Bruchteile einer Sekunde können als Fließkommazahlen ausgedrückt werden.

  • Stream lesbar

Der Callback wird ausgeführt, wenn sich Daten auf dem Stream befinden, die gelesen werden sollen, oder die Verbindung geschlossen wird.

  • Stream beschreibbar

Der Callback wird ausgeführt, wenn im Schreibpuffer genügend Platz ist, um neue Daten zu schreiben.

  • Signal

Der Callback wird ausgeführt, wenn der Prozess ein bestimmtes Signal vom Betriebssystem erhält.

OK, wir müssen also Rückrufe innerhalb unserer Route erstellen. Aufgrund unserer Anforderungen benötigen wir den repeat Rückruf. So sieht er aus:

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

Wahnsinn! Und was ist das?

Die Ereignisschleife

EventLoop::run(); wird weiterhin funktionieren solange es Arbeit hat. Was wir also tun, ist eine Arbeitslast mit der statischen Rückruferstellung zu erzeugen EventLoop::repeat(). Hier sind die wichtigsten Teile davon:

  • Das erste Argument des Rückrufs ist 0, da dies ein Float für das Intervall ist, das wir zwischen den Iterationen wünschen. Keine Verzögerungen bitte, wir haben Dinos auf freiem Fuß!

  • Die zweite ist unsere Rückrufgenerierung - wir erhalten die callbackID für die Faserverwaltung.

  • Die Variable $static speichert einen Zähler, der angibt, wie viele Rückrufe erstellt werden. Sie wird als Index für die Variable $phoneNumbersSobald wir also keine Daten mehr haben, isset($phoneNumbers[$i]) falsch ist und wir die Ereignisschleife mit unserer Rückruf-ID als Referenz abbrechen.

Das ist der Code-Teil, aber was geht unter der Haube vor sich? Endlich kommen wir dazu:

Asynchrones PHP

Im Gegensatz zu traditionellen synchronen PHP-Operationen werden die gekapselten Rückrufe von dem Moment an, in dem die Ereignisschleife ausgeführt wird, über die repeat Rückrufe auf die PHP-Laufzeit-Fasern verteilt. Das sind 2000 Aufrufe, die mit Fasern abgefeuert werden, anstatt synchron ausgeführt zu werden. Interessant aus Sicht der PHP-Entwickler ist, dass dies ohne einige der üblichen technischen Ansätze zur Verteilung der Last, wie z. B. die Verwendung von Laravel Job/Queue-Worker oder eine Serverless-Architektur mit Bref gebunden an Google Cloud Compute oder AWS Lambda. Das sind alles sehr gute Ansätze, aber der wichtigste Punkt ist, dass unser Ansatz einfaches PHP ist.

Dank Vonage und RevoltPhp sind wir alle ein wenig schneller in Sicherheit, da unsere Parkmitarbeiter unermüdlich daran gearbeitet haben, die Anlage so schnell wie möglich wieder unter Kontrolle zu bringen.

Teilen Sie:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeSenior PHP Entwickler Advocate

Als ausgebildeter Schauspieler mit einer Dissertation in Standup-Comedy bin ich über die Meetup-Szene zur PHP-Entwicklung gekommen. Man findet mich, wenn ich über Technik spreche oder schreibe, oder wenn ich seltsame Platten aus meiner Vinylsammlung spiele oder kaufe.