tnmdev/ussd

Laravel PHP的USSD适配器

v1.0.4 2022-09-15 18:00 UTC

README

此包创建了一个适配器、样板代码和功能,允许您与USSDC交互,并向您的API提供USSD通道。接口是开放的并且已经文档化,可以与各种USSD接口实现。

目录表生成于markdown-toc

安装

composer require tnmdev/ussd

然后发布包的迁移和配置文件。

php artisan vendor:publish --provider="TNM\USSD\USSDServiceProvider"

然后安装ussd脚手架。这也会运行迁移以创建会话跟踪表。

php artisan ussd:install

一旦安装了包,USSD应用程序将在/api/ussd端点上可用。将在App\Screens\Welcome.php中为您创建一个着陆屏幕。

用法

创建USSD屏幕

php artisan make:ussd <name>

这将为您创建一个样板USSD屏幕对象。您可以继续编辑messageoptionsexecute方法的内 容。该屏幕扩展了TNM\USSD\Screen类,这为您提供了访问请求详细信息和使用USSD响应编码的方法。

请求对象

Screen有一个公共属性$request。这是一个TNM\USSD\Http\Request类的对象。

请求类公开了USSDC传递的XML请求中的四个属性。

发送给用户的USSD屏幕由Screens表示,它们扩展了TNM\USSD\Screen类。

请求有效载荷

您可以通过请求有效载荷在屏幕之间移动有效载荷。会话中的任何请求都可以访问添加到请求有效载荷中的任何数据。

设置请求有效载荷

可以通过在请求的对象链上调用addPayload方法来添加请求有效载荷。它接受键值对参数。

$this->addPayload('key', $this->value());

检索请求有效载荷

$this->payload('key');

在有效载荷中使用数组

有时您有用于选项的关联数组。例如,您可以有一个包含产品列表,其中包含idpricenamehumanized。其中name是在您的系统中对产品的引用,而humanized是您希望在屏幕上显示的方式。

可以带有第三个布尔参数的此类项目的数组可以推送到有效载荷中。这告诉跟踪对象在存储之前对输入进行序列化。

$this->addPayload('products', $array, true);

通过TNM\USSD\Traits命名空间中的HasBundledOptions特质,可以实现对数组有效载荷的操纵。因此,要在您的有效载荷中使用数组,您需要在您的Screen中使用HasBundledOptions特质。

以下是捆绑选项特质的一些用途:要将关联数组作为USSD选项列表/映射,可以使用map方法映射到您选择的数组键。

public function options(): array 
{
    return $this->map('humanized', 'products');
}

map方法接受两个参数。第一个是要映射的数组键,第二个是要从有效载荷中列出的有效载荷键。

当用户在USSD屏幕上做出选择时,您可以通过调用find方法回溯到关联数组选项的任何键。

$this->addPayload('chosenProduct', $this->find('id', 'products'));

上述代码片段中的实现会将所选产品的 ID 分配给有效载荷键 chosenProduct。特性会查找作为第二个参数传递给用户的选项。您可以通过传递第三个参数来指定要查找的字段,默认为 humanized。因此,假设您的选项关联数组将有一个用于显示内容的字段。您可以将其重命名为任何适合您的名称。只需确保传递第三个参数来告诉方法在哪里查找。

在其他情况下,您可能只想获取特定有效载荷键上的整个数组。方法与普通有效载荷相同,再次带有第二个布尔参数。

$this->payload('products', true);

必选方法

Screen 类将要求您实现以下方法。

  • message() 必须返回一个将在屏幕上显示的字符串消息。
  • options() 必须返回一个选项数组,这些选项将暴露给用户。对于不需要选项的屏幕,请返回空数组。
  • execute() 应用于实现应用程序应如何处理请求数据。请求数据由屏幕对象中的 getRequestValue() 返回。您可以使用它来访问请求数据。如果您想将用户重定向到另一个屏幕,请返回目标屏幕的 render() 方法:return (new Register($this->request))->render();。屏幕初始化需要一个参数,即 request 对象。
  • previous() 这应该返回一个 Screen 类的对象。它告诉会话当用户选择后退选项时导航到哪里。

可选方法

