Рубрики
Uncategorized

Создайте приложение по вызову с помощью React Native и Symfony

Вы разработчик? Вы когда-нибудь были на вызове и вам приходилось устанавливать одно из тех надоедливых приложений, которые… С тегами symfony, php, react native.

Вы разработчик? Вы когда-нибудь были на вызове и вам приходилось устанавливать одно из тех надоедливых приложений, которые уведомляют вас, когда что-то не так? Превышен порог для ошибок, или серверу требуется слишком много времени, чтобы дать ответы, например? Если да, то задумывались ли вы когда-нибудь: “Я бы хотел сам создать один из таких сервисов?” Что ж, с помощью этого урока вы начнете изучать основы создания одного из этих приложений и использования Vonage для выполнения связи.

Этот учебник поможет вам создать начало API на PHP с помощью Симфония и мобильное приложение, использующее Реагировать на Родной .

Полный код этого руководства можно найти на нашем сайте: Репозиторий сообщества . Обязательно загляните в ветку end-учебник .

Предпосылки

Для завершения этого урока вам понадобится следующее:

Учетная запись API Vonage

Для завершения этого урока вам понадобится учетная запись API Vonage . Если у вас его еще нет, вы можете зарегистрироваться сегодня и начать строительство с бесплатным кредитом. Как только у вас появится учетная запись, вы можете найти свой ключ API и секрет API в верхней части панели управления Vonage API .

В этом руководстве также используется виртуальный номер телефона. Чтобы приобрести его, перейдите в раздел Номера > Покупайте номера и ищите тот, который соответствует вашим потребностям.

Клонировать репозиторий

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

Создание API

Генерировать пару ключей JWT

В этом проекте будет использоваться мобильное приложение, встроенное в React Native. Вам нужно будет аутентифицировать пользователя между мобильным приложением и API. Этот проект использует JWT для обработки аутентификации, поэтому для создания токенов JWT необходимо сгенерировать сертификаты. В корневом каталоге вашего проекта выполните следующие три команды:

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

Предоставление доступа к Вашему приложению в Интернете

Для совершения телефонного звонка с помощью Vonage требуется виртуальный номер телефона. Вы также захотите настроить веб-браузер для регистрации событий, которые происходят всякий раз, когда телефонный звонок совершается, на него отвечают, он отклоняется или завершается. В этом учебном пособии ngrok является предпочтительным сервисом для доступа приложения к Интернету. Установите ngrok и выполните следующую команду в новом окне терминала:

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

Обязательно скопируйте свой URL-адрес HTTPS ngrok, так как он вам понадобится позже при настройке проекта.

Переменные среды

Внутри каталога Docker находится файл с именем .env.dist ; скопируйте или переименуйте этот файл в .env .

Первые поля, которые необходимо обновить, – это ваши учетные данные базы данных. В приведенном ниже примере показаны учетные данные, которые я использовал для этого урока, но, пожалуйста, используйте более безопасные.

DATABASE_URL=mysql://db_user:db_password@mysql:3306/on_call?serverVersion=8.0

MYSQL_DATABASE=on_call
MYSQL_USER=db_user
MYSQL_PASSWORD=db_password
MYSQL_ROOT_PASSWORD=root_password

Обновите значения для обоих ВОНАГЕ_АПИ_КЕЙ= и VONAGE_API_SECRET= , который вы можете найти на панели инструментов разработчика Vonage .

Затем на панели мониторинга перейдите в раздел “Ваши приложения”. Создайте новое приложение, обязательно загрузив файл private.key в корневой каталог проекта и убедившись, что ваше приложение имеет голосовые возможности.

При использовании голосового API необходимо задать URL-адрес веб-крючка события. Установите для этого значение URL-адреса HTTPS ngrok, который вы скопировали в последнем разделе.

Обновите следующие два:

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

Затем привяжите ранее приобретенный виртуальный номер Vonage к вашему приложению. Затем в вашем коде обновите следующее в вашем файле .env внутри Докер :

VONAGE_BRAND=OnCallAlerts
VONAGE_NUMBER=

JWT_PASSPHRASE=

Наконец, найдите ON_CALL_NUMBER= в том же файле и добавьте свой номер телефона к этому значению. Это должен быть реальный номер, способный принимать SMS-сообщения и голосовые вызовы.

Запустить окно настройки

Выполните следующие пять команд — комментарии справа от каждой описывают, что они делают:

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

Время для создания API!

Создание объектов Базы данных

