https://d226lax1qjow5r.cloudfront.net/blog/blogposts/how-to-build-an-on-call-application-with-react-native-and-symfony/symfony-native_oncall_1200x600.png

React NativeとSymfonyでオンコールアプリケーションを構築する方法

最終更新日 March 2, 2021

所要時間:21 分

あなたは開発者ですか?通話中に、何かが少しおかしいと通知してくる厄介なアプリをインストールしなければならなかったことはないだろうか?例えば、エラーの閾値を超えたとか、サーバーの応答に時間がかかりすぎるとか?このチュートリアルでは、このようなアプリケーションを構築し、Vonageを使って通信を行うための基本を説明します。

このチュートリアルは を使ってPHPでを使ったモバイルアプリケーションと Reactネイティブ.

このチュートリアルの完全なコードは、私たちのサイトにあります: コミュニティ・リポジトリ.ブランチにチェックアウトしてください。 end-tutorialブランチにチェックアウトしてください。

前提条件

このチュートリアルを完了するには、以下のものが必要です:

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

リポジトリのクローン

git clone https://github.com/nexmo-community/on-call-application-api cd on-call-application-api

APIの構築

JWTキーペアの生成

このプロジェクトでは、React Nativeで構築されたモバイルアプリを使用します。
モバイル・アプリケーションとAPIの間でユーザーを認証する必要がある。このプロジェクトでは認証にJWTを使用するので、JWTトークンを作成するために証明書を生成する必要があります。
プロジェクトのルートで、以下の3つのコマンドを実行する:

mkdir -p API/var/jwt # Creates a directory to store your private and public key files. openssl genpkey -out API/var/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096 # Generates your private key file openssl pkey -in API/var/jwt/private.pem -out API/var/jwt/public.pem -pubout # Generates the public key file

アプリケーションをインターネットに公開する

Vonageで電話をかけるにはバーチャル電話番号が必要です。また、電話をかけたり、応答したり、拒否したり、終了したりするたびに発生するイベントを記録するために、Webhookをセットアップする必要があります。
このチュートリアルでは ngrokは、アプリケーションをインターネットに公開するために選択するサービスです。ngrokをインストールし、新しいターミナル・ウィンドウで次のコマンドを実行する:

ngrok http 8080 # Creates an http tunnel to the Internet from your computer on port 8080

後でプロジェクトを設定するときに必要になるので、ngrok HTTPS URLをコピーしておいてください。

環境変数

ディレクトリの中には Dockerというファイルがあります。 .env.distこのファイルを .env.

最初に更新するフィールドは、データベースの認証情報です。以下の例は、私がこのチュートリアルで使用した認証情報を示していますが、より安全なものを使用してください。

DATABASE_URL=mysql://db_user:db_password@mysql:3306/on_call?serverVersion=8.0 MYSQL_DATABASE=on_call MYSQL_USER=db_user MYSQL_PASSWORD=db_password MYSQL_ROOT_PASSWORD=root_password

VONAGE_API_KEY=VONAGE_API_SECRET=の値を更新します。 Vonage Developer Dashboard.

次に、ダッシュボードで「Your Applications」に移動します。新しいアプリケーションを作成します。 private.keyファイルをプロジェクトのルート・ディレクトリにダウンロードし、アプリケーションがVoice機能を持っていることを確認してください。

Voice API を使用する場合は、Event webhook URL を設定する必要があります。これは、最後のセクションでコピーした ngrok HTTPS URL に設定します。

以下の2つを更新する:

VONAGE_APPLICATION_PRIVATE_KEY_PATH=/var/www/API/private.key VONAGE_APPLICATION_ID=...

次に、購入済みのVonageバーチャル番号をアプリケーションにリンクします。次に、コード内の .envファイル内の Docker:

VONAGE_BRAND=OnCallAlerts VONAGE_NUMBER=... JWT_PASSPHRASE=...

最後に、同じファイルから ON_CALL_NUMBER=を見つけ、この値に電話番号を追加する。この電話番号は実際の番号で、SMSメッセージと音声通話を受信できるものでなければなりません。

Dockerの起動

以下の5つのコマンドを実行する。各コマンドの右側にあるコメントで、何をするのかを説明する:

cd Docker docker-compose up -d # To start all Docker containers for this project docker-compose exec php bash # To create a tunnel into your PHP container composer install # Installing all third-party libraries used in this project php bin/console doctrine:migrations:migrate # Creates the user table already defined in `/API/migrations`

API構築の時間!

データベース・エンティティの作成

このプロジェクトには3つの新しいデータベース・テーブルがある。 Alerts, OnCallそして、アラートとユーザーを結びつけるテーブルである、 UserAlerts.
開始するには、以下のコマンドを実行し、以下の入力指示に従ってください:

php bin/console make:entity

各フィールドに以下を追加してください:

  • クラス名:アラート

  • プロパティ名: title (String, 255, Not null)

  • プロパティ名: description (String, 255, Not null)

  • プロパティ名: status (String, 255, Not null)

コマンドが完了したら、新しいファイルを開く: src/Entity/Alert.php

この新しい Entity ファイルでは、他に 3 つのクラスが使用されます。これらのインポートをファイルの先頭に追加します:

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Gedmo\Timestampable\Traits\TimestampableEntity;