您可以通过以下方法扩展来更改屏幕的一些属性。

  • type() 应返回一个整数,委托给 TNM\USSD\Response 类的常量 RELEASERESPONSE。如果没有重写,则默认为 RESPONSERESPONSE 渲染一个带有输入字段的屏幕,而 RELEASE 渲染一个不带输入字段的屏幕,用于指示 USSD 网关关闭 USSD 会话。
  • acceptsResponse(),与 type() 方法的复杂性相比,您可以调用 acceptsResponse()。它应该返回一个布尔值,指示屏幕是否渲染输入字段或发送一个标记 USSD 会话结束的屏幕。
  • goesBack() 返回一个布尔值,定义屏幕是否应具有 back 导航选项。除非您正在定义登录屏幕,否则请保留它。

异常处理

USSD 适配器有一个自渲染的异常处理器。要使用它,请抛出 TNM\USSD\Exceptions 命名空间的 UssdException。它接受两个参数:请求对象和您想要传递给用户的消息。异常处理器将渲染一个带有错误消息的 USSD 屏幕,并终止会话。

输入数据验证

您可以通过使用 TNM\USSD\Http 命名空间的 Validates 特性来设置规则以验证用户输入。轨迹将要求您实现 rules() 方法,它应该返回一个验证规则字符串。

要验证输入,请在该 Screen 类的 execute() 方法中调用 $this->validate($this->request, $label)

如果输入有验证错误,将抛出 TNM\USSD\Exceptions 命名空间的 ValidationException,并将为您自动渲染错误屏幕。

namespace App\Screens;

use TNM\USSD\Screen;
use TNM\USSD\Http\Validates;

class EnterPhoneNumber extends Screen
{
    use Validates;

    protected function message() : string
    {
        return 'Enter your phone number';
    }
    
    //...
    
    protected function execute()
    {
        $this->validate($this->request, 'phone');
        $this->addPayload('phone', $this->value());
        return (new NextScreen($this->request))->render();
    }

    protected function rules() : string
    {
        return 'regex:/(088)[0-9]{7}/';
    }
}      

扩展以支持多种实现

此适配器是考虑扩展性而设计的。目前,它支持 TNM 和 Airtel Malawi 分别使用的 TruRoute 和 Flares USSD 接口。然而,凭借可插拔的接口,它可以扩展以支持任何移动网络运营商。

要扩展功能,请创建一个请求类和响应类。这些类必须分别实现TNM\USSD\Http\UssdRequestInterfaceTNM\USSD\Http\UssdResponseInterface接口。响应类已经进一步简化,您只需要扩展XMLResponse类。

请求类的实现细节可能有所不同。然而,我们强烈建议您有一个构造函数,该构造函数将来自移动运营商的USSD请求解码为数组,并将其分配给$request私有属性,接口方法应基于私有属性返回它们的值。

示例请求实现

use TNM\USSD\Http\UssdRequestInterface;

class TruRouteRequest implements UssdRequestInterface
{
    /**
     * @var array
     */
    private $request;

    public function __construct()
    {
        $this->request = json_decode(json_encode(simplexml_load_string(request()->getContent())), true);
    }

    public function getMsisdn(): string
    {
        return $this->request['msisdn'];
    }
    // ...
}
必需方法

请求接口要求您实现以下方法

  • getSession()应返回由USSD网关分配的会话id
  • getMsisdn()应返回发起USSD请求的msisdn
  • getMessage()应返回与请求一起发送的消息
  • getType()应返回请求类型。

示例响应实现

以下是一个响应类实现的示例。您需要在响应目录中添加一个示例响应XML文件。此文件将由MNO提供。对于TNM和Airtel Malawi,我们已经为您准备好了。

TNM响应XML模板
<ussd>
    <type>{{type}}</type>
    <msg>{{message}}</msg>
    <premium>
        <cost>0</cost>
        <ref>NULL</ref>
    </premium>
</ussd>
TNM响应类
use TNM\USSD\Http\UssdResponseInterface;

use TNM\USSD\Screen;

class TruRouteResponse extends XMLResponse
{
    protected function getPayload(): array
    {
        return [
            'type' => $this->screen->type(),
            'message' => $this->screen->getResponseMessage(),
        ];
    }