Для этого проекта есть три новые таблицы базы данных. Оповещения , По вызову и таблица для связи оповещений и пользователей вместе, Оповещения пользователей . Для начала выполните приведенную ниже команду и следуйте приведенным ниже инструкциям по вводу:

php bin/console make:entity

Для каждого поля, пожалуйста, добавьте следующее:

  • Название класса: Тревога
  • Имя свойства: заголовок (Строка, 255, Не нуль)
  • Имя свойства: описание (Строка, 255, Не равно нулю)
  • Имя свойства: статус (Строка, 255, Не пустое значение)

Когда команда будет завершена, откройте новый файл: src/Entity/Alert.php

В этом новом файле сущностей используются три других класса. Добавьте эти импортные данные в верхней части файла:

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

Одним из таких новых классов является TimestampableEntity, который добавляет поля created_at и updated_at в базу данных. Добавьте используйте TimestampableEntity; в верхней части класса, как показано ниже:

class Alert
{
    use TimestampableEntity;

Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже:

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

Пока мы находимся в этом классе, добавьте две функции ниже. Функция Назначить пользователя() определяет, какой пользователь является текущим пользователем, ответственным за оповещение. Вторая функция, toArray() , преобразует значения класса в массив, готовый для ответов API.

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

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

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

Чтобы создать объект OnCall , который мы используем для хранения того, кто звонит каждую неделю, выполните приведенную ниже команду и следуйте инструкциям по вводу, как указано:

php bin/console make:entity

Для каждого поля, пожалуйста, добавьте следующее:

  • Имя класса: OnCall
  • Имя свойства: пользователь (отношение, Пользователь, ManyToOne, Не равно нулю, Добавить свойство пользователю Да)
  • Имя свойства: Дата начала (дата-время, не пустое)
  • Имя свойства: Дата окончания (дата-время, не пустое)

Когда команда будет завершена, откройте новый файл: src/Entity/OnCall.php

В этом новом файле сущностей используется еще один класс. Добавьте этот импорт в верхней части файла:

use Gedmo\Timestampable\Traits\TimestampableEntity;

Одним из таких новых классов является TimestampableEntity, который добавляет поля created_at и updated_at в базу данных, добавьте используйте TimestampableEntity; в верхней части класса, как показано ниже:

class OnCall
{
    use TimestampableEntity;

Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже:

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

Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже: Чтобы связать сущности User и Alert вместе, вам нужно создать новую сущность с именем UserAlert . Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже: Чтобы связать сущности User и Alert вместе, вам нужно создать новую сущность с именем

php bin/console make:entity
  • Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже: Чтобы связать сущности User и Alert вместе, вам нужно создать новую сущность с именем
  • UserAlert
  • . Следуйте приведенным ниже инструкциям: Имя класса: Предупреждение пользователя
  • Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже: Чтобы связать сущности User и Alert вместе, вам нужно создать новую сущность с именем
  • UserAlert

Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже: Чтобы связать сущности User и Alert вместе, вам нужно создать новую сущность с именем UserAlert

Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже: Чтобы связать сущности User и Alert вместе, вам нужно создать новую сущность с именем || UserAlert ||. Следуйте приведенным ниже инструкциям: Имя класса: Оповещение пользователя Имя свойства: пользователь (отношение, Пользователь, ManyToOne, Не равно нулю, Добавить свойство к пользователю Да) Имя свойства: оповещение (отношение, Предупреждение, ManyToOne, Не равно нулю, Добавить свойство в предупреждение да) Имя свойства: smsSentAt (До завершения команды откройте новый файл: Prope|В этом новом файле сущности используется еще один класс. | src/Entity/UserAlert.php имя rty: voiceSentAt (дата, время, ноль) время, ноль) Добавьте этот импорт в верхней части файла:

use Gedmo\Timestampable\Traits\TimestampableEntity;

Одним из таких новых классов является TimestampableEntity, который добавляет поля created_at и updated_at в базу данных, добавьте используйте TimestampableEntity; в верхней части класса, как показано ниже:

class UserAlert
{
    use TimestampableEntity;

Нам нужно добавить некоторые значения по умолчанию в класс, поэтому создайте новую конструкцию и установите значения по умолчанию, как показано ниже:

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

Запустите миграцию!

Теперь пришло время выполнить и выполнить миграцию, создав новые таблицы и столбцы в вашей базе данных, чтобы отразить эти вновь созданные сущности.

В вашем терминале выполняется:

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

Вносить исправления в данные

Нам нужно создать некоторые предопределенные параметры для таблицы базы данных По вызову , чтобы определить, кто находится на вызове в определенное время. Для этого выполните следующую команду и следуйте приведенным инструкциям:

php bin/console make:fixture

Ввод имени Приборы по вызову создаст файл внутри API/src/DataFixtures под названием OnCallFixtures.php . Замените содержимое этого файла следующим:

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