これらの新しいクラスの1つが TimestampableEntity です。 created_atupdated_atフィールドをデータベースに追加します。クラスの先頭に use TimestampableEntity;をクラスの先頭に追加します:

class Alert
{
    use TimestampableEntity;

クラス内にデフォルト値を追加する必要があるので、新しいコンストラクトを作成し、以下のようにデフォルト値を設定します:

    public function __construct()
    {
        $this->status = 'raised';
        $this->createdAt = new \DateTime();
        $this->updatedAt = new \DateTime();
    }

このクラスにいる間に、以下の2つの関数を追加する。
関数 getUserAssigned()関数は、どのユーザーが現在のアラート担当ユーザーであるかを決定します。番目の関数、 toArray()は、クラスの値を配列に変換し、APIのレスポンスに対応させます。

    public function getUserAssigned(): ?User
    {
        if ($this->getUserAlerts()->isEmpty()) {
            return null;
        }

        return $this
            ->getUserAlerts()
            ->first()
            ->getUser();
    }

    public function toArray()
    {
        return [
            'id' => $this->getId(),
            'title' => $this->getTitle(),
            'description' => $this->getDescription(),
            'status' => $this->getStatus(),
            'dateRaised' => $this->getCreatedAt()->format('Y-m-d H:i:s'),
            'assigned' => $this->getUserAssigned()->getName(),
            'incidentId' => $this->getId()
        ];
    }

エンティティを作成するには OnCallエンティティを作成するには、以下のコマンドを実行し、入力の指示に従ってください:

php bin/console make:entity

各フィールドに以下を追加してください:

  • クラス名:OnCall

  • プロパティ名:user (relation, User, ManyToOne, Not null, Add Property to User Yes)

  • プロパティ名: startDate (datetime, Not null)

  • プロパティ名: endDate (datetime, Not null)

コマンドが完了したら、新しいファイルを開く: src/Entity/OnCall.php

この新しい Entity ファイルで使用されるクラスはもう 1 つあります。このインポートをファイルの先頭に追加します:

use Gedmo\Timestampable\Traits\TimestampableEntity;

これらの新しいクラスの1つが TimestampableEntity です。 created_atupdated_atフィールドを追加します。 use TimestampableEntity;を追加します:

class OnCall
{
    use TimestampableEntity;

クラス内にデフォルト値を追加する必要があるので、新しいコンストラクトを作成し、以下のようにデフォルト値を設定します:

    public function __construct()
    {
        $this->createdAt = new \DateTime();
        $this->updatedAt = new \DateTime();
    }

UserとAlertのエンティティを結びつけるには、次のような新しいエンティティを作成する必要があります。 UserAlert.以下の手順に従ってください:

php bin/console make:entity
  • クラス名:UserAlert

  • プロパティ名:user (relation, User, ManyToOne, Not null, Add Property to User Yes)

  • プロパティ名:alert (relation, Alert, ManyToOne, Not null, Add Property to Alert yes)

  • プロパティ名: smsSentAt (datetime, null)

  • プロパティ名:voiceSentAt (datetime, null)

コマンドが完了したら、新しいファイルを開く: src/Entity/UserAlert.php

この新しい Entity ファイルで使用されるクラスはもう 1 つあります。このインポートをファイルの先頭に追加します:

use Gedmo\Timestampable\Traits\TimestampableEntity;

これらの新しいクラスの1つが TimestampableEntity です。 created_atupdated_atフィールドを追加します。 use TimestampableEntity;を追加します:

class UserAlert
{
    use TimestampableEntity;

クラス内にデフォルト値を追加する必要があるので、新しいコンストラクトを作成し、以下のようにデフォルト値を設定します:

    public function __construct()
    {
        $this->createdAt = new \DateTime();
        $this->updatedAt = new \DateTime();
    }

マイグレーションを実行する!

これで移行を実行し、新しく作成されたエンティティを反映するために、データベースに新しいテーブルとカラムを作成します。

ターミナルを実行します:

php bin/console make:migration php bin/console doctrine:migrations:migrate # If you wish to see what is being migrated, check the `API/migrations/` files for the SQL query

データフィクスチャを作る

ある時間に誰がオンコールなのかを決定するために OnCallデータベース・テーブルに定義済みのフィクスチャを作成する必要があります。そのためには、以下のコマンドを実行し、表示される指示に従ってください:

php bin/console make:fixture

名前を入力すると OnCallFixturesを入力すると API/src/DataFixturesという OnCallFixtures.php.このファイルの内容を以下のように置き換える:

<?php

namespace App\DataFixtures;

use App\Entity\OnCall;
use Carbon\CarbonImmutable;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;

class OnCallFixtures extends Fixture implements DependentFixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $currentWeek = CarbonImmutable::now();

        $onCall = new OnCall();
        $onCall
            ->setUser($this->getReference('user_1'))
            ->setStartDate($currentWeek->startOfWeek())
            ->setEndDate($currentWeek->endOfWeek());

        $manager->persist($onCall);

        $manager->flush();
    }