    protected function getTemplate(): string
    {
        return __DIR__ . '/response.xml';
    }
}

路由

您可以使用路由参数adapter来区分来自不同移动运营商的请求。

使用Flares适配器的网络的全部请求应路由到api/ussd/flares。因此,当您创建自己的扩展时,运营商的路由应该是api/ussd/{adapter}

这不是魔法般地解决的。您需要在TNM\USSD\Factories\RequestFactoryTNM\USSD\Factories\ResponseFactory中定义实现。

示例请求工厂
namespace TNM\USSD\Factories;

class RequestFactory
{
    public function make(): UssdRequestInterface
    {
        return match (request()->route('adapter')) {
            'flares' => resolve(FlaresRequest::class),
            default => resolve(TruRouteRequest::class),
        };
    }
}
示例响应工厂
namespace TNM\USSD\Factories;

class ResponseFactory
{
    public function make(): UssdResponseInterface
    {
        return match (request()->route('adapter')) {
            'flares' => resolve(FlaresResponse::class),
            default => resolve(TruRouteResponse::class),
        };
    }
}

本地化

您可以在应用的任何屏幕上设置会话语言。以下屏幕将以新选定的语言显示。

$this->request->trail->setLocale('en');

此功能实现了Laravel的本地化,使用语言文件。有关更多详细信息,请参阅Laravel文档。因此,您的实现可能如下所示

public function message(): string 
{
    return __("screens.welcome_message");
}

示例本地化实现

public function execute()
{
    $locale = $this->value() == 'English' ? 'en' : 'fr';
    $this->request->trail->setLocale($locale);
    return (new NextScreen($this->request))->render();
}

审计

您可以使用CLI工具跟踪用户会话、系统消息和用户响应。

php artisan ussd:list <phone>

此命令为您提供了一个列表,其中包含一个号码执行的所有交易。列表包含会话ID和时间戳。

php artisan ussd:audit <session-id>

此命令提供从会话开始到结束的交易的所有详细信息。轨迹包括系统消息、用户对每个消息的响应及其按时间顺序的时间戳。

当用户响应是一个选项时,它报告一个字符串值,该值由选定的数字表示,从而节省了您查找哪个选项位于数字1、2等的麻烦。

会话数据清理

该软件包使用数据库表跟踪会话。此数据库表可能需要一段时间后进行清理。要清理,请在应用目录中运行以下命令。

php artisan ussd:clean-up --days=30

它接受保留天数的数据选项。如果没有传递选项,则删除60天以上的所有内容。

  • 关于审计的注意事项:清理后的数据将不可用审计轨迹。

示例屏幕实现

// app/Screens/Subscribe.php

namespace App\Screens;

use TNM\USSD\Screen;

class Subscribe extends Screen
{
    public function message(): string
    {
        return "Please select a plan you want to subscribe to";
    }

    public function options(): array
    {
        return ['Plan 1', 'Plan 2', 'Plan 3'];
    }

    public function execute()
    {
        // save the request value to session object 
        // to access it in the next screen with $this->payload($key) 
        $this->addPayload('plan', $this->value());

        return (new ConfirmSubscription($this->request))->render();
    }
        
    public function previous(): Screen
    {
        return new Welcome($this->request);
    }
}
// app/Screens/ConfirmSubscription.php

namespace App\Screens;

use Exception;use TNM\USSD\Screen;
use TNM\USSD\Exceptions\UssdException;

class ConfirmSubscription extends Screen
{
    public function message(): string
    {
        return sprintf("Please confirm subscription to %s", $this->payload('plan'));
    }

    public function options(): array
    {
        return ['Confirm', 'Cancel'];
    }

    public function execute()
    {
        if ($this->value() === 'Cancel') return $this->previous()->render();
        
        $service = new SubscriptionService($this->request->msisdn);

        try {
       
            $service->subscribe($this->payload('plan'));
            return (new Subscribed($this->request))->render();
            
        } catch (Exception $exception) {
            throw new UssdException($this->request, "Subscription failed. Please try again later");
        }
    }
    
    public function previous(): Screen
    {
        return new Subscribe($this->request);
    }
}