17/09/2018, 13:58

Communicating with PHP through Phone Calls! Part-1

Twilio là một ứng dụng SaaS cho phép developer xây dựng các ứng dụng mobile bằng cách sử dụng các công nghệ web. Trong loạt bài này, chúng ta sẽ sử dụng Twilio để xây dựng một ứng dụng dự báo thời tiết và được truy cập bằng mobile. Phần server sẽ được viết bằng Laravel framework. Trong phần này, ...

Twilio là một ứng dụng SaaS cho phép developer xây dựng các ứng dụng mobile bằng cách sử dụng các công nghệ web. Trong loạt bài này, chúng ta sẽ sử dụng Twilio để xây dựng một ứng dụng dự báo thời tiết và được truy cập bằng mobile. Phần server sẽ được viết bằng Laravel framework.

Trong phần này, chúng ta sẽ tạo một chương trình đơn giản cho phép user gọi một số điện thoại mà chúng tôi mua từ Twilio, nhập mã zip và nhận dự báo thời tiết hiện tại. Người dùng cũng có thể nhận được thời tiết cho bất kỳ ngày nào trong tuần thông qua các lời nhắc menu bằng giọng nói. Trong phần thứ hai của loạt bài này, chúng ta sẽ sử dụng những gì được xây dựng trong bài viết này để cho phép người dùng tương tác với ứng dụng qua SMS (tin nhắn văn bản).

Prerequisites

Development Environment

Loạt bài viết này sẽ sử dụng Homestead Improved tuy nhiên, bạn không nhất thiết phải sử dụng nó, và các lệnh có thể sẽ hơi khác nhau nếu bạn sử dụng một môi trường khác.

Dependencies

Chúng ta sẽ tạo một dự án Laravel mới và sau đó thêm Twilio PHP SDK và package Guzzle HTTP vào dự án:

cd ~/Code
composer create-project --prefer-dist laravel/laravel Laravel-twilio 5.4.*
cd Laravel-twilio
composer require "twilio/sdk:^5.7"
composer require "guzzlehttp/guzzle:~6.0"

Development

Routes

Mở file routes/web.php và thêm các route sau:

Route::group(['prefix' => 'voice', 'middleware' => 'twilio'], function () {
    Route::post('enterZipcode', 'VoiceController@showEnterZipcode')->name('enter-zip');

    Route::post('zipcodeWeather', 'VoiceController@showZipcodeWeather')->name('zip-weather');

    Route::post('dayWeather', 'VoiceController@showDayWeather')->name('day-weather');

    Route::post('credits', 'VoiceController@showCredits')->name('credits');
});

Trong ứng dụng này, tất cả các request sẽ nằm trong đường dẫn /voice/. Khi Twilio kết nối lần đầu tiên với ứng dụng, nó sẽ chuyển đến /voice/enterZipcode thông qua HTTP POST. Tùy thuộc vào những gì xảy ra trong cuộc gọi điện thoại, Twilio sẽ thực hiện request đến các thiết bị đầu cuối khác. Điều này bao gồm /voice/zipcodeWeather để cung cấp dự báo, /voice/dayWeather thời tiết của ngày hôm nay và /voice/credits cung cấp thông tin về nơi dữ liệu đến.

Service Layer

Chúng ta sẽ thêm một class Service. Class này sẽ chứa các business logic sẽ được chia sẻ giữa ứng dụng mobile này và ứng dụng SMS.

Tạo một thư mục con mới được gọi là Services bên trong thư mục app. Sau đó, tạo một file có tên WeatherService.php và có nội dung như sau:

<?php

namespace AppServices;

use TwilioTwiml;
use IlluminateSupportFacadesCache;

class WeatherService
{
}

Đây là một file lớn trong dự án, vì vậy chúng ta sẽ xây dựng nó từng phần một. Thêm các phần code sau vào class service này:

public $daysOfWeek = [
    'Today',
    'Sunday',
    'Monday',
    'Tuesday',
    'Wednesday',
    'Thursday',
    'Friday',
    'Saturday'
];

Chúng ta sẽ sử dụng mảng này để ánh xạ một ngày trong tuần thành một số; Chủ nhật = 1, Thứ Hai = 2, v.v.