    public function getDependencies(): array
    {
        return [
            UserFixtures::class,
        ];
    }
}

ユーザーとオンコール記録があるように、フィクスチャを実行してみましょう!ターミナルで

php bin/console doctrine:fixtures:load

フォームを作る

アラートを発生させるための API リクエストを処理するとき、期待通りの入力であることを確認するために入力のバリデーションを行う必要があります。symfony では、これを行う最も簡単な方法はフォームを使うことです。フォームを使うことで、期待する値や制約を定義できます。以下のコマンドを実行することから始めましょう:

php bin/console make:form

画像のように指示に従ってください:

Creating an Alert Form Type

ここで、新しく作成した AlertType.phpファイルを開き src/Form/を開き、ファイルの内容を

<?php

namespace App\Form;

use App\Entity\Alert;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Length;

class AlertType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class, [
                'required' => true,
                'constraints' => [
                    new Length(['min' => 5]),
                    new NotBlank()
                ]
            ])
            ->add('description', TextType::class, [
                'required' => true,
                'constraints' => [
                    new Length(['min' => 5]),
                    new NotBlank()
                ]
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Alert::class,
            'csrf_protection' => false,
        ]);
    }
}

クラスに追加した新しいコードによって、このフォームの2つのフィールドにさらなる制約と要件が追加されます。 AlertTypeクラスに追加した新しいコードは、このフォームの2つのフィールドにさらなる制約と要件を追加します、 titledescriptionを追加し、それらが最小の長さを持ち、空白でないことを保証します。

Vonageユーティリティの構築

SMS メッセージの送信や音声通話の際に Vonage API リクエストを処理するには、Utility クラスが必要です。

API/srcという新しいディレクトリを作成する。 Utilという新しいディレクトリを作成し、その中に VonageUtil.php

Vonage の認証情報は、このチュートリアルですでに .envファイルに保存しており、 この新しい PHP クラスでこれを使用することになります。

新しいファイルに以下のコードを追加する:

<?php

namespace App\Util;

use Vonage\Client;
use Vonage\SMS\Message\SMS;
use Vonage\Voice\Endpoint\Phone;
use Vonage\Voice\NCCO\NCCO;
use Vonage\Voice\NCCO\Action\Talk;
use Vonage\Voice\OutboundCall;

class VonageUtil
{
    /**
     * @var Client
     */
    protected $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }
}

現在、このコードは新しい PHP クラスを初期化し、PHP SDK 用の Vonage Symfony ラッパーを使用して Vonage API 用の新しいクライアントを作成します。

次に、このクラス内に2つの新しい関数を追加します。この関数は、SMSを送信したり音声通話をかけたりするAPIへのリクエストを処理します。次の2つを追加します:

    public function sendSms(string $to, string $from, string $text): bool
    {
        $response = $this->client->sms()->send(
            new SMS($to, $from, $text)
        );

        $message = $response->current();

        if ($message->getStatus() == 0) {
            return true;
        }

        return false;
    }

    public function makePhoneCall(string $to, string $from, string $text)
    {
        $outboundCall = new OutboundCall(
            new Phone($to),
            new Phone($from)
        );

        $ncco = new NCCO();
        $ncco->addAction(new Talk($text));
        $outboundCall->setNCCO($ncco);

        $this->client->voice()->createOutboundCall($outboundCall);
    }

Webhookコントローラーを構築する

コントローラを作成する前に、データベースから特定のデータを引き出すためのRepository関数が必要になります。データベースの OnCallRepository.phpを開きます。 src/Repository.の下のクラス内に __construct()関数の下のクラス内に、新しい関数 findCurrentOnCallを追加します。

    public function findCurrentOnCall(\Carbon\Carbon $date)
    {
        return $this->createQueryBuilder('o')
            ->andWhere('o.startDate <= :date')
            ->andWhere('o.endDate >= :date')
            ->setParameter('date', $date->format('Y-m-d H:i:s'))
            ->getQuery()
            ->getOneOrNullResult();
    }

データを取得するための機能を作成しました。次に、リクエストを処理してデータを取得するコントローラを作成しましょう。
まず、ターミナルで以下を実行します:

php bin/console make:controller

コントローラーの名前を聞かれたら、次のように入力する。 WebhookController.

新しく作成したファイルを開く: API/src/Controller/WebhookController.php.

以下のクラスはすべて使用するので、最初からインクルードしておきましょう。ファイルの一番上の namespace App\Controller;のすぐ下に以下を追加します:

use App\Entity\Alert;
use App\Entity\OnCall;
use App\Entity\UserAlert;
use App\Form\AlertType;
use App\Util\VonageUtil;
use Carbon\Carbon;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

クラスはSymfonyがEntityManagerとVonageUtilクラスをインジェクトするためのコンストラクトを必要とします。クラスのトップに

    /** @var VonageUtil */
    protected $vonageUtil;

    /** @var EntityManagerInterface */
    private $entityManager;

    public function __construct(
        VonageUtil $vonageUtil,
        EntityManagerInterface $entityManager
    ) {
        $this->vonageUtil = $vonageUtil;
        $this->entityManager = $entityManager;
    }