        $manager->persist($onCall);

        $manager->flush();
    }

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

Давайте запустим ваши устройства, чтобы у нас был пользователь и запись по вызову! В вашем терминале выполняется:

php bin/console doctrine:fixtures:load

Сделать Форму

При обработке запроса API для поднятия предупреждения нам необходимо проверить входные данные, чтобы убедиться, что они соответствуют нашим ожиданиям. В Symfony самый простой способ сделать это – использовать форму. С помощью формы мы можем определить, какие значения мы ожидаем, и любые ограничения на эти значения. Начните с выполнения приведенной ниже команды:

php bin/console make:form

Следуйте инструкциям, как показано на рисунке:

Теперь откройте только что созданный AlertType.php файл найден в src/форме/ и заменить содержимое файла на:

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

Новый код, который вы добавили в класс AlertType , добавляет дополнительные ограничения и требования к двум полям в этой форме, заголовок и описание , чтобы убедиться, что они имеют минимальную длину и не являются пустыми.

Постройте Vonage, используйте

Служебный класс необходим для обработки запросов API Vonage при отправке SMS-сообщений и совершении голосовых вызовов.

В API/src создайте новый каталог под названием Использование , вместе с новым файлом в этом новом каталоге под названием VonageUtil.php

Вы уже сохранили свои учетные данные Vonage в файле .env ранее в этом уроке, и вы будете использовать их в этом новом классе PHP.

В новый файл добавьте следующий код:

client = $client;
    }
}

Прямо сейчас этот код инициализирует новый класс PHP и создает новый клиент для API Vonage, используя оболочку Vonage Symfony для PHP SDK.

Затем в этом классе вам нужно будет добавить две новые функции, которые будут обрабатывать отправку запроса в API для отправки SMS или совершения голосового вызова. Добавьте следующие два:

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

        $message = $response->current();

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

        return false;
    }

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

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

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

Создайте контроллер Webhook

Прежде чем создавать контроллер, нам понадобится функция репозитория для извлечения определенных данных из базы данных. Откройте OnCallRepository.php найдено в src/Репозитории . Внутри класса под функцией __construct() добавьте новую функцию найти текущего пользователя по вызову , которая найдет текущего пользователя по вызову.

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

Мы создали функциональность для извлечения данных. Далее давайте создадим контроллер для обработки любых запросов и извлечения данных. Сначала в своем терминале выполните следующие действия:

php bin/console make:controller

Когда вас спросят имя вашего контроллера, введите WebhookКонтроллер .

Откройте только что созданный файл: API/src/Controller/WebhookController.php .

Мы будем использовать все следующие классы, поэтому давайте обязательно включим их с самого начала. В верхней части файла, чуть ниже Приложение пространства имен\Контроллер; добавьте следующее:

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

Вашему классу нужна конструкция для Symfony для внедрения классов EntityManager и VonageUtil. В верхней части вашего класса добавьте:

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

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

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

Теперь замените функцию index() приведенным ниже кодом для создания новых предупреждений. Эта новая функция обрабатывает тело запроса POST, создает эти данные как новый Предупреждение и передает это предупреждение в форму для проверки значений. Если все будет так, как ожидалось, он создаст новое Оповещение пользователя , в котором человек, находящийся в данный момент на связи, будет получать оповещение.

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

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

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

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

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

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

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

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

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

            $entityManager->flush();

            return new JsonResponse([], 201);
        }

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

Возможно, вы заметили, что функция $this->получать сообщения об ошибках() вызывается внизу, но в вашем классе ее еще нет. Затем вам нужно будет добавить эту функцию. Он восстановит все ошибки формы, обнаруженные при запуске конечной точки, но некоторые данные отсутствуют. Под вашим методом index() добавьте следующее:

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

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

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

        return $errors;
    }

Мы подошли к тому моменту, когда можем проверить это прямо сейчас!

Проверьте аутентификацию

В этой части руководства есть две конечные точки, которые мы можем протестировать с помощью нашего API, поэтому, если Docker все еще работает в фоновом режиме, сделайте запрос POST в http://localhost:8080/api/login_check с телом JSON из:

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