public function getWeather($zip, $dayName)
{
    $point = $this->getPoint($zip);
    $tz = $this->getTimeZone($point);
    $forecast = $this->retrieveNwsData($zip);
    $ts = $this->getTimestamp($dayName, $zip);
    $tzObj = new DateTimeZone($tz->timezoneId);
    $tsObj = new DateTime(null, $tzObj);
    $tsObj->setTimestamp($ts);

    foreach ($forecast->properties->periods as $k => $period) {
        $startTs = strtotime($period->startTime);
        $endTs = strtotime($period->endTime);

        if ($ts > $startTs and $ts < $endTs) {
            $day = $period;
            break;
        }
    }

    $response = new Twiml();
    $weather = $day->name;
    $weather .= ' the ' . $tsObj->format('jS') . ': ';
    $weather .= $day->detailedForecast;
    $gather = $response->gather(
        [
            'numDigits' => 1,
            'action' => route('day-weather', [], false)
        ]
    );

    $menuText = ' ';
    $menuText .= "Press 1 for Sunday, 2 for Monday, 3 for Tuesday, ";
    $menuText .= "4 for Wednesday, 5 for Thursday, 6 for Friday, ";
    $menuText .= "7 for Saturday. Press 8 for the credits. ";
    $menuText .= "Press 9 to enter in a new zipcode. ";
    $menuText .= "Press 0 to hang up.";
    $gather->say($weather . $menuText);

    return $response;
}

Phương thức getWeather lấy một mã zipcode với ngày trong tuần và soạn thảo văn bản dự báo thời tiết. Đầu tiên, nó tính toán thời gian tham chiếu cho ngày được yêu cầu, và sau đó tìm kiếm dự báo thời tiết bằng cách thực hiện một nghiên cứu trên mảng dữ liệu dự báo. Sau đó, nó trả về một response Voice TwiML. Dưới đây là một ví dụ về những gì được trả về:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1" action="/voice/dayWeather">
    <Say>
      This Afternoon the 31st: Sunny, with a high near 72. South southwest wind around 8 mph. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up.
    </Say>
  </Gather>
</Response>

Thẻ <Gather> yêu cầu Twilio mong đợi input từ phía người dùng. Thuộc tính numDigits cho biết số lượng chữ số mong đợi. Thuộc tính action cho biết endpoint nào sẽ contact tiếp theo.

protected function retrieveNwsData($zip)
    {
        return Cache::remember('weather:' . $zip, 60, function () use ($zip) {
            $point = $this->getPoint($zip);

            $point = $point->lat . ',' . $point->lng;
            $url = 'https://api.weather.gov/points/' . $point . '/forecast';

            $client = new GuzzleHttpClient();

            $response = $client->request('GET', $url, [
                'headers' => [
                    'Accept' => 'application/geo+json',
                ]
            ]);

            return json_decode((string)$response->getBody());
        });
    }

Method retrieveNwsData lấy dữ liệu dự báo thời tiết. Đầu tiên, nó kiểm tra xem bản sao dự báo thời tiết của mã zipcode có trong bộ nhớ cache hay không. Nếu không, thì ứng dụng khách Guzzle HTTP được sử dụng để thực hiện request HTTP GET tới API của Dịch vụ thời tiết quốc gia (NWS) https://api.weather.gov/points/{point}/forecast. Để có được điểm địa lý của mã zipcode, một cuộc gọi được thực hiện theo phương thức getPoint trước khi thực hiện request tới API thời tiết. Phản hồi từ API là dự báo thời tiết ở định dạng GeoJSON. Dự báo là cho mỗi ngày và đêm trong một tuần (với một số trường hợp ngoại lệ chúng ta sẽ thảo luận sau);. Chúng ta lưu trữ response API trong một giờ vì request chậm, cộng với việc chúng ta không muốn truy cập server quá thường xuyên.

protected function getPoint($zip)
    {
        return Cache::remember('latLng:' . $zip, 1440, function () use ($zip) {
            $client = new GuzzleHttpClient();
            $url = 'http://api.geonames.org/postalCodeSearchJSON';

            $response = $client->request('GET', $url, [
                'query' => [
                    'postalcode' => $zip,
                    'countryBias' => 'US',
                    'username' => env('GEONAMES_USERNAME')
                ]
            ]);

            $json = json_decode((string)$response->getBody());

            return $json->postalCodes[0];
        });
    }

Phương thức getPoint ánh xạ một zipcode đến một địa điểm địa lý. Điều này được thực hiện bằng cách sử dụng API GeoNames. Kết quả được lưu trữ trong một ngày vì sử dụng API chậm.

protected function getTimeZone($point)
    {
        $key = 'timezone:' . $point->lat . ',' . $point->lng;

        return Cache::remember($key, 1440, function () use ($point) {
            $client = new GuzzleHttpClient();
            $url = 'http://api.geonames.org/timezoneJSON';

            $response = $client->request('GET', $url, [
                'query' => [
                    'lat' => $point->lat,
                    'lng' => $point->lng,
                    'username' => env('GEONAMES_USERNAME')
                ]
            ]);

            return json_decode((string) $response->getBody());
        });
    }

Phương thức getTimeZone được sử dụng để lấy múi giờ theo zipcode. API GeoNames cũng được sử dụng và kết quả được lưu trong bộ nhớ cache trong một ngày cũng cùng một lý do như trên.