ここで index()関数を以下のコードに置き換えて、新しいアラートを作成します。この新しい関数は POST リクエストボディを処理し、このデータを新しい Alertとして作成し、そのアラートをフォームに渡して値を検証します。期待通りであれば、新しい UserAlertを生成します。

    /**
     * @Route("/webhooks/raise_alert", name="raise_alert", methods={"POST"})
     */
    public function index(Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);

        // Create an alert.
        $alert = (new Alert())
            ->setStatus('raised');

        $form = $this->createForm(AlertType::class, $alert);
        $form->submit($data);

        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($alert);
            $entityManager->flush();

            // Get the on call user
            $onCall = $this->entityManager
                ->getRepository(OnCall::class)
                ->findCurrentOnCall(Carbon::now());

            if (!$onCall) {
                return new JsonResponse(['message' => 'No Alerts found.'], 400);
            }

            // Create a UserAlert
            $userAlert = (new UserAlert())
                ->setUser($onCall->getUser())
                ->setAlert($alert);
            $entityManager->persist($userAlert);

            // Notify the on call user
            $this->vonageUtil->sendSms(
                $onCall->getUser()->getPhoneNumber(),
                getenv('VONAGE_BRAND'),
                'A new alert has been raised, please log into the mobile app to investigate.'
            );

            // Save this update to the user alert
            $userAlert->setSmsSentAt(Carbon::now());

            $entityManager->flush();

            return new JsonResponse([], 201);
        }

        return new JsonResponse($this->getErrorMessages($form), 400);
    }

関数 $this->getErrorMessages()が一番下に呼び出されていることにお気づきかもしれませんが、あなたのクラスにはまだ関数がありません。次にこの関数を追加する必要があります。この関数は、エンドポイントがトリガーされたときに見つかったフォームエラーをすべて取得します。あなたの index()メソッドの下に、以下を追加します:

    private function getErrorMessages(Form $form): array
    {
        $errors = [];

        foreach ($form->getErrors() as $key => $error) {
            if ($form->isRoot()) {
                $errors['#'][] = $error->getMessage();
            } else {
                $errors[] = $error->getMessage();
            }
        }

        foreach ($form->all() as $child) {
            if (!$child->isValid()) {
                $errors[$child->getName()] = $this->getErrorMessages($child);
            }
        }

        return $errors;
    }

今はこれをテストできる段階にある!

認証のテスト

チュートリアルのこの部分には、APIでテストできる2つのエンドポイントがある。 POSTリクエストを http://localhost:8080/api/login_checkにJSONボディでリクエストする:

{
    "username": "dev+1@company.com",
    "password": "test_pass"
}

レスポンスはJSONオブジェクトで、キーは tokenを持つJSONオブジェクトで、値はJWTトークンです。

下の画像はPostmanを使った例である:

An example of authenticating with Postman

警告を発するテスト

この例では、アラートを発するために認証される必要はないので、前の例のJWTを使う必要はない。

アラートを出すには、URLフィールドを更新する: http://localhost:8080/webhooks/raise_alertメソッドを POSTリクエストとし、JSONボディを

{
    "title": "ERRORRRRRR ASAP FIX NOW ITS BORKED",
    "description": "THE PAGE AINT LOADING TOP PRIORITY FIX ASAP."
}

レスポンスは空の配列となり、HTTPステータスコードは201(created)となります。Postman でのこのリクエストの例を下の画像で見ることができます:

An example of raising an alert with Postman

アラートの対処法

symfony の Workflow コンポーネントによって、オブジェクトが通過できるステータスのライフサイクルを定義できます。トランジションはオブジェクトがある場所から別の場所に移動するために必要なアクションを定義します。

ワークフローは、アラートが次のステップに進むために、どの場所にいることができるかを定義することができます。 raisedから最後のステップである cancelledまたは completed.

を開く workflow.yamlファイルを開き config/packages/を開き、その内容を以下の例に置き換える:

framework:
    workflows:
        alerts:
            type: 'state_machine'
            supports:
                - App\Entity\Alert
            marking_store:
                type: 'method'
                property: 'status'
            initial_marking: new
            places:
                - new
                - raised
                - accepted
                - cancelled
                - completed
            transitions:
                raise:
                    from: [new]
                    to: raised
                accept:
                    from: [raised]
                    to: accepted
                cancel:
                    from: [raised, accepted]
                    to: cancelled
                complete:
                    from: [accepted]
                    to: completed

に関するすべてのAPIリクエストを処理するコントローラが必要になった。 Alerts.そこで、以下のコマンドを実行して、新しい AlertsApiController の作成を開始する:

php bin/console make:controller

コントローラ名を聞かれたら AlertsApiController.このコマンドは AlertsApiController.phpファイルを作成します。 src/Controllers.この新しいファイルを開いてください。

以下のクラスはすべて使用するので、最初からインクルードしておきましょう。ファイルの一番上の namespace App\Controller;のすぐ下に以下を追加します:

use App\Entity\Alert;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Workflow\Registry;

以下の例のように、クラスベースのルーティングを追加して、このクラス内のすべてのルーティングの先頭に /api/alerts/:

/** 
 * @Route("/api/alerts")
 */
class AlertsApiController extends AbstractController
{

このコントローラーは $workflowRegistry$entityManagerをこのクラスのあちこちで使用するので、 コードを何カ所も書き換えるのを避けるために、 コンストラクタの内部にこれらを配置します。次のコードをクラスの先頭に追加します:

    /** Registry */
    private $workflowRegistry;

    /** EntityManagerInterface */
    private $entityManager;