Ответом будет объект JSON с ключом токен , а значением будет токен JWT.

На изображении ниже показан пример выполнения этого с почтальоном:

Тест, поднимающий тревогу

В этом примере вам не нужно проходить аутентификацию, чтобы вызвать предупреждение, поэтому нет необходимости использовать JWT из предыдущего примера.

Чтобы вызвать оповещение, обновите поле URL: http://localhost:8080/webhooks/raise_alert , сохранить метод в виде запроса POST , а тело JSON:

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

Ответом будет пустой массив и код состояния HTTP 201 (создан). Вы можете увидеть пример этого запроса в Postman на изображении ниже:

Как справиться с предупреждением

Компонент Symphony Workflow позволяет определить жизненный цикл, который может пройти ваш объект, с его статусами. Каждый шаг, который может пройти ваш объект, называется местом, а переходы определяют действие, которое объект должен предпринять, чтобы добраться из одного места в другое.

Рабочие процессы позволят вам определить, в каких местах может находиться ваше оповещение, чтобы получить повышение к последнему шагу, который либо отменяется или завершено .

Откройте рабочий процесс.yaml файл, найденный в конфигурации/пакеты/ и заменить содержимое примером ниже:

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

Теперь необходим контроллер для обработки всех запросов API, касающихся Оповещения . Итак, выполните приведенную ниже команду, чтобы начать создавать наш новый контроллер Api оповещений:

php bin/console make:controller

Когда он запрашивает имя контроллера, отправьте Оповещения контроллеру Api . Эта команда создаст новый AlertsApiController.php файл внутри src/контроллеров . Так что откройте этот новый файл.

Мы будем использовать все следующие классы, поэтому давайте обязательно включим их с самого начала. В верхней части файла, чуть ниже Приложение пространства имен\Контроллер; добавьте следующее:

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

Добавьте маршрутизацию на основе класса, как показано в примере ниже, чтобы все маршруты в этом классе имели префикс /api/оповещения/ :

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

Этот контроллер будет использовать реестр $workflow и $EntityManager в нескольких местах этого класса, поэтому, чтобы избежать переписывания кода в нескольких местах, мы разместим их внутри конструкции. Добавьте следующий код в начало своего класса:

    /** Registry */
    private $workflowRegistry;

    /** EntityManagerInterface */
    private $entityManager;

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

При создании контроллера была автоматически добавлена функция с именем index() . Нам это не понадобится для этого проекта, так что удалите эту функцию.

Теперь мы создадим наше действие список() , которое будет извлекать все оповещения из базы данных и возвращать их в виде ответа JSON. Добавьте действие список() в свой контроллер, как показано ниже:

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

Далее мы создадим действие чтения() для извлечения одного предупреждения по идентификатору из базы данных и возврата его в виде ответа JSON. Добавьте действие чтения() к вашему контроллеру, как показано ниже:

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

Мы создадим наше действие принять() , которое найдет оповещение по идентификатору из базы данных; если оно будет найдено, оно попытается перевести статус этого оповещения с ожидающий на принято . Ответом будет пустой JSON-ответ с кодом состояния HTTP 200. Добавьте действие принять() в свой контроллер, как показано ниже:

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

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

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

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

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

        return new JsonResponse([], 200);
    }

Далее мы создадим наш completeAction() , который найдет оповещение по идентификатору из базы данных; если он будет найден, он попытается перевести статус этого оповещения с принято на завершено . Ответом будет пустой JSON-ответ с кодом состояния HTTP 200. Добавьте завершить действие() к вашему контроллеру, как показано ниже:

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

Наконец, мы создадим ваше действие отмена() , которое найдет оповещение по идентификатору из базы данных; если оно будет найдено, оно попытается перевести статус этого оповещения из принято или в ожидании до отменено . Ответом будет пустой JSON-ответ с кодом состояния HTTP 200. Добавьте действие отмены() в свой контроллер, как показано ниже:

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

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

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

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

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

        return new JsonResponse([], 200);
    }

Подводя итог, мы добавили в наш проект конфигурацию, которая управляет потоком оповещений на протяжении всего жизненного цикла. Затем мы создали контроллер API, который позволит нам получать список наших предупреждений, извлекать конкретное предупреждение, принимать, отклонять, отменять или завершать оповещения в зависимости от их статуса.

Создайте команду эскалации

