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

Comment construire une application de garde avec React Native et Symfony.

Publié le March 2, 2021

Temps de lecture : 36 minutes

Êtes-vous un développeur ? Vous est-il déjà arrivé d'être de garde et de devoir installer l'une de ces applications gênantes qui vous avertissent dès que quelque chose ne va pas ? Le seuil d'erreurs a été dépassé ou le serveur met trop de temps à répondre, par exemple ? Si c'est le cas, vous êtes-vous déjà dit : "J'aimerais bien créer moi-même un de ces services" ? Eh bien, avec ce tutoriel, vous allez commencer à apprendre les bases de la création d'une de ces applications et de l'utilisation de Vonage pour effectuer les communications.

Ce tutoriel vous aidera à construire le début d'une API en PHP en utilisant Symfony et l'application mobile en utilisant React Native.

Le code complet de ce tutoriel peut être trouvé sur notre : Dépôt de la Communauté. Assurez-vous de vous connecter à la branche end-tutorial branche.

Conditions préalables

Pour réaliser ce tutoriel, vous aurez besoin des éléments suivants :

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.

Cloner le référentiel

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

Création de l'API

Générer une paire de clés JWT

Ce projet utilisera une application mobile construite en React Native.
Vous aurez besoin d'authentifier l'utilisateur entre l'application mobile et l'API. Ce projet utilise JWT pour gérer l'authentification, il faut donc générer des certificats pour créer les jetons JWT.
À la racine de votre projet, exécutez les trois commandes suivantes :

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

Exposer votre application à l'Internet

Pour passer un appel téléphonique avec Vonage, vous devez disposer d'un numéro de téléphone virtuel. Vous devrez également configurer un webhook pour enregistrer les événements qui se produisent chaque fois qu'un appel téléphonique est passé, répondu, rejeté ou terminé.
Pour ce tutoriel, ngrok est le service choisi pour exposer l'application à l'internet. Installez ngrok et exécutez la commande suivante dans une nouvelle fenêtre de terminal :

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

Veillez à copier l'URL HTTPS de ngrok, car vous en aurez besoin plus tard lors de la configuration du projet.

Variables d'environnement

Dans le répertoire Docker se trouve un fichier appelé .env.dist; copier ou renommer ce fichier en .env.

Les premiers champs à mettre à jour sont les identifiants de votre base de données. L'exemple ci-dessous montre les identifiants que j'ai utilisés pour ce tutoriel, mais je vous invite à utiliser des identifiants plus sûrs.

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

Mettre à jour les valeurs de VONAGE_API_KEY= et VONAGE_API_SECRET=que vous trouverez dans le tableau de bord du développeur Vonage.

Ensuite, dans le tableau de bord, naviguez vers "Vos Applications". Créez une nouvelle application, en veillant à télécharger le fichier private.key dans le répertoire racine du projet et en veillant à ce que votre application soit dotée de capacités vocales.

Vous devez définir l'URL du webhook de l'événement lorsque vous utilisez l'API Voice. Définissez-la sur l'URL HTTPS de ngrok que vous avez copiée dans la dernière section.

Mettre à jour les deux suivants :

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

Ensuite, liez votre numéro virtuel Vonage acheté précédemment à votre application. Ensuite, dans votre code, mettez à jour ce qui suit à l'intérieur de votre fichier .env à l'intérieur de Docker:

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

Enfin, trouvez ON_CALL_NUMBER= dans le même fichier, et ajoutez votre numéro de téléphone à cette valeur. Il devra s'agir d'un numéro réel, capable de recevoir des SMS et des appels vocaux.

Démarrer Docker

Exécutez les cinq commandes suivantes - les commentaires à droite de chacune d'elles décrivent ce qu'elles font :

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`

Il est temps de créer l'API !

Créer des entités de base de données

Ce projet comporte trois nouvelles tables de base de données. Alerts, OnCallet une table pour relier les alertes et les utilisateurs, UserAlerts.
Pour commencer, exécutez la commande ci-dessous et suivez les instructions de saisie ci-dessous :

php bin/console make:entity

Pour chaque champ, veuillez ajouter les éléments suivants :

  • Nom de la classe : Alert

  • Nom de la propriété : title (String, 255, Not null)

  • Nom de la propriété : description (String, 255, Not null)

  • Nom de la propriété : status (String, 255, Not null)

Lorsque la commande est terminée, ouvrez le nouveau fichier : src/Entity/Alert.php

Trois autres classes sont utilisées dans ce nouveau fichier Entité. Ajoutez ces importations au début du fichier :

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

L'une de ces nouvelles classes est la TimestampableEntity, qui ajoute les éléments suivants created_at et updated_at à la base de données. Ajouter use TimestampableEntity; en haut de la classe, comme indiqué ci-dessous :

class Alert
{
    use TimestampableEntity;

Nous devons ajouter des valeurs par défaut dans la classe, donc créer une nouvelle construction et définir les valeurs par défaut comme indiqué ci-dessous :

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

Pendant que nous sommes dans cette classe, ajoutez les deux fonctions ci-dessous.
La fonction getUserAssigned() détermine quel utilisateur est l'utilisateur actuel responsable de l'alerte. La seconde fonction, toArray()convertit les valeurs de la classe en un tableau, prêt à recevoir les réponses de l'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()
        ];
    }

Pour créer l'entité OnCall que nous utilisons pour enregistrer la personne qui est de garde chaque semaine, exécutez la commande ci-dessous et suivez les instructions de saisie telles qu'elles sont indiquées :

php bin/console make:entity

Pour chaque champ, veuillez ajouter les éléments suivants :

  • Nom de la classe : OnCall

  • Nom de la propriété : user (relation, User, ManyToOne, Not null, Add Property to User Yes)

  • Nom de la propriété : startDate (datetime, Not null)

  • Nom de la propriété : endDate (datetime, Not null)

Lorsque la commande est terminée, ouvrez le nouveau fichier : src/Entity/OnCall.php

Il y a une autre classe utilisée dans ce nouveau fichier d'entité. Ajoutez cette importation au début du fichier :

use Gedmo\Timestampable\Traits\TimestampableEntity;

L'une de ces nouvelles classes est la TimestampableEntity, qui ajoute les éléments suivants created_at et updated_at à la base de données, ajoutez use TimestampableEntity; en haut de la classe, comme indiqué ci-dessous :

class OnCall
{
    use TimestampableEntity;

Nous devons ajouter des valeurs par défaut dans la classe, donc créer une nouvelle construction et définir les valeurs par défaut comme indiqué ci-dessous :

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

Pour relier les entités User et Alert, vous devez créer une nouvelle entité appelée UserAlert. Suivez les instructions ci-dessous :

php bin/console make:entity
  • Nom de la classe : UserAlert

  • Nom de la propriété : user (relation, User, ManyToOne, Not null, Add Property to User Yes)

  • Nom de la propriété : alert (relation, Alert, ManyToOne, Not null, Add Property to Alert yes)

  • Nom de la propriété : smsSentAt (datetime, null)

  • Nom de la propriété : voiceSentAt (datetime, null)

Lorsque la commande est terminée, ouvrez le nouveau fichier : src/Entity/UserAlert.php

Il y a une autre classe utilisée dans ce nouveau fichier d'entité. Ajoutez cette importation au début du fichier :

use Gedmo\Timestampable\Traits\TimestampableEntity;

L'une de ces nouvelles classes est la TimestampableEntity, qui ajoute les éléments suivants created_at et updated_at à la base de données, ajoutez use TimestampableEntity; en haut de la classe, comme indiqué ci-dessous :

class UserAlert
{
    use TimestampableEntity;

Nous devons ajouter des valeurs par défaut dans la classe, donc créer une nouvelle construction et définir les valeurs par défaut comme indiqué ci-dessous :

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

Exécutez les migrations !

Il est maintenant temps d'effectuer et d'exécuter les migrations, en créant de nouvelles tables et colonnes dans votre base de données pour refléter les entités nouvellement créées.

Dans votre terminal, exécutez :

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

Faire des mélanges de données

Nous devons définir des paramètres prédéfinis pour la table de la base de données afin de déterminer qui est d'astreinte à un moment donné. OnCall afin de déterminer qui est d'astreinte à un moment donné. Pour ce faire, exécutez la commande suivante et suivez les instructions indiquées :

php bin/console make:fixture

En entrant le nom OnCallFixtures créera un fichier à l'intérieur de API/src/DataFixtures appelé OnCallFixtures.php. Remplacez le contenu de ce fichier par ce qui suit :

<?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,
        ];
    }
}

Exécutons vos fixtures pour que nous ayons un utilisateur et une fiche d'astreinte ! Dans votre terminal, exécutez :

php bin/console doctrine:fixtures:load

Faire un formulaire

Lorsque nous traitons une requête API pour lever une alerte, nous devons valider l'entrée pour nous assurer qu'elle correspond à ce que nous attendons. Avec Symfony, la façon la plus simple de le faire est d'utiliser un formulaire. Avec un formulaire, nous pouvons définir les valeurs que nous attendons et les contraintes sur ces valeurs. Commencez par exécuter la commande ci-dessous :

php bin/console make:form

Suivez les instructions, comme indiqué dans l'image :

Creating an Alert Form Type

Ouvrez maintenant le fichier AlertType.php nouvellement créé, qui se trouve à l'intérieur de src/Form/ et remplacez le contenu du fichier par :

<?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,
        ]);
    }
}

Le nouveau code que vous avez ajouté à la classe AlertType ajoute des contraintes et des exigences supplémentaires sur les deux champs de ce formulaire, title et descriptionpour s'assurer qu'ils ont une longueur minimale et qu'ils ne sont pas vides.

Construire un utilitaire Vonage

Une classe Utility est nécessaire pour traiter les demandes de l'API Voice lors de l'envoi de messages SMS et de l'émission d'appels vocaux.

En API/srccréez un nouveau répertoire appelé Utilainsi qu'un nouveau fichier dans ce nouveau répertoire appelé VonageUtil.php

Vous avez déjà stocké vos informations d'identification Vonage dans le fichier .env plus tôt dans ce tutoriel, et vous les utiliserez dans cette nouvelle classe PHP.

Dans le nouveau fichier, ajoutez le code suivant :

<?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;
    }
}

Pour l'instant, ce code initialise une nouvelle Classe PHP et crée un nouveau client pour l'API de Vonage, en utilisant le wrapper Symfony de Vonage pour le SDK PHP.

Ensuite, dans cette classe, vous allez vouloir ajouter deux nouvelles fonctions, qui s'occuperont de faire la demande à l'API pour envoyer un SMS ou passer un appel vocal. Ajoutez les deux fonctions suivantes :

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

Construire le contrôleur Webhook

Avant de créer le contrôleur, nous allons avoir besoin d'une fonction Repository pour extraire des données spécifiques de la base de données. Ouvrez la fonction OnCallRepository.php qui se trouve à l'intérieur de src/Repository. A l'intérieur de la classe sous la fonction __construct() ajoutez la nouvelle fonction findCurrentOnCall qui trouvera l'utilisateur actuel lors de l'appel.

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

Nous avons créé la fonctionnalité permettant d'extraire les données. Ensuite, nous allons créer un contrôleur pour gérer les requêtes et extraire les données.
Tout d'abord, dans votre terminal, exécutez la commande suivante :

php bin/console make:controller

Lorsqu'on vous demande le nom de votre contrôleur, entrez WebhookController.

Ouvrez le fichier nouvellement créé : API/src/Controller/WebhookController.php.

Nous utiliserons toutes les classes suivantes, alors assurons-nous de les inclure dès le début. En haut du fichier, juste en dessous de namespace App\Controller; ajoutez ce qui suit :

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;

Votre classe a besoin d'une construction pour que Symfony injecte les classes EntityManager et VonageUtil. Au début de votre classe, ajoutez :

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

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

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

Remplacez maintenant la fonction index() par le code ci-dessous pour créer de nouvelles alertes. Cette nouvelle fonction traite le corps de la requête POST, crée ces données en tant que nouvelles alertes, et passe cette alerte dans le formulaire pour valider les valeurs. Alertet passe cette alerte dans le formulaire pour valider les valeurs. Si tout se passe comme prévu, elle créera alors une nouvelle alerte UserAlertavec la personne actuellement en ligne comme destinataire de l'alerte.

    /**
     * @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);
    }

Vous avez peut-être remarqué que la fonction $this->getErrorMessages() est appelée en bas, mais votre classe ne l'a pas encore. Vous devrez ajouter cette fonction ensuite. Elle récupérera toutes les erreurs de formulaire trouvées lorsque le point final est déclenché, mais certaines données sont manquantes. Sous votre méthode index() ajoutez ce qui suit :

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

Nous en sommes à un stade où nous pouvons le tester maintenant !

Tester l'authentification

Il y a deux points de terminaison à cette partie du tutoriel que nous pouvons tester avec notre API, donc avec Docker toujours en cours d'exécution en arrière-plan, faites une POST à l'adresse http://localhost:8080/api/login_check avec le corps JSON de :

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

La réponse sera un objet JSON avec une clé tokenet la valeur est un jeton JWT.

L'image ci-dessous montre un exemple de cette opération avec Postman :

An example of authenticating with Postman

Test de déclenchement d'une alerte

Dans cet exemple, il n'est pas nécessaire de s'authentifier pour déclencher une alerte, et il n'est donc pas nécessaire d'utiliser le JWT de l'exemple précédent.

Pour déclencher une alerte, mettez à jour le champ URL : http://localhost:8080/webhooks/raise_alert, conservez la méthode en tant que POST et le corps JSON de :

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

La réponse sera un tableau vide et le code d'état HTTP 201 (créé). Vous pouvez voir un exemple de cette requête dans Postman dans l'image ci-dessous :

An example of raising an alert with Postman

Comment gérer une alerte

Le composant Workflow de Symfony vous permet de définir le cycle de vie de votre objet avec ses statuts. Chaque étape par laquelle votre objet peut passer est appelée une place, les transitions définissant l'action que l'objet doit entreprendre pour passer d'une place à l'autre.

Les flux de travail vous permettent de définir les endroits où votre alerte peut se trouver pour passer de l'état d'alerte à la dernière étape, qui est soit l'état d'alerte, soit l'état d'alerte. raised à la dernière étape, qui est soit cancelled ou completed.

Ouvrez le fichier workflow.yaml qui se trouve à l'intérieur de config/packages/ et remplacez le contenu par l'exemple ci-dessous :

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

Un contrôleur est maintenant nécessaire pour gérer toutes les demandes d'API concernant Alerts. Exécutez donc la commande ci-dessous pour commencer à créer notre nouveau contrôleur AlertsApiController :

php bin/console make:controller

Lorsqu'il vous demande un nom de contrôleur, soumettez AlertsApiController. Cette commande créera un nouveau fichier AlertsApiController.php dans le fichier src/Controllers. Ouvrez donc ce nouveau fichier.

Nous utiliserons toutes les classes suivantes, alors assurons-nous de les inclure dès le début. En haut du fichier, juste en dessous de namespace App\Controller; ajoutez ce qui suit :

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

Ajoutez votre routage basé sur la classe comme indiqué dans l'exemple ci-dessous, de sorte que tous les itinéraires au sein de cette classe soient préfixés par /api/alerts/:

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

Ce contrôleur utilisera les éléments $workflowRegistry et $entityManager à plusieurs endroits de cette classe, donc pour éviter de réécrire du code à plusieurs endroits, nous les placerons à l'intérieur de la construction. Ajoutez le code suivant au début de votre classe :

    /** Registry */
    private $workflowRegistry;

    /** EntityManagerInterface */
    private $entityManager;

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

Lors de la création du contrôleur, une fonction nommée index() a été automatiquement ajoutée. Nous n'en aurons pas besoin pour ce projet, alors supprimez cette fonction.

Nous allons maintenant créer notre listAction() qui récupérera toutes les alertes de la base de données et les renverra sous forme de réponse JSON. Ajoutez l'élément listAction() à votre contrôleur comme indiqué ci-dessous :

    /**
     * @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
        );
    }

Ensuite, nous allons créer readAction() pour récupérer une alerte par ID dans la base de données et la renvoyer sous forme de réponse JSON. Ajoutez readAction() à votre contrôleur comme indiqué ci-dessous :

    /**
     * @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
        );
    }

Nous allons créer notre acceptAction()qui recherchera une alerte par ID dans la base de données ; si elle est trouvée, elle essaiera de faire passer l'état de cette alerte de pending à accepted. La réponse sera une réponse JSON vide avec le code d'état HTTP 200.
Ajoutez l'élément acceptAction() à votre contrôleur comme indiqué ci-dessous :

    /**
     * @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);
    }

Ensuite, nous allons créer notre completeAction()qui recherchera une alerte par ID dans la base de données ; si elle est trouvée, elle essaiera de faire passer l'état de cette alerte de accepted à completed. La réponse sera une réponse JSON vide avec le code d'état HTTP 200.
Ajoutez l'élément completeAction() à votre contrôleur comme indiqué ci-dessous :

    /**
     * @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);
    }

Enfin, nous créerons notre cancelAction()qui recherchera une alerte par ID dans la base de données ; si elle est trouvée, elle essaiera de faire passer l'état de cette alerte de accepted ou pending à cancelled. La réponse sera une réponse JSON vide avec le code d'état HTTP 200.
Ajoutez l'élément cancelAction() à votre contrôleur comme indiqué ci-dessous :

    /**
     * @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);
    }

En résumé, nous avons ajouté une configuration à notre projet qui contrôle le flux de nos alertes tout au long de leur cycle de vie. Nous avons ensuite créé un contrôleur API qui nous permettra de récupérer une liste de nos alertes, de récupérer une alerte spécifique, d'accepter, de refuser, d'annuler ou de compléter les alertes en fonction de leur état.

Créer l'ordre d'escalade

Que faire si le SMS n'a pas été reçu ? Ou s'il est ignoré ?! Ne vous inquiétez pas ! L'étape suivante consiste à mettre en œuvre une commande Symfony qui s'exécutera comme un planificateur de tâches basé sur le temps (tâche Cron) et escaladera toutes les alertes datant de plus de 10 minutes.

Avant de créer cette commande, nous devons ajouter une méthode de dépôt pour récupérer les alertes nécessitant une escalade. Ouvrez votre fichier UserAlertRepository.php dans API/src/Repository/.

En haut de ce fichier, ajoutez d'autres bibliothèques tierces pour l'importation :

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

Ensuite, ajoutez la méthode de dépôt pour récupérer toutes les alertes pour lesquelles un SMS a été envoyé il y a plus de 10 minutes mais qui sont toujours dans l'état de 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();
    }

Cette nouvelle commande Symfony va escalader toutes les alertes récupérées. Pour la créer, lancez la commande suivante dans votre Terminal :

php bin/console make:command

Lorsqu'on vous demande le nom de la commande, entrez app:escalate-alertqui crée un nouveau fichier appelé EscalateAlertCommand.php dans API/src/Command. Ouvrez ce nouveau fichier.

Nous utiliserons toutes les classes suivantes, alors assurons-nous de les inclure dès le début. En haut du fichier, juste en dessous de namespace App\Command; ajoutez ce qui suit :

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

La classe a besoin de deux objets injectés, les objets VonageUtil et EntityManagerInterface. Avec Symfony, la façon la plus simple de le faire est de passer par le constructeur. Au début de votre classe, ajoutez la fonctionnalité suivante :

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

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

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

        parent::__construct();
    }

Il est maintenant temps d'écrire la fonctionnalité de cette commande. Elle récupérera toutes les alertes dont le SMS a été envoyé il y a plus de 10 minutes, mais qui ont toujours le statut raised statut. S'il y en a, elle récupérera l'utilisateur assigné à l'alerte et lui enverra une notification d'appel vocal en synthèse vocale. Remplacer la fonctionnalité actuelle dans protected function execute() par :

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

Tester l'API

Votre utilisateur doit s'authentifier pour tester ces nouveaux points d'accès.
Tout d'abord, assurez-vous d'obtenir votre jeton JWT en envoyant une POST à http://localhost:8080/api/login_check avec les informations d'identification de vos utilisateurs fixes.

Une fois que vous avez copié votre JWT, mettez à jour le type pour qu'il s'agisse d'une GET et l'URL en http://localhost:8080/api/alerts. Vous devez fournir un en-tête avec la clé Authorisation et la valeur comme Bearer <JWT> en remplaçant <JWT> par votre jeton.

Le point de terminaison List Alerts renvoie un tableau JSON, que vous pouvez voir dans l'exemple Postman ci-dessous :

An example of listing alerts through Postman

Gardons cette alerte dans son état actuel et utilisons-la plus tard lorsque nous testerons l'application mobile.

Vous avez construit une API ; il est maintenant temps de créer l'application mobile.

Créer l'application mobile

Mise à jour config.json où la valeur de l'élément APIURL est l'URL ngrok que vous avez sauvegardé précédemment.

Ouvrez une nouvelle fenêtre de Terminal et exécutez les commandes suivantes :

cd MobileApp npm install expo start

Au bout d'un moment, un navigateur web s'ouvre. Sur le côté gauche, il y a de multiples options pour exécuter l'application à travers, que ce soit sur votre appareil mobile, simulateur iOS ou simulateur Android. Choisissez l'option qui vous convient, et lorsque l'application démarre, l'écran de connexion sera le premier écran que vous verrez.

Les informations d'identification de l'utilisateur fixe dans la base de données sont les suivantes :

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

Comme le montre l'image ci-dessous :

Example of a login screen on a mobile phone

Une connexion réussie ne fait rien pour l'instant ! Nous devons d'abord implémenter plus d'écrans, mais pour vérifier que votre connexion a été correcte, vérifiez dans votre Terminal où vous avez exécuté expo start. Vous devriez voir la ligne : You Successfully logged in!.

API Alertes

Affichage d'une liste de signalements

Dans le répertoire API créer un nouveau fichier appelé alerts.js.
Ajoutez l'exemple ci-dessous, qui importe le fichier client.js afin d'utiliser les fonctionnalités du fichier getClient().
Cette nouvelle fonction appelée getAlerts() fait une demande à l'API sur le point de terminaison /api/alerts. Nous pouvons ajouter les autres appels API, accepter, compléter et annuler les alertes pendant que nous sommes ici.

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

Maintenant que nous disposons de la fonctionnalité permettant d'obtenir les alertes, construisons le composant AlertsScreen. Créez un nouveau fichier à l'intérieur de components/ appelé 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;

Nous avons maintenant une classe AlertsScreen vide et un peu de style. Ajoutons à cette classe pour montrer quelque chose :

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

Ok, ceci nous montre notre page. Mais elle ne récupère aucune information et ne nous dit pas ce qu'il faut faire ensuite !

Au-dessus de votre méthode renderItem() ajoutez ce qui suit :

  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,
    })
  }

Afficher une alerte spécifique

Créer un nouveau fichier dans components appelé AlertScreen.jsqui affiche l'alerte spécifique par ID.

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;

Votre application ne dispose actuellement d'aucune instruction sur la manière d'afficher les deux nouveaux écrans que vous avez créés. Dans le navigation/MainStackNavigator.js ci-dessous import Loginajoutez les deux lignes suivantes :

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

Ensuite, sous le Login Stack.Screenajoutez deux nouveaux écrans :

        <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}}
          )}
        />

De retour dans votre fichier LoginScreen.js trouvez la ligne indiquant : console.log('You Successfully logged in!'); et ajoutez l'extrait ci-dessous pour rediriger l'utilisateur en cas de connexion réussie.

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

Essais

Pour tester cette application dans votre Terminal, assurez-vous d'avoir navigué jusqu'au répertoire MobileApp et exécutez la commande suivante :

expo start

Au bout d'un certain temps, un navigateur web devrait s'ouvrir. Sur le côté gauche, il y a plusieurs options pour exécuter l'application à travers, que ce soit sur votre appareil mobile, simulateur iOS ou simulateur Android. Choisissez l'option qui vous convient. Lorsque l'application démarre, le premier écran que vous voyez est l'écran de connexion.

Les informations d'identification de l'utilisateur fixe dans la base de données sont les suivantes :

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

Une fois la connexion réussie, l'écran suivant est celui des alertes. Toutefois, il est vide pour l'instant car la base de données ne contient pas d'alertes.

An example of raising an alert with Postman

Maintenant, essayez à nouveau de vous connecter à votre application mobile. Vous verrez la nouvelle alerte et vous pourrez également cliquer sur cette alerte pour accéder à un écran contenant plus d'informations.

Vous pouvez également modifier cette alerte, qu'elle soit acceptée ou annulée.

Conclusion

Dans ce tutoriel, nous avons appris à construire une API à l'aide d'un framework PHP appelé Symfony. Nous avons également construit une application mobile à l'aide de React Native. Les API de Voice nous ont permis d'envoyer des notifications par SMS et des appels vocaux en mode texte. En appliquant tout cela ensemble, nous nous sommes construit une application d'astreinte fonctionnelle pour que les développeurs ou les administrateurs système soient alertés lorsque quelque chose ne va pas. Le fait d'avoir un webhook nous permet d'intégrer notre système d'astreinte à de multiples services afin de couvrir le plus grand nombre possible.

Vous trouverez ci-dessous quelques autres tutoriels que nous avons rédigés et qui mettent en œuvre l'API Voice de Vonage dans des projets :

Comme toujours, si vous avez des questions, des conseils ou des idées que vous souhaitez partager avec la communauté, n'hésitez pas à vous rendre sur notre espace de travail Slack de la communauté. J'aimerais savoir comment vous avez suivi ce tutoriel et comment fonctionne votre projet.

Partager:

https://a.storyblok.com/f/270183/250x250/b052219541/greg-holmes.png
Greg HolmesAnciens de Vonage

Ancien développeur éducateur @Vonage. Issu d'une formation PHP, mais pas limité à un seul langage. Joueur passionné et adepte du Raspberry pi. On le trouve souvent en train de faire du bloc dans des salles d'escalade.