    public function __construct(Registry $workflowRegistry, EntityManagerInterface $entityManager)
    {
        $this->workflowRegistry = $workflowRegistry;
        $this->entityManager = $entityManager;
    }

コントローラの作成時に index()という関数が自動的に追加されました。このプロジェクトでは必要ないので、この関数を削除します。

次に listAction()を作成し、データベースからすべてのアラートを取得し、JSONレスポンスとして返します。以下のように listAction()をコントローラに追加します:

    /**
     * @Route("", methods={"GET"})
     */
    public function listAction(): JsonResponse
    {
        $data = $this->entityManager
            ->getRepository(Alert::class)
            ->findAll();

        $alerts = [];

        foreach ($data as $alert) {
            $alerts[] = $alert->toArray();
        }

        return new JsonResponse(
            $alerts, 
            JsonResponse::HTTP_OK
        );
    }

次に readAction()を作成し、データベースからIDでアラートを取得し、JSONレスポンスとして返します。以下のように readAction()をコントローラに追加します:

    /**
     * @Route("/{id}", methods={"GET"})
     */
    public function readAction(int $id): JsonResponse
    {
        $alert = $this->entityManager
            ->getRepository(Alert::class)
            ->findOneById($id);
        
        if (!$alert) {
            return new JsonResponse(
                null,
                JsonResponse::HTTP_NOT_FOUND
            );
        }

        return new JsonResponse(
            $alert->toArray(), 
            JsonResponse::HTTP_OK
        );
    }

を作成します。 acceptAction()もしアラートが見つかれば、このアラートのステータスを pendingから accepted.レスポンスは、HTTPステータスコード200の空のJSONレスポンスになります。
以下のように acceptAction()をコントローラに追加します:

    /**
     * @Route("/{id}/accept", methods={"POST"})
     */
    public function acceptAction(int $id): JsonResponse
    {
        $alert = $this->entityManager
            ->getRepository(Alert::class)
            ->findOneById($id);

        if (!$alert) {
            return new JsonResponse(null, JsonResponse::HTTP_NOT_FOUND);
        }

        $workflow = $this->workflowRegistry->get($alert);

        try {
            $workflow->apply($alert, 'accept');

            $this->entityManager->flush();
        } catch (LogicException $exception) {
            return new JsonResponse(['message' => $exception->getMessage()], 400);
        }

        return new JsonResponse([], 200);
    }

次に completeAction()もしアラートが見つかれば、このアラートのステータスを acceptedから completed.レスポンスは、HTTPステータスコード200の空のJSONレスポンスになります。
以下のように completeAction()をコントローラに追加します:

    /**
     * @Route("/{id}/complete", methods={"POST"})
     */
    public function completeAction(int $id): JsonResponse
    {
        $alert = $this->entityManager
            ->getRepository(Alert::class)
            ->findOneById($id);

        if (!$alert) {
            return new JsonResponse(null, JsonResponse::HTTP_NOT_FOUND);
        }

        $workflow = $this->workflowRegistry->get($alert);

        try {
            $workflow->apply($alert, 'complete');

            $this->entityManager->flush();
        } catch (LogicException $exception) {
            return new JsonResponse(['message' => $exception->getMessage()], 400);
        }

        return new JsonResponse([], 200);
    }

最後に cancelAction()もしアラートが見つかれば、このアラートのステータスを acceptedまたは pendingから cancelled.レスポンスは、HTTPステータスコード200の空のJSONレスポンスになります。
以下のように cancelAction()をコントローラに追加します:

    /**
     * @Route("/{id}/cancel", methods={"POST"})
     */
    public function cancelAction(int $id): JsonResponse
    {
        $alert = $this->entityManager
            ->getRepository(Alert::class)
            ->findOneById($id);

        if (!$alert) {
            return new JsonResponse(null, JsonResponse::HTTP_NOT_FOUND);
        }

        $workflow = $this->workflowRegistry->get($alert);

        try {
            $workflow->apply($alert, 'cancel');

            $this->entityManager->flush();
        } catch (LogicException $exception) {
            return new JsonResponse(['message' => $exception->getMessage()], 400);
        }

        return new JsonResponse([], 200);
    }

要約すると、アラートのライフサイクルの流れを制御する設定をプロジェクトに追加しました。そして、アラートのリストを取得し、特定のアラートを取得し、そのステータスに応じてアラートを受け入れ、拒否し、キャンセルし、完了させることができるAPIコントローラを作成しました。

エスカレーション・コマンドの作成

SMSが届かなかったら?それとも無視されたのでしょうか?ご心配なく!次のステップは、時間ベースのジョブスケジューラ(Cronジョブ)として実行され、10分以上前のすべてのアラートをエスカレーションするsymfonyコマンドを実装することです。

このコマンドを作成する前に、エスカレーションが必要なアラートを取得するためのリポジトリ・メソッドを追加する必要がある。ファイルを UserAlertRepository.phpファイルを API/src/Repository/.

このファイルの一番上に、インポートするためのサードパーティ・ライブラリをいくつか追加する:

use App\Entity\Alert;
use App\Entity\UserAlert;
use Carbon\Carbon;

次に、10分以上前にSMSが送信されたが、まだ以下のステータスのすべてのアラートを取得するために、リポジトリメソッドを追加します。 raised:

    public function findRaisedUserAlerts()
    {
        $queryBuilder = $this->createQueryBuilder('ua');
        $lastAlertSent = (Carbon::now())
            ->sub('10 minutes');

        return $queryBuilder
            ->join(Alert::class, 'a', Join::WITH, $queryBuilder->expr()->andX(
                $queryBuilder->expr()->eq('a', 'ua.alert'),
                $queryBuilder->expr()->eq('a.status', ':alertStatus')
            ))
            ->where($queryBuilder->expr()->isNull('ua.voiceSentAt'))
            ->andWhere($queryBuilder->expr()->lte('ua.smsSentAt', ':smsSentAt'))
            ->setParameter('alertStatus', 'raised')
            ->setParameter('smsSentAt', $lastAlertSent->format('Y-m-d H:i:s'))
            ->getQuery()
            ->getResult();
    }

この新しい symfony コマンドは検索されたすべてのアラートをエスカレーションします。このコマンドを作成するには、ターミナルで次のコマンドを実行します:

php bin/console make:command

コマンド名を聞かれたら app:escalate-alertと入力すると EscalateAlertCommand.phpという新しいファイルが作成されます。 API/src/Command.この新しいファイルを開く。

以下のクラスはすべて使用するので、最初からインクルードしておきましょう。ファイルの一番上の namespace App\Command;のすぐ下に以下を追加します:

use App\Entity\UserAlert;
use App\Util\VonageUtil;
use Carbon\Carbon;
use Doctrine\ORM\EntityManagerInterface;

このクラスには2つのオブジェクトを注入する必要があります。 VonageUtilEntityManagerInterface.symfony において、これを行うもっとも簡単な方法はコンストラクタです。クラスの先頭に次の機能を追加します:

    /** @var VonageUtil */
    protected $vonageUtil;

    /** @var EntityManagerInterface */
    private $entityManager;

    public function __construct(
        VonageUtil $vonageUtil,
        EntityManagerInterface $entityManager
    ) {
        $this->vonageUtil = $vonageUtil;
        $this->entityManager = $entityManager;

        parent::__construct();
    }

さて、このコマンドの機能を書く番だ。このコマンドは、10分以上前に送信されたSMSを持つすべてのアラートを検索します。 raisedステータスを持つすべてのアラートを検索します。これらのアラートがあれば、そのアラートに割り当てられているユーザーを検索し、音声合成による音声通話通知を送信します。現在の機能を protected function execute()で置き換えてください:

        $io = new SymfonyStyle($input, $output);

        $userAlertRepository = $this->entityManager->getRepository(UserAlert::class);
        $userAlerts = $userAlertRepository->findRaiseduserAlerts();

        if (!$userAlerts) {
            $io->warning('There are no alerts needing to be raised.');
        }

        /** @var UserAlert $userAlert */
        foreach ($userAlerts as $userAlert) {
            $this->vonageUtil->makePhoneCall(
                $userAlert->getUser()->getPhoneNumber(),
                getenv('VONAGE_NUMBER'),
                'A new alert has been raised, please log into the mobile app to investigate.'
            );

            $userAlert->setVoiceSentAt(Carbon::now());
            $this->entityManager->flush();
        }

        return Command::SUCCESS;

APIのテスト

これらの新しいエンドポイントをテストするには、ユーザーの認証が必要です。
まず、JWTトークンを取得するために POSTリクエストを http://localhost:8080/api/login_checkにリクエストを送信して、JWT トークンを取得してください。

JWTをコピーしたら、タイプを GETリクエストに更新し、URLを http://localhost:8080/api/alerts.ヘッダーにキー Authorisationと値を Bearer <JWT><JWT>をトークンに置き換えてください。

以下のPostmanの例で見ることができる:

An example of listing alerts through Postman

そのアラートは現在の状態のままにしておいて、後でモバイル・アプリケーションをテストするときに使おう。

APIを構築したら、次はモバイル・アプリケーションを作る番だ。

モバイルアプリの構築

更新 config.jsonここで APIURLの値は、以前に保存したngrok URLです。

新しいターミナル・ウィンドウを開き、以下のコマンドを実行する:

cd MobileApp npm install expo start

しばらくすると、ウェブブラウザが開く。左側には、モバイルデバイス、iOSシミュレーター、Androidシミュレーターのいずれでも、アプリケーションを実行するための複数のオプションがあります。自分に合ったオプションを選択し、アプリケーションが起動すると、ログイン画面が最初に表示されます。

データベースに登録されている固定ユーザーのクレデンシャルは以下の通り:

username: dev+1@company.com
password: test_pass

下の画像のように:

Example of a login screen on a mobile phone

ログインに成功しても、今のところ何もできません!しかし、ログインが正しいかどうか再確認するには、ターミナルで expo start.という行が表示されるはずです: You Successfully logged in!.

アラートAPI

アラートリストの表示

ディレクトリ内に APIディレクトリ内に alerts.js.
このファイルをインポートして client.jsファイルをインポートして getClient().
この新しい関数 getAlerts()は、エンドポイント /api/alerts.ここで、他のAPIコール、accept、complete、cancel alertを追加することができる。

import { getClient } from "./client.js";

export function getAlerts() {
  return getClient()
    .then(function(client) {
      return client.get("/api/alerts");
    });
};

export function acceptAlert(alertId) {
  return getClient()
    .then(function(client) {
      return client.post(`/api/alerts/${alertId}/accept`);
    });
};

export function cancelAlert(alertId) {
  return getClient()
    .then(function(client) {
      return client.post(`/api/alerts/${alertId}/cancel`);
    });
};

export function completeAlert(alertId) {
  return getClient()
    .then(function(client) {
      return client.post(`/api/alerts/${alertId}/complete`);
    })
};

アラートを受け取る機能ができたので、AlertsScreenコンポーネントをビルドする。新しいファイルを components/という名前の AlertsScreen.js.

import React, { Component } from 'react'
import { FlatList, Text, View, StyleSheet, StatusBar } from 'react-native'
import { TouchableOpacity } from 'react-native-gesture-handler';
import { getAlerts } from '../api/alerts.js'

class AlertsScreen extends Component {

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: StatusBar.currentHeight || 0,
  },
  header: {
    backgroundColor: '#03A5C9',
    padding: 10,
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
  },
  body: {
    padding: 10,
    borderBottomLeftRadius: 20,
    borderBottomRightRadius: 20,
  },
  item: {
    marginVertical: 8,
    marginHorizontal: 16,
    paddingBottom: 10,
    borderWidth: 1,
    borderRadius: 20
  },
  title: {
    fontSize: 24,
  },
  incidentId: {
    textAlign: 'right'
  }
});

