
Compartir:
Antiguo educador de desarrolladores @Vonage. Procedente de PHP, pero no limitado a un solo lenguaje. Un ávido jugador y un entusiasta de Raspberry pi. A menudo se le encuentra practicando escalada en rocódromo.
Cómo crear una aplicación de guardia con React Native y Symfony
Tiempo de lectura: 36 minutos
¿Es usted desarrollador? ¿Alguna vez has estado de guardia y has tenido que instalar una de esas molestas aplicaciones que te avisan cada vez que algo va un poco mal? ¿Se ha superado el umbral de errores o el servidor tarda demasiado en responder, por ejemplo? Si es así, ¿alguna vez has pensado: "Me gustaría construir yo mismo uno de esos servicios?" Bueno, con este tutorial, estás a punto de comenzar con los conceptos básicos para construir una de estas aplicaciones y usar Vonage para realizar las comunicaciones.
Este tutorial le ayudará a construir el principio de una API en PHP usando Symfony y la aplicación móvil usando React Native.
El código completo de este tutorial se puede encontrar en nuestro: Repositorio de la Comunidad. Asegúrese de hacer checkout en la rama end-tutorial rama.
Requisitos previos
Para completar este tutorial necesitarás lo siguiente:
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.
Clonar el repositorio
Creación de la API
Generar par de claves JWT
Este proyecto utilizará una aplicación móvil construida en React Native.
Necesitarás autenticar al usuario entre la aplicación móvil y la API. Este proyecto utiliza JWT para manejar la autenticación, por lo que es necesario generar certificados para hacer los tokens JWT.
En la raíz de tu proyecto, ejecuta los siguientes tres comandos:
Exponer su aplicación a Internet
Realizar una llamada telefónica con Vonage requiere un número de teléfono virtual. También querrás configurar un webhook para registrar los eventos que ocurren cada vez que se realiza, responde, rechaza o finaliza una llamada telefónica.
Para este tutorial, ngrok es el servicio elegido para exponer la aplicación a Internet. Instala ngrok, y ejecuta el siguiente comando en una nueva ventana de Terminal:
Asegúrate de copiar la URL HTTPS de ngrok, ya que la necesitarás más adelante cuando configures el proyecto.
Variables de entorno
Dentro del directorio Docker hay un archivo llamado .env.distcopie o cambie el nombre de este archivo a .env.
Los primeros campos a actualizar son las credenciales de tu base de datos. El ejemplo de abajo muestra las credenciales que he usado para este tutorial, pero por favor usa unas más seguras.
Actualice los valores de VONAGE_API_KEY= y VONAGE_API_SECRET=que puedes encontrar dentro del Panel para desarrolladores de Vonage.
A continuación, en el panel de control, vaya a "Sus Applications". Cree una nueva aplicación, asegurándose de descargar el archivo private.key en el directorio raíz del proyecto y asegúrate de que tu aplicación tiene funciones de voz.
Tienes que establecer la URL del webhook de eventos cuando utilices la Voice API. Ajústala a la URL HTTPS de ngrok que copiaste en la última sección.
Actualiza los dos siguientes:
Luego, vincula tu número virtual de Vonage previamente adquirido a tu aplicación. Luego, en tu código, actualiza lo siguiente dentro de tu archivo .env archivo dentro de Docker:
Por último, busque ON_CALL_NUMBER= en el mismo archivo y añádele tu número de teléfono. Tendrá que ser un número real y capaz de recibir mensajes SMS y llamadas de voz.
Iniciar Docker
Ejecute los cinco comandos siguientes: los comentarios a la derecha de cada uno describen lo que hacen:
Es hora de construir la API
Crear entidades de base de datos
Hay tres nuevas tablas de base de datos para este proyecto. Alerts, OnCally una tabla para vincular las Alertas y los Usuarios, UserAlerts.
Para empezar, ejecute el comando que aparece a continuación y siga las instrucciones para la introducción de datos:
Para cada campo, añada lo siguiente:
Nombre de la clase: Alerta
Nombre de la propiedad: title (String, 255, Not null)
Nombre de la propiedad: descripción (String, 255, Not null)
Nombre de propiedad: status (String, 255, Not null)
Una vez completado el comando, abra el nuevo archivo: src/Entity/Alert.php
Hay otras tres clases utilizadas dentro de este nuevo archivo de Entidad. Añadir estas importaciones en la parte superior del archivo:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Gedmo\Timestampable\Traits\TimestampableEntity;Una de estas nuevas clases es la TimestampableEntity, que añade created_at y updated_at campos a la base de datos. Añade use TimestampableEntity; en la parte superior de la clase, como se muestra a continuación:
class Alert
{
use TimestampableEntity;Tenemos que añadir algunos valores por defecto dentro de la clase, por lo que crear una nueva construcción y por defecto los valores como se muestra a continuación:
public function __construct()
{
$this->status = 'raised';
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}Mientras estamos en esta clase, añade las dos funciones de abajo.
La función getUserAssigned() determina qué usuario es el responsable actual de la alerta. La segunda función, toArray()convierte los valores de la clase en un array, listo para las respuestas de la 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()
];
}Para crear la entidad OnCall que vamos a utilizar para almacenar qué persona está de guardia cada semana, ejecute el siguiente comando y siga las instrucciones de entrada que se indican:
Para cada campo, añada lo siguiente:
Nombre de la clase: OnCall
Nombre de propiedad: user (relation, User, ManyToOne, Not null, Add Property to User Yes)
Nombre de la propiedad: startDate (datetime, Not null)
Nombre de la propiedad: endDate (datetime, Not null)
Una vez completado el comando, abra el nuevo archivo: src/Entity/OnCall.php
Hay otra clase usada dentro de este nuevo archivo de Entidad. Añadir esta importación en la parte superior del archivo:
use Gedmo\Timestampable\Traits\TimestampableEntity;Una de estas nuevas clases es la TimestampableEntity, que añade created_at y updated_at a la base de datos, añade use TimestampableEntity; en la parte superior de la clase como se muestra a continuación:
class OnCall
{
use TimestampableEntity;Tenemos que añadir algunos valores por defecto dentro de la clase, por lo que crear una nueva construcción y por defecto los valores como se muestra a continuación:
public function __construct()
{
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}Para vincular las entidades Usuario y Alerta, debe crear una nueva entidad denominada UserAlert. Siga las siguientes instrucciones:
Nombre de la clase: UserAlert
Nombre de propiedad: user (relation, User, ManyToOne, Not null, Add Property to User Yes)
Nombre de propiedad: alert (relation, Alert, ManyToOne, Not null, Add Property to Alert yes)
Nombre de la propiedad: smsSentAt (datetime, null)
Nombre de la propiedad: voiceSentAt (datetime, null)
Una vez completado el comando, abra el nuevo archivo: src/Entity/UserAlert.php
Hay otra clase usada dentro de este nuevo archivo de Entidad. Añadir esta importación en la parte superior del archivo:
use Gedmo\Timestampable\Traits\TimestampableEntity;Una de estas nuevas clases es la TimestampableEntity, que añade created_at y updated_at a la base de datos, añade use TimestampableEntity; en la parte superior de la clase como se muestra a continuación:
class UserAlert
{
use TimestampableEntity;Tenemos que añadir algunos valores por defecto dentro de la clase, por lo que crear una nueva construcción y por defecto los valores como se muestra a continuación:
public function __construct()
{
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
} Ejecute las migraciones
Ahora es el momento de realizar y ejecutar las migraciones, creando nuevas tablas y columnas en su base de datos para reflejar estas entidades recién creadas.
En tu Terminal ejecuta:
Hacer DataFixtures
Tenemos que hacer algunos accesorios predefinidos para la OnCall para determinar quién está de guardia a una hora determinada. Para ello, ejecute el siguiente comando y siga las instrucciones que se indican:
Al introducir el nombre OnCallFixtures se creará un archivo dentro de API/src/DataFixtures llamado OnCallFixtures.php. Sustituya el contenido de este archivo por lo siguiente:
<?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,
];
}
}¡Vamos a ejecutar tus fixtures para que tengamos un usuario y un registro de guardia! En tu terminal ejecuta:
Hacer formulario
Cuando manejamos una petición API para lanzar una alerta, necesitamos validar la entrada para asegurarnos de que es lo que esperamos. Con Symfony, la forma más fácil de hacer esto es mediante el uso de un formulario. Con un formulario, podemos definir los valores que esperamos y las restricciones de estos valores. Comienza ejecutando el siguiente comando:
Siga las instrucciones, como se muestra en la imagen:

Ahora, abra el archivo AlertType.php que se encuentra dentro de src/Form/ y sustituye el contenido del archivo por
<?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,
]);
}
}El nuevo código que ha añadido a la clase AlertType añade más restricciones y requisitos a los dos campos de este formulario, title y descriptionpara garantizar que tienen una longitud mínima y que no están en blanco.
Construye una Util de Vonage
Se necesita una clase de utilidad para manejar las solicitudes de la API de Vonage al enviar mensajes SMS y realizar llamadas de voz.
En API/srccree un nuevo directorio llamado Utiljunto con un nuevo archivo dentro de este nuevo directorio llamado VonageUtil.php
Ya has almacenado tus credenciales de Vonage en el archivo .env anteriormente en este tutorial, y las utilizarás en esta nueva clase PHP.
En el nuevo archivo añada el siguiente código:
<?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;
}
}En este momento, este código inicializa una nueva clase PHP y crea un nuevo cliente para la API de Vonage, utilizando la envoltura Symfony de Vonage para el SDK de PHP.
A continuación, dentro de esta clase, vas a querer añadir dos nuevas funciones, que se encargarán de hacer la petición a la API para enviar un SMS o hacer una llamada de voz. Añade las dos siguientes:
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);
} Construir el controlador Webhook
Antes de hacer el controlador, vamos a necesitar una función Repository para extraer datos específicos de la base de datos. Abrimos la función OnCallRepository.php que se encuentra dentro de src/Repository. Dentro de la clase debajo de la función __construct() añade la nueva función findCurrentOnCall que encontrará al usuario actual en la llamada.
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();
}Hemos creado la funcionalidad para extraer los datos. A continuación, vamos a crear un controlador para manejar las solicitudes y extraer los datos.
Primero, en tu Terminal, ejecuta lo siguiente:
Cuando se le pregunte el nombre de su controlador, introduzca WebhookController.
Abra el archivo recién creado: API/src/Controller/WebhookController.php.
Vamos a utilizar todas las clases siguientes, así que asegurémonos de incluirlas desde el principio. En la parte superior del archivo, justo debajo de namespace App\Controller; añade lo siguiente:
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;Tu clase necesita una construcción para que Symfony inyecte las clases EntityManager y VonageUtil. En la parte superior de tu clase, añade:
/** @var VonageUtil */
protected $vonageUtil;
/** @var EntityManagerInterface */
private $entityManager;
public function __construct(
VonageUtil $vonageUtil,
EntityManagerInterface $entityManager
) {
$this->vonageUtil = $vonageUtil;
$this->entityManager = $entityManager;
}Ahora reemplace la función index() por el siguiente código para crear nuevas alertas. Esta nueva función maneja el cuerpo de la solicitud POST, crea estos datos como una nueva alerta Alerty pasa la alerta al formulario para validar los valores. Si todo es como se espera, se creará una nueva alerta UserAlertcon la persona que está recibiendo la alerta.
/**
* @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);
}Te habrás dado cuenta de que la función $this->getErrorMessages() es llamada en la parte inferior, pero tu clase aún no la tiene. Tendrás que añadir esta función a continuación. Recuperará todos los errores de formulario encontrados cuando se dispara el endpoint, pero faltan algunos datos. Debajo de tu método index() añade lo siguiente:
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;
}Ya podemos probarlo.
Probar la autenticación
Hay dos endpoints en esta parte del tutorial que podemos probar con nuestra API, así que con Docker aún ejecutándose en segundo plano, haz una POST petición a http://localhost:8080/api/login_check con el cuerpo JSON de:
{
"username": "dev+1@company.com",
"password": "test_pass"
}La respuesta será un objeto JSON con una clave tokeny el valor será un token JWT.
La siguiente imagen muestra un ejemplo de cómo hacerlo con Postman:

Prueba de activación de una alerta
En este ejemplo no es necesario estar autenticado para lanzar una alerta, por lo que no es necesario utilizar el JWT del ejemplo anterior.
Para lanzar una alerta, actualice el campo URL: http://localhost:8080/webhooks/raise_alert, mantenga el método como POST request, y el cuerpo JSON de:
{
"title": "ERRORRRRRR ASAP FIX NOW ITS BORKED",
"description": "THE PAGE AINT LOADING TOP PRIORITY FIX ASAP."
}La respuesta será un array vacío y el código de estado HTTP 201 (creado). Puedes ver un ejemplo de esta petición en Postman en la imagen inferior:

Cómo gestionar una alerta
El componente Workflow de Symfony te permite definir un ciclo de vida que tu objeto puede atravesar con sus estados. Cada paso por el que puede pasar tu objeto se denomina lugar, y las transiciones definen la acción que debe realizar el objeto para pasar de un lugar a otro.
Los flujos de trabajo le permitirán definir en qué lugares puede estar su alerta para pasar de ser raised hasta el último paso, que es cancelled o bien completed.
Abra el workflow.yaml que se encuentra en config/packages/ y sustituya el contenido por el ejemplo siguiente:
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: completedAhora se necesita un controlador para manejar todas las solicitudes de la API en relación con Alerts. Por lo tanto, ejecuta el siguiente comando para empezar a crear nuestro nuevo AlertsApiController:
Cuando se le pida el nombre del controlador, introduzca AlertsApiController. Este comando creará un nuevo archivo AlertsApiController.php archivo dentro de src/Controllers. Así que abra este nuevo archivo.
Vamos a utilizar todas las clases siguientes, así que asegurémonos de incluirlas desde el principio. En la parte superior del archivo, justo debajo de namespace App\Controller; añade lo siguiente:
use App\Entity\Alert;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Workflow\Registry;Añada su enrutamiento basado en clases como se muestra en el siguiente ejemplo, de modo que cualquier ruta dentro de esta clase lleve el prefijo /api/alerts/:
/**
* @Route("/api/alerts")
*/
class AlertsApiController extends AbstractController
{Este controlador utilizará los parámetros $workflowRegistry y $entityManager en varios lugares de esta clase, así que para evitar reescribir código en varios lugares, los colocaremos dentro de la construcción. Añade el siguiente código a la parte superior de tu clase:
/** Registry */
private $workflowRegistry;
/** EntityManagerInterface */
private $entityManager;
public function __construct(Registry $workflowRegistry, EntityManagerInterface $entityManager)
{
$this->workflowRegistry = $workflowRegistry;
$this->entityManager = $entityManager;
}Cuando se creó el controlador, se añadió automáticamente una función denominada index() fue añadida automáticamente. No la vamos a necesitar para este proyecto, así que elimina esa función.
Ahora crearemos nuestro listAction() que recuperará todas las alertas de la base de datos y las devolverá como respuesta JSON. Añade la directiva listAction() a tu controlador como se muestra a continuación:
/**
* @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
);
}A continuación, crearemos readAction() para recuperar una alerta por ID de la base de datos y devolverla como respuesta JSON. Añade readAction() a tu controlador como se muestra a continuación:
/**
* @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
);
}Crearemos nuestro acceptAction()que buscará una alerta por ID en la base de datos; si encuentra una, intentará pasar el estado de esta alerta de pending a accepted. La respuesta será una respuesta JSON vacía con el código de estado HTTP 200.
Añada la directiva acceptAction() a tu controlador como se muestra a continuación:
/**
* @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);
}A continuación, crearemos nuestro completeAction()que buscará una alerta por ID en la base de datos; si encuentra una, intentará cambiar el estado de esta alerta de accepted a completed. La respuesta será una respuesta JSON vacía con el código de estado HTTP 200.
Añada la directiva completeAction() a tu controlador como se muestra a continuación:
/**
* @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);
}Por último, crearemos nuestro cancelAction()que buscará una alerta por ID en la base de datos; si encuentra una, intentará cambiar el estado de esta alerta de accepted o pending a cancelled. La respuesta será una respuesta JSON vacía con el código de estado HTTP 200.
Añada la directiva cancelAction() a su controlador como se muestra a continuación:
/**
* @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 resumen, hemos añadido una configuración a nuestro proyecto que controla el flujo de nuestras alertas a través de su ciclo de vida. A continuación, hemos creado un controlador de API que nos permitirá recuperar una lista de nuestras alertas, recuperar una alerta específica, aceptar, rechazar, cancelar o completar las alertas en función de su estado.
Crear el comando de escalada
¿Y si el SMS no se ha recibido? ¿O es ignorado? Bueno, ¡no te preocupes! El siguiente paso es implementar un comando Symfony que se ejecutará como un programador de tareas basado en el tiempo (Cron job) y escalará todas las alertas con más de 10 minutos de antigüedad.
Antes de crear este comando, necesitaremos añadir un método de repositorio para recuperar las alertas que requieren escalado. Abra su archivo UserAlertRepository.php dentro de API/src/Repository/.
En la parte superior de este archivo, añadir algunas bibliotecas más de terceros para la importación:
use App\Entity\Alert;
use App\Entity\UserAlert;
use Carbon\Carbon;A continuación, añada el método de repositorio para recuperar todas las alertas a las que se haya enviado un SMS hace más de 10 minutos pero que aún se encuentren en el estado 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();
}Este nuevo comando de Symfony escalará todas las alertas recuperadas. Para crearlo, ejecuta el siguiente comando en tu Terminal:
Cuando se le pida el nombre del comando, introduzca app:escalate-alertque crea un nuevo archivo llamado EscalateAlertCommand.php dentro de API/src/Command. Abra este nuevo archivo.
Vamos a utilizar todas las clases siguientes, así que asegurémonos de incluirlas desde el principio. En la parte superior del archivo, justo debajo de namespace App\Command; añade lo siguiente:
use App\Entity\UserAlert;
use App\Util\VonageUtil;
use Carbon\Carbon;
use Doctrine\ORM\EntityManagerInterface;La clase necesita que se le inyecten dos objetos, el VonageUtil y EntityManagerInterface. Con Symfony, la forma más fácil de hacerlo es a través del constructor. En la parte superior de tu clase, añade la siguiente funcionalidad:
/** @var VonageUtil */
protected $vonageUtil;
/** @var EntityManagerInterface */
private $entityManager;
public function __construct(
VonageUtil $vonageUtil,
EntityManagerInterface $entityManager
) {
$this->vonageUtil = $vonageUtil;
$this->entityManager = $entityManager;
parent::__construct();
}Ahora es el momento de escribir la funcionalidad para este comando. Recuperará todas las Alertas con un SMS enviado hace más de 10 minutos, pero todavía con raised estado. Si hay alguna, recuperará el usuario asignado a la alerta y le enviará una notificación de llamada de voz de texto a voz. Sustituya la funcionalidad actual dentro de protected function execute() por:
$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; Probar la API
Tu usuario necesita autenticarse para probar estos nuevos endpoints.
En primer lugar, asegúrese de obtener su token JWT enviando una POST solicitud a http://localhost:8080/api/login_check con las credenciales de tu usuario fijo.
Una vez que hayas copiado tu JWT, actualiza el tipo para que sea una GET y la URL para que sea http://localhost:8080/api/alerts. Es necesario proporcionar un encabezado con la clave Authorisation y el valor como Bearer <JWT> sustituyendo <JWT> por tu token.
El endpoint list Alerts devuelve un array JSON, que puedes ver en el ejemplo de Postman a continuación:

Mantengamos esa alerta en su estado actual y utilicémosla más tarde al probar la aplicación móvil.
Ha creado una API; ahora es el momento de crear la aplicación móvil.
Construir la aplicación móvil
Actualización config.json donde el valor de APIURL es la URL ngrok que guardó anteriormente.
Abra una nueva ventana de Terminal y ejecute los siguientes comandos:
Al cabo de un rato, se abre un navegador web. En la parte izquierda, hay varias opciones para ejecutar la aplicación, ya sea en tu dispositivo móvil, simulador iOS o simulador Android. Elige la opción que más te convenga y, cuando la aplicación arranque, la pantalla de inicio de sesión será la primera que veas.
Las credenciales del usuario fijo en la base de datos son:
username: dev+1@company.com
password: test_passComo se muestra en la siguiente imagen:

Un inicio de sesión correcto no hará nada. Tenemos que implementar más pantallas primero, pero para comprobar que su inicio de sesión fue correcto, compruebe su Terminal donde ejecutó expo start. Deberías ver la línea : You Successfully logged in!.
API de alertas
Mostrar una lista de alertas
Dentro del directorio API cree un nuevo archivo llamado alerts.js.
Añade el siguiente ejemplo, que importa el archivo client.js para utilizar la funcionalidad de getClient().
Esta nueva función llamada getAlerts() realiza una petición a la API en el endpoint /api/alerts. Podemos añadir las otras llamadas a la API, aceptar, completar y cancelar alertas mientras estamos aquí.
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`);
})
};Ahora que tenemos la funcionalidad para obtener las alertas, construye el componente AlertsScreen. Crea un nuevo archivo dentro de components/ llamado 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;Ahora tenemos una clase AlertsScreen y algo de estilo. Vamos a añadir a esta clase para mostrar algo:
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>
);
}Bien, esto nos muestra nuestra página. Pero no recupera ninguna información y no nos dice qué hacer a continuación.
Encima de su método renderItem() añada lo siguiente:
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,
})
} Mostrar una alerta específica
Cree un nuevo archivo en components llamado AlertScreen.jsque muestra la alerta específica por 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;Actualmente tu aplicación no tiene instrucciones sobre cómo mostrar estas dos nuevas pantallas que has creado. En navigation/MainStackNavigator.js debajo de import Loginañade las dos líneas siguientes:
import Alert from '../components/AlertScreen';
import Alerts from '../components/AlertsScreen';A continuación, debajo de Login Stack.Screenañada dos nuevas pantallas:
<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}}
)}
/>En su archivo LoginScreen.js encuentre la línea console.log('You Successfully logged in!'); y añada el siguiente fragmento de código para redirigir al usuario en caso de inicio de sesión correcto.
return this.props.navigation.navigate('Alerts'); Pruebas
Para probar esta aplicación en tu Terminal, asegúrate de que has navegado hasta el directorio MobileApp y ejecuta el siguiente comando:
Al cabo de un rato, debería abrirse un navegador web. En la parte izquierda, hay varias opciones para ejecutar la aplicación, ya sea en tu dispositivo móvil, en el simulador de iOS o en el simulador de Android. Elige la opción que más te convenga. Cuando se inicie la aplicación, la primera pantalla que verá será la de inicio de sesión.
Las credenciales del usuario fijo en la base de datos son:
username: dev+1@company.com
password: test_passUna vez iniciada la sesión con éxito, la siguiente pantalla que verá es la de Alertas. Sin embargo, en este momento estará vacía porque, en la base de datos, no hay ninguna alerta.

Ahora, vuelve a intentar iniciar sesión en tu aplicación móvil. Verás la nueva alerta y también podrás hacer clic en ella para acceder a una pantalla con más información.
También puede transicionar esta alerta, ya sea para aceptarla o cancelarla.
Conclusión
En este tutorial, hemos aprendido a construir una API usando un framework PHP llamado Symfony. También hemos construido una aplicación móvil usando React Native. Las API de Vonage nos permitieron enviar notificaciones a través de SMS y llamadas de voz de texto a voz. Aplicando todo esto junto, nos hemos construido una aplicación de guardia funcional para que los desarrolladores o administradores de sistemas reciban alertas cuando algo va mal. Disponer de un webhook nos permite integrar nuestro sistema de guardia con múltiples servicios para cubrir el mayor número posible.
A continuación, encontrarás otros tutoriales que hemos escrito para implementar Voice API de Vonage en proyectos:
Como siempre, si tienes alguna pregunta, consejo o idea que quieras compartir con la comunidad, no dudes en entrar en nuestro espacio de trabajo espacio de trabajo comunitario Slack. Me encantaría saber cómo te ha ido con este tutorial y cómo funciona tu proyecto.
