Effortlessly construct intricate USSD menus with streamlined efficiency by replacing convoluted nests of PHP files with the simplicity of XML-based menu construction. This approach allows for seamless execution similar to standard PHP scripts, minimizing code complexity and enhancing readability.
Let's explore an example of a simple SACCO USSD application.
<?xml version="1.0" encoding="UTF-8"?>
<menu name="sacco"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="menu.xsd">
<action name="check_user"/>
<options header="SACCO Services" noback="no">
<option text="Savings">
<list header="Saving Accounts" provider="saving_accounts" prefix="account"/>
<options header="Savings">
<option text="Deposit">
<options header="Deposit From:">
<option text="My Number">
<variable name="sender" value="{{phone_number}}"/>
</option>
<option text="Another Number">
<question name="sender" text="Enter Phone Number: "/>
</option>
</options>
<question name="amount" text="Enter Amount: "/>
<action name="deposit"/>
</option>
<option text="Withdraw">
<options header="Withdraw To:">
<option text="My Number">
<variable name="receiver" value="{{phone_number}}"/>
</option>
<option text="Another Number">
<question name="receiver" text="Enter Phone Number: "/>
</option>
</options>
<question name="amount" text="Enter Amount: "/>
<action name="withdraw"/>
</option>
<option text="Check Balance">
<action name="check_balance" text="To see your balance, enter PIN: "/>
</option>
<option text="Check Transaction">
<question name="transaction_id" text="Enter Transaction ID: "/>
<action name="check_transaction" text="To check transaction, enter PIN: "/>
</option>
</options>
</option>
<option text="Loans">
<response text="Coming soon."/>
</option>
</options>
</menu>
Installation
Install the package via the Composer.
composer require bmatovu/laravel-ussd
Configurations
php artisan vendor:publish --provider="Bmatovu\Ussd\UssdServiceProvider" --tag="ussd-config"
menus/menu.xml
<?xml version="1.0" encoding="UTF-8" ?>
<menu name="demo">
<question name="guest" text="Enter Name: "/>
<response text="Hello {{guest}}."/>
</menu>
app/Http/Controller/Api/UssdController
use Bmatovu\Ussd\Ussd;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* @see https://developers.africastalking.com/docs/ussd/overview
*/
class UssdController extends Controller
{
public function __invoke(Request $request): Response
{
try {
$output = Ussd::make('menu.xml', $request->session_id)
->handle($request->text);
} catch(\Exception $ex) {
return response('END ' . $ex->getMessage());
}
return response('CON ' . $output);
}
}
See more examples in the demo repo
Publish the menu schema
Defaults to using the schema bundled within the package if none is present in your menus path, usually menus/menu.xsd
.
php artisan vendor:publish --provider="Bmatovu\Ussd\UssdServiceProvider" --tag="ussd-schema"
Validate your menu files against the schema
php artisan ussd:validate
VSCode
The RedHat XML package is useful for realtime XSD validations and suggestions.
- <menu name="demo">
+ <menu name="demo"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="menu.xsd">
<question name="guest" text="Enter Name: "/>
<response text="Hello {{guest}}."/>
</menu>
The package comes with a CLI USSD simulator supporting a handful of populator aggregators.
Publish the simulator config file to get started. Update the aggregator and the USSD service endpoint in the config file.
php artisan vendor:publish --provider="Bmatovu\Ussd\UssdServiceProvider" --tag="ussd-simulator"
Usage:
./vendor/bin/ussd --help
./vendor/bin/ussd 256772100103
Aggregators
- Africastalking
- Comviva (Airtel & MTN)
If you're an aggregator missing from the current list reachout to have you added. Or simply send a pull request
$color = 'blue';
<variable name="color" value="blue"/>
Note: This tag has no output
$username = readline('Enter username: ');
<question name="username" text="Enter username: "/>
exit('Thank you for banking with us.');
<response text="Thank you for banking with us."/>
Note: this tag throws an exception to mark a break in the normal flow.
Options are like named grouped if, else-if
statements that allow a user to navigate to a predefined path.
$choice = readline('Choose service [1. Deposit, 2. Withdraw]: ');
if($choice === 1) {
// deposit...
} elseif($choice === 2) {
// withdraw...
}
<options header="Choose service">
<option text="Deposit">
<!-- ... -->
</option>
<option text="Withdraw">
<!-- ... -->
</option>
</options>
Disable backward navigation
By default 0) Back
option will be added to the options rendered. Use the attribute noback
to disable this behavior.
This behavior may only be used for nested <options>
tags.
<options header="Choose service" noback="no">
<!-- ... -->
</options>
Can contain any other tags inclusive of the IF tag itself.
if($role == 'treasurer') {
// ...
}
<if key="role" value="treasurer">
<!-- ... -->
</if>
This construct should cover for if-else
, if-elseif-else
, and the native switch
.
Example #1
if($role == 'treasurer') {
// ...
} else {
// ...
}
<choose>
<when key="role" value="treasurer">
<!-- ... -->
</when>
<otherwise>
<!-- ... -->
</otherwise>
</choose>
Example #2
if($role == 'treasurer') {
// ...
} elseif($role == 'member') {
// ...
} else {
}
<choose>
<when key="role" value="treasurer">
<!-- ... -->
</when>
<when key="role" value="member">
<!-- ... -->
</when>
<otherwise>
<!-- ... -->
</otherwise>
</choose>
Example #3
switch ($role) {
case "treasurer":
// ...
break;
case "member":
// ...
break;
default:
// ...
}
<choose>
<when key="role" value="treasurer">
<!-- ... -->
</when>
<when key="role" value="memeber">
<!-- ... -->
</when>
<otherwise>
<!-- ... -->
</otherwise>
</choose>
Action tags give you the ability to perform more customized operations.
$userInfo = \App\Ussd\Actions\GetUserInfoAction('256732000000');
Arguments
You can pass arguments to actions via attributes or as variables.
<!-- Read from cache -->
<!-- $msisdn = $this->store->get('msisdn'); -->
<action name="get_user_info"/>
<!-- Pass as attribute -->
<action name="get_user_info" msisdn="{{msisdn}}"/>
<!-- Pass as variable -->
<action name="get_user_info">
<variable name="msisdn" value="{{msisdn}}"/>
</action>
Getting user input
If the text
attribute is set on an action, it will behave like the <question>
tag waiting for user input
<!-- Approach #1 - user input handled by a qn tag -->
<question name="pin" text="To check balance, enter PIN: "/>
<action name="validate_pin"/>
<!-- Approach #2 - user input handled by the action -->
<action name="validate_pin" text="To check balance, enter PIN: "/>
Lists are used to display dynamic items.
The provider must return a list of items, each containing an id
and a label
.
$listItems = (new \App\Ussd\Providers\SavingAccountsProvider)->load();
[
[
'id' => 4364852, // account_id
'label' => '01085475262', // account_number
],
];
<list header="Saving Accounts" provider="saving_accounts" prefix="account"/>
Accessing the selected item on the list
<!-- Format: {prefix}_<id, label> -->
<response text="{{account_id}}"/><!-- 4364852 -->
<response text="{{account_label}}"/><!-- 01085475262 -->
Note: Similar to actions, you can pass arguments to lists via attributes or as variables.
It's also possible to set the number of retries and a custom error message.
Question
Using regex patterns.
<question
name="pin"
text="Enter PIN: "
+ retries="1"
+ pattern="^[0-9]{5}$"
+ error="You entered the wrong PIN. Try again" />
Options & Lists
Validation is against the possible list options.
<options
header="Choose a test"
+ retries="1"
+ error="Choose the correct number:">
...
</option>
<list
header="Saving Accounts"
provider="saving_accounts"
prefix="account"
+ retries="1"
+ error="Choose the correct number:"/>
Note: Retries in <action>
tags are discouraged because the action tags are not aware of tags preceeding them.
The <if>
and <when>
tags allow comparisions.
Falls back to eq
if the cond
is not set or it's unsupported.
<if key="age" value="18">
<if key="age" cond="eq" value="18">
Type | Conditions |
---|---|
Numbers | - lt - gt - lte - gte - eq - ne - btn |
Strings | - str.equals - str.not_equals - str.starts - str.ends - str.contains |
Regex | - regex.match |
Arrays | - arr.in - arr.not_in |
Dates | - date.equals - date.before - date.after - date.between |
Time | - time.equals - time.before - time.after - time.between |
Create the translation files in your project and return keys in your menu files... See the example below
menus/menu.xml
<menu name="demo">
<action name="set_locale" locale="fr" />
<question name="guest" text="AskForName" />
<response text="GreetGuest" />
</menu>
resources/lang/fr.json
{
"AskForName": "Entrez le nom:",
"GreetGuest": "Boujour {{guest}}"
}
USSD simulation
ussd-demo$ vendor/bin/ussd 250723000123
Entrez le nom:
John
Boujour John
Note:
- use the
set_locale
action to change locale directly from the ussd menu, and - use
App::setLocale
to change locale in your controller
This package persists USSD session data in cache. Each key is prefixed with the session_id
and it automatically expires after the configured ttl
.
Accessing variables
<variable name="color" value="blue"/>
$this->store->get('color'); // blue
Cache::store($driver)->get("{$sessionId}color"); // blue
Reusing existing variables
<variable name="msg" value="Bye bye."/>
<response text="{{msg}}"/> <!-- Bye bye -->
Save default variables
Example for saving any variable from the incoming USSD request.
Ussd::make($menu, $request->session_id)
->save([
'phone_number' => $request->phone_number,
])
->handle(...);
Use custom menu entry point
By default the parsing starts at the 1st element in your menu file, i.e /menu/*[1]
.
If you wish to start from a different point or use a custom menu file structure. Here's how to go about it...
Ussd::make($menu, $request->session_id)
->entry("/menus/menu[@name='sacco']/*[1]")
->handle(...);
See: xpath playground
You can extend the USSD simulator with your aggregator of choice by simply registering it in the simulator config file.
The provider class should implement Bmatovu\Ussd\Contracts\Aggregator
.
simulator.json
{
+ "aggregator": "africastalking",
"aggregators": {
+ "hubtel": {
+ "provider": "App\\Ussd\\Simulator\\Africastalking",
+ "uri": "http://localhost:8000/api/ussd/africastalking",
+ "service_code": "*123#"
+ }
}
}
Why use XML 🥴 and not JSON 😉?
XML is better suited for writing constructs resembling programming languages. It offers straightforward validation of schemas. Additionally, XML is both more compact and readable.
Compare the snippets below...
<menu name="demo">
<question name="guest" text="Enter Name: "/>
<response text="Hello {{guest}}."/>
</menu>
{
"@name": "demo",
"question": {
"@name": "guest",
"@text": "Enter Name:"
},
"response": {
"@text": "Hello {{guest}}."
}
}
To run the package's unit tests, run the following command:
composer test
If you find any security related issues, please contact me directly at mtvbrianking@gmail.com to report it.
If you wish to make any changes or improvements to the package, feel free to make a pull request.
Note: A contribution guide will be added soon.
- sparors/laravel-ussd takes a completely different approach on building USSD menus.
The MIT License (MIT). Please see License file for more information.