export default AlertsScreen;

これで空の AlertsScreenクラスとスタイルができました。このクラスに追加して、何かを表示してみよう:

  state = {
    alerts: []
  }

  renderItem = ({ item }) => (
    <View style={styles.item}>
      <TouchableOpacity onPress={() => this.onPress(item)}>
        <View style={styles.header}>
          <Text style={styles.title}>
            {item.title}
          </Text>
        </View>
        <View style={styles.body}>
          <Text>
            {item.dateRaised}
          </Text>
          <Text>
            {item.assigned !== '' ? item.assigned : 'Unassigned'}
          </Text>
          <Text style={styles.incidentId}>
            #{item.incidentId}
          </Text>
        </View>
      </TouchableOpacity>
    </View>
  );

  render() {
    return (
      <View>
        <FlatList
          data={this.state.alerts}
          renderItem={this.renderItem}
          keyExtractor={item => item.id}
        />
      </View>
    );
  }

よし、これでページが表示された。しかし、何の情報も取得していないし、次に何をすべきかも教えてくれない!

メソッドの上に renderItem()メソッドの上に、以下を加える:

  componentDidMount() {
    getAlerts()
      .then(response => {
        return response.data.map(alert => ({
          id: `${alert.id}`,
          title: `${alert.title}`,
          description: `${alert.description}`,
          dateRaised: `${alert.dateRaised}`,
          assigned: `${alert.assigned}`,
          incidentId: `${alert.incidentId}`,
          status: `${alert.status}`
        }))
      })
      .then(alerts => {
        this.setState({ alerts: alerts });
      })
      .catch((err) => console.log(err));
  }

  onPress = (item) => {
    return this.props.navigation.navigate('Alert', {
      alert: item,
    })
  }

特定のアラートを表示する

に新しいファイルを作成する。 componentsと呼ばれる AlertScreen.jsという新しいファイルを作成する。

import React, { Component } from 'react'
import { Text, View, ScrollView, StyleSheet, StatusBar, TouchableOpacity } from 'react-native'
import { acceptAlert, cancelAlert, completeAlert } from '../api/alerts.js'

class AlertScreen extends Component {
  state = {
    alert: {}
  }
  
  const = this.state.alert = this.props.route.params.alert;

  onPressComplete = () => {
    completeAlert(this.state.alert.id)
      .then(() => {
        this.setState({ alert: { ...this.state.alert, status: 'completed'} });
      })
      .catch((err) => console.log(err));
  }

  onPressCancel = () => {
    cancelAlert(this.state.alert.id)
      .then(() => {
        this.setState({ alert: { ...this.state.alert, status: 'cancelled'} });
      })
      .catch((err) => console.log(err));
  }

  onPressAccept = () => {
    acceptAlert(this.state.alert.id)
      .then(() => {
        this.setState({ alert: { ...this.state.alert, status: 'accepted'} });
      })
      .catch((err) => console.log(err));
  }