protected function getTimestamp($day, $zip)
    {
        $point = $this->getPoint($zip);
        $tz = $this->getTimeZone($point);

        $tzObj = new DateTimeZone($tz->timezoneId);

        $now = new DateTime(null, $tzObj);

        $hourNow = $now->format('G');
        $dayNow = $now->format('l');

        if ($day == $dayNow and $hourNow >= 18) {
            $time = new DateTime('next ' . $day . ' noon', $tzObj);
            $ts = $time->getTimestamp();
        } elseif (($day == 'Today' or $day == $dayNow) and $hourNow >= 6) {
            $ts = $now->getTimestamp();
        } else {
            $time = new DateTime($day . ' noon', $tzObj);
            $ts = $time->getTimestamp();
        }

        return $ts;
    }

Phương thức getTimestamp trả về một thời gian tham chiếu được sử dụng để tìm kiếm một dự báo cho một ngày cụ thể. Hầu hết thời gian, dữ liệu dự báo có dự báo ngày và đêm, nhưng đôi khi nó có một đêm (trước 6 giờ sáng) và dự báo buổi chiều (chiều, trước 6 giờ chiều) cho ngày hiện tại. Vì lý do này, chúng ta phải thực hiện một số phép tính để có thời gian tham chiếu tốt. Trong hầu hết các trường hợp, nó trả về thời gian buổi trưa của theo mã zipcode cho ngày được yêu cầu.

public function getCredits()
    {
        $credits = "Weather data provided by the National Weather Service. ";
        $credits .= "Zipcode data provided by GeoNames.";

        return $credits;
    }
}

Phương thức getCredits chỉ trả về một số văn bản chuẩn về nơi dữ liệu đến.

Controller

Tạo file VoiceController.php trong thư mục app/Http/Controllers và nó sẽ có nội dung như sau:

<?php

namespace AppHttpControllers;

use AppServicesWeatherService;
use IlluminateHttpRequest;
use TwilioTwiml;

class VoiceController extends Controller
{
    protected $weather;

    public function __construct(WeatherService $weatherService)
    {
        $this->weather = $weatherService;
    }

    public function showEnterZipcode()
    {
        $response = new Twiml();

        $gather = $response->gather(
            [
                'numDigits' => 5,
                'action' => route('zip-weather', [], false)
            ]
        );

        $gather->say('Enter the zipcode for the weather you want');

        return $response;
    }

    public function showZipcodeWeather(Request $request)
    {
        $zip = $request->input('Digits');

        $request->session()->put('zipcode', $zip);

        return $this->weather->getWeather($zip, 'Today');
    }

    public function showDayWeather(Request $request)
    {
        $digit = $request->input('Digits', '0');

        switch ($digit) {
            case '8':
                $response = new Twiml();
                $response->redirect(route('credits', [], false));
                break;
            case '9':
                $response = new Twiml();
                $response->redirect(route('enter-zip', [], false));
                break;
            case '0':
                $response = new Twiml();
                $response->hangup();
                break;
            default:
                $zip = $request->session()->get('zipcode');
                $day = $this->weather->daysOfWeek[$digit];
                $response = $this->weather->getWeather($zip, $day);
                break;
        }

        return $response;
    }

    public function showCredits()
    {
        $response = new Twiml();
        $credits = $this->weather->getCredits();

        $response->say($credits);
        $response->hangup();

        return $response;
    }
}

Phương thức showEnterZipcode được thực thi khi một request được thực hiện tới /voice/enterZipcode. Phương thức này trả về TwiML yêu cầu người gọi nhập mã zip. TwiML cũng cho biết là sẽ request đến /voice/zipcodeWeather khi người gọi đã nhập 5 chữ số. Đây là một câu trả lời mẫu:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="5" action="/voice/zipcodeWeather">
    <Say>
      Enter the zipcode for the weather you want
    </Say>
  </Gather>
</Response>

Phương thức showZipcodeWeather được thực hiện khi có request tới /voice/zipcodeWeather. Phương thức này trả về văn bản dự báo của ngày hôm nay và menu thoại để điều hướng ứng dụng ở định dạng TwiML. Dưới đây là ví dụ về một câu trả lời:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1" action="/voice/dayWeather">
    <Say>
      This Afternoon the 31st: Sunny, with a high near 72. South southwest wind around 8 mph. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up.
    </Say>
  </Gather>
</Response>

Khi request đến /voice/dayWeather, phương thức showDayWeather được thực hiện. Nó trả về dự báo cho ngày được yêu cầu và menu thoại để điều hướng ứng dụng ở định dạng TwiML. Ví dụ cho ngày thứ hai:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1" action="/voice/dayWeather">
    <Say>
      Monday the 3rd: Sunny, with a high near 70. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up.
    </Say>
  </Gather>
</Response>

Phương thức cuối cùng, showCredits, được thực hiện khi request đến /voice/credits. Ví dụ một response:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>
    Weather data provided by the National Weather Service. Zipcode data provided by GeoNames.
  </Say>
  <Hangup/>
</Response>

HẾT PART 1

References

https://www.sitepoint.com/hello-laravel-communicating-php-phone-calls/

0