Что делать, если SMS не было получено? Или это игнорируется?! Ну, не волнуйся! Следующим шагом будет реализация команды Symfony, которая будет выполняться как планировщик заданий на основе времени (задание Cron) и увеличивать все оповещения старше 10 минут.

Перед созданием этой команды нам нужно будет добавить метод репозитория для получения предупреждений, требующих эскалации. Откройте свой UserAlertRepository.php файл в API/src/репозитории/ .

В верхней части этого файла добавьте еще несколько сторонних библиотек для импорта:

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

Затем добавьте метод репозитория для получения всех предупреждений, которые были отправлены по SMS более 10 минут назад, но все еще находятся в статусе подняты :

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

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

Эта новая команда Symfony увеличит все полученные оповещения. Чтобы создать его, выполните следующую команду в своем терминале:

php bin/console make:command

Когда вас попросят ввести имя команды, введите приложение:эскалация-оповещение , которое создаст новый файл с именем EscalateAlertCommand.php внутри API/src/команды . Откройте этот новый файл.

Мы будем использовать все следующие классы, поэтому давайте обязательно включим их с самого начала. В верхней части файла, чуть ниже приложение пространства имен\Команда; добавьте следующее:

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

Классу нужно ввести в него два объекта, Vonage, пока и Интерфейс управления объектами . В Symfony самый простой способ сделать это – с помощью конструктора. В верхней части вашего класса добавьте следующие функции:

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

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

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

        parent::__construct();
    }

Теперь пришло время написать функциональность для этой команды. Он будет получать все оповещения с SMS, отправленными более 10 минут назад, но все еще со статусом повышен . Если таковые имеются, он получит пользователя, назначенного для оповещения, и отправит ему уведомление о голосовом вызове с преобразованием текста в речь. Заменить текущую функциональность в защищенной функции execute() на:

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

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

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

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

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

        return Command::SUCCESS;

Протестируйте API

Вашему пользователю необходима аутентификация для тестирования этих новых конечных точек. Во-первых, убедитесь, что вы получили свой токен JWT, отправив POST запрос на http://localhost:8080/api/login_check с вашими фиксированными учетными данными пользователей.

Как только вы скопируете свой СОБСТВЕННЫЙ, обновите тип запроса GET и URL-адрес, который будет http://localhost:8080/api/alerts . Вам необходимо указать заголовок с ключом Авторизация и значением На предъявителя замена с вашим жетоном.

Конечная точка list Alerts возвращает массив JSON, который вы можете увидеть в примере Postman ниже:

Давайте сохраним это предупреждение в его текущем состоянии и используем его позже при тестировании мобильного приложения.

Вы создали API; теперь пришло время создать мобильное приложение.

Создайте мобильное приложение

Обновить config.json где значение URL API является URL-адресом ngrok, который вы сохранили ранее.

Откройте новое окно терминала и выполните следующие команды:

cd MobileApp
npm install
expo start

Через некоторое время откроется веб-браузер. С левой стороны есть несколько вариантов запуска приложения, будь то на вашем мобильном устройстве, симуляторе iOS или симуляторе Android. Выберите подходящий вам вариант, и когда приложение загрузится, первым экраном, который вы увидите, будет Экран входа в систему.

Учетные данные пользователя fixtures в базе данных следующие:

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

Как показано на изображении ниже:

Успешный вход в систему в настоящее время ничего не даст! Сначала нам нужно внедрить больше экранов, но чтобы перепроверить правильность вашего входа в систему, проверьте свой терминал, на котором вы запустили expo start . Вы должны увидеть строку: Вы успешно вошли в систему! .

API оповещений

Отображение списка предупреждений

Внутри каталога API создайте новый файл с именем alerts.js . Добавьте приведенный ниже пример, который импортирует client.js файл для использования функциональности из getClient() . Эта новая функция, называемая получать оповещения() делает запрос к API на конечной точке /api/оповещения . Мы можем добавлять другие вызовы API, принимать, завершать и отменять оповещения, пока мы здесь.

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

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

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

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

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

Теперь, когда у нас есть функциональность для получения предупреждений, создайте компонент экрана предупреждений. Создайте новый файл внутри компонентов/ под названием AlertsScreen.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;

Теперь у нас есть пустой Экран предупреждений класс и некоторые стили. Давайте добавим в этот класс, чтобы показать что-то:

  state = {
    alerts: []
  }

  renderItem = ({ item }) => (
    
       this.onPress(item)}>
        
          
            {item.title}
          
        
        
          
            {item.dateRaised}
          
          
            {item.assigned !== '' ? item.assigned : 'Unassigned'}
          
          
            #{item.incidentId}
          
        
      
    
  );

  render() {
    return (
      
         item.id}
        />
      
    );
  }

Хорошо, это показывает нам нашу страницу. Но он не извлекает никакой информации и не говорит нам, что делать дальше!

Над вашим методом renderItem() добавьте следующее:

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

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

Отображение определенного предупреждения

Создайте новый файл в компонентах под названием AlertScreen.js , который показывает конкретное предупреждение по идентификатору.

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

class AlertScreen extends Component {
  state = {
    alert: {}
  }

  const = this.state.alert = this.props.route.params.alert;

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

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

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

  render() {
    let buttons;

    if (this.state.alert.status === 'raised') {
      buttons = 
          
             this.onPressAccept()}
              underlayColor='#fff'>
              Accept
            
          
          
             this.onPressCancel()}
              underlayColor='#fff'>
              Cancel
            
          
        
    } else if (this.state.alert.status === 'accepted') {
      buttons = 
          
             this.onPressComplete()}
              underlayColor='#fff'>
              Complete
            
          
          
             this.onPressCancel()}
              underlayColor='#fff'>
              Cancel
            
          
        
    }

    return (
      
        
          
            {this.state.alert.title}
          
        
        
          
            Date Raised: {this.state.alert.raisedDate}
          
          
            Assignee: {this.state.alert.assigned !== '' ? this.state.alert.assigned : 'Unassigned'}
          
          
            Incident ID: #{this.state.alert.incidentId}
          
          
            Status: {this.state.alert.status}
          
        
        {buttons}
        
          
            
              {this.state.alert.description}
            
          
        
      
    );
  }
}

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;

В настоящее время в вашем приложении нет инструкций о том, как отображать эти два новых экрана, которые вы создали. В navigation/MainStackNavigator.js ниже импортируйте логин , добавьте следующие две строки:

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

Затем ниже стека Login . Экран , добавьте два новых экрана:

        
         (
            {headerTitle: 'Alert Screen', 
            route: {route}, 
            navigation: {navigation}}
          )}
        />

Вернуться в свой LoginScreen.js файл, найдите строку, показывающую: console.log("Вы успешно вошли в систему!"); и добавьте фрагмент ниже, чтобы перенаправить пользователя при успешном входе в систему.

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

Тестирование

Чтобы протестировать это приложение в своем терминале, убедитесь, что вы перешли в каталог Мобильное приложение , и выполните следующую команду:

expo start

Через некоторое время должен открыться веб-браузер. С левой стороны есть несколько вариантов запуска приложения, будь то на вашем мобильном устройстве, симуляторе iOS или симуляторе Android. Выберите тот вариант, который вам подходит. Когда приложение запускается, первый экран, который вы видите, – это экран входа в систему.

Учетные данные пользователя fixtures в базе данных следующие:

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

При успешном входе в систему следующий экран, который вы видите, – это экран предупреждений. Однако сейчас это поле будет пустым, потому что в базе данных нет никаких предупреждений.

Теперь повторите попытку входа в свое мобильное приложение. Вы увидите новое предупреждение, а также сможете нажать на это предупреждение, чтобы перейти к экрану с дополнительной информацией.

Вы также можете перенести это предупреждение, независимо от того, будет ли оно принято или отменено.

Вывод

В этом уроке мы узнали, как создать API с использованием PHP-фреймворка Symfony. Мы также создали мобильное приложение с использованием React Native. API-интерфейсы Vonage позволили нам отправлять уведомления с помощью SMS и голосовых вызовов с преобразованием текста в речь. Применив все это вместе, мы создали функциональное приложение по вызову для разработчиков или системных администраторов, которое будет предупреждено, если что-то пойдет не так. Наличие webhook позволяет нам интегрировать нашу систему по вызову с несколькими службами, чтобы охватить как можно больше.

Ниже приведены несколько других руководств, которые мы написали по внедрению голосового API Vonage в проекты:

Как всегда, если у вас есть какие-либо вопросы, советы или идеи, которыми вы хотели бы поделиться с сообществом, пожалуйста, не стесняйтесь обращаться к нашему рабочему пространству Community Slack . Я хотел бы услышать, как вы справились с этим уроком и как работает ваш проект.

Оригинал: “https://dev.to/vonagedev/build-an-on-call-application-with-react-native-and-symfony-3362”