  render() {
    let buttons;

    if (this.state.alert.status === 'raised') {
      buttons = <View style={styles.buttonContainer}>
          <View style={styles.buttonView}>
            <TouchableOpacity
              style={styles.button}
              onPress={() => this.onPressAccept()}
              underlayColor='#fff'>
              <Text style={styles.actionText}>Accept</Text>
            </TouchableOpacity>
          </View>
          <View style={styles.buttonView}>
            <TouchableOpacity
              style={styles.button}
              onPress={() => this.onPressCancel()}
              underlayColor='#fff'>
              <Text style={styles.actionText}>Cancel</Text>
            </TouchableOpacity>
          </View>
        </View>
    } else if (this.state.alert.status === 'accepted') {
      buttons = <View style={styles.buttonContainer}>
          <View style={styles.buttonView}>
            <TouchableOpacity
              style={styles.button}
              onPress={() => this.onPressComplete()}
              underlayColor='#fff'>
              <Text style={styles.actionText}>Complete</Text>
            </TouchableOpacity>
          </View>
          <View style={styles.buttonView}>
            <TouchableOpacity
              style={styles.button}
              onPress={() => this.onPressCancel()}
              underlayColor='#fff'>
              <Text style={styles.actionText}>Cancel</Text>
            </TouchableOpacity>
          </View>
        </View>
    }

    return (
      <View style={styles.item}>
        <View style={styles.header}>
          <Text style={styles.title}>
            {this.state.alert.title}
          </Text>
        </View>
        <View style={styles.body}>
          <Text>
            Date Raised: {this.state.alert.raisedDate}
          </Text>
          <Text>
            Assignee: {this.state.alert.assigned !== '' ? this.state.alert.assigned : 'Unassigned'}
          </Text>
          <Text style={styles.incidentId}>
            Incident ID: #{this.state.alert.incidentId}
          </Text>
          <Text style={styles.status}>
            Status: {this.state.alert.status}
          </Text>
        </View>
        {buttons}
        <View style={styles.scrollView}>
          <ScrollView>
            <Text style={styles.text}>
              {this.state.alert.description}
            </Text>
          </ScrollView>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: StatusBar.currentHeight || 0,
  },
  header: {
    backgroundColor: '#03A5C9',
    padding: 10,
  },
  body: {
    padding: 10,
  },
  item: {
    paddingBottom: 10,
  },
  title: {
    fontSize: 24,
  },
  buttonContainer: {
    flex: 1,
    flexDirection: "row",
    alignItems: 'center',
    justifyContent: 'center',
    paddingBottom: 30
  },
  buttonView: {
    flex: 1,
    height: 10
  },
  button: {
    marginRight: 40,
    marginLeft: 40,
    marginTop: 10,
    paddingTop: 10,
    paddingBottom: 10,
    backgroundColor: '#1E6738',
    borderRadius: 10,
    borderWidth: 1,
    borderColor: '#fff'
  },
  actionText: {
      color: '#fff',
      textAlign: 'center',
      paddingLeft: 10,
      paddingRight: 10
  },
  text: {
    fontSize: 20,
  },
});

export default AlertScreen;

あなたのアプリケーションは現在、あなたが作成したこれら2つの新しい画面を表示する方法について何も指示していません。以下の navigation/MainStackNavigator.js以下の import Loginに以下の2行を追加してください:

import Alert from '../components/AlertScreen';
import Alerts from '../components/AlertsScreen';

次に、ログイン Stack.Screenの下に、2つの新しい画面を追加します:

        <Stack.Screen 
          name='Alerts' 
          component={Alerts} 
          options={{ title: 'Alerts Screen' }}
        />
        <Stack.Screen
          name='Alert'
          component={Alert}
          options={({route, navigation}) => (
            {headerTitle: 'Alert Screen', 
            route: {route}, 
            navigation: {navigation}}
          )}
        />

ファイルに戻って LoginScreen.jsファイルに戻って console.log('You Successfully logged in!');と表示されている行を探し、ログイン成功時にユーザーをリダイレクトするために以下のスニペットを追加する。

return this.props.navigation.navigate('Alerts');

テスト

ターミナルでこのアプリをテストするには、次のディレクトリに移動したことを確認してください。 MobileAppディレクトリに移動したことを確認し、次のコマンドを実行する:

expo start

しばらくすると、ウェブブラウザが開きます。左側には、モバイル・デバイス、iOSシミュレーター、Androidシミュレーターなど、アプリケーションを実行するための複数のオプションがあります。自分に合ったオプションを選んでください。アプリケーションが起動すると、最初にログイン画面が表示されます。

データベースに登録されている固定ユーザーのクレデンシャルは以下の通り:

username: dev+1@company.com
password: test_pass

ログインに成功すると、次にアラート画面が表示されます。しかし、データベースにはアラートが存在しないため、今は空の状態です。

An example of raising an alert with Postman

モバイル・アプリケーションに再ログインしてください。新しいアラートが表示され、このアラートをクリックして詳細情報を表示する画面に移動することもできます。

また、このアラートを変更することもできます。

結論

このチュートリアルでは、Symfony という PHP フレームワークを使用して API を構築する方法を学びました。また、React Native を使ってモバイルアプリケーションを構築しました。Vonage の API を使って、SMS と音声合成で通知を送ることができました。これらすべてのアプリケーションを組み合わせることで、開発者やシステム管理者が何か問題が発生したときにアラートを受け取れるような、機能的なオンコール・アプリケーションを構築しました。ウェブフックを使うことで、オンコールシステムを複数のサービスと統合し、できるだけ多くのサービスをカバーできるようになりました。

以下は、Vonage Voice APIをプロジェクトに実装するために私たちが書いた他のチュートリアルです:

いつものように、コミュニティで共有したい質問、アドバイス、アイデアなどがあれば、遠慮なく私たちの コミュニティSlackワークスペース.このチュートリアルの進め方や、あなたのプロジェクトがどのように機能しているか、ぜひお聞かせください。

シェア:

https://a.storyblok.com/f/270183/250x250/b052219541/greg-holmes.png
Greg Holmesヴォネージの卒業生

元Vonage開発者エデュケーター。PHPのバックグラウンドを持つが、一つの言語に縛られることはない。熱心なゲーマーでRaspberry pi愛好家。屋内クライミング施設でボルダリングをしていることが多い。