
Partager:
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.
Comment construire une application de garde avec React Native et Symfony.
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
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 :
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 :
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.
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 :
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:
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
Suivez les instructions, comme indiqué dans l'image :

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 :
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 :

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 :

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: completedUn 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 :
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 :
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 :

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 :
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_passComme le montre l'image ci-dessous :

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 :
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_passUne 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.

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.
