Skip to content

Docs ‐ State

papamarfo edited this page Jul 30, 2026 · 8 revisions

Introduction to States

USSD state help as create pages for users and exaluate the input they choose after.

Creating USSD States

You just need to run the following command to create a new USSD State.

php artisan ussd:state AvailableCountriesState

If the state should be the first state, you can use the init option

php artisan ussd:state WelcomeState --init

If the state should be a continuing state, you can use the cont option. Continue state is used to resume uncompleted USSD state by first asking the user if they want to continue an old session or start a new one.

php artisan ussd:state WouldYouLikeToContinueState --cont

Ask how the user if they will like to continue and specify which decision should be used to validate the input.

<?php

namespace App\Ussd\States;

use Speso\Ussd\Contracts\ContinueState;
use Speso\Ussd\Contracts\Decision;
use Speso\Ussd\Decisions\Equal;
use Speso\Ussd\Menu;

class WouldYouLikeToContinueState implements ContinueState
{
    public function render(): Menu
    {
        return Menu::build()->text('Enter 1 to continue or any key start over');
    }

    public function confirm(): Decision
    {
        return new Equal(1);
    }
}

Working with USSD States

States should have a render function that returns a USSD menu.

Transitioning between States

You can use Transition attributes to indicate the transitioning from one state to the other. Transition attributes allows you to define a callback that should be run before performing the transitioning.

<?php

namespace App\Ussd\States;

use App\Models\Customer;
use Speso\Ussd\Attributes\Transition;
use Speso\Ussd\Contracts\State;
use Speso\Ussd\Decisions\Equal;
use Speso\Ussd\Decisions\Fallback;
use Speso\Ussd\Menu;
use Speso\Ussd\Record;


#[Transition(to: RegisterState::class, match: new Equal(1), callback: [self::class, 'callback'])]
#[Transition(to: HelplineState::class, match: new Fallback())]
class WelcomeState implements State
{
    public function render(): Menu
    {
        return Menu::build()
            ->line('Banc')
            ->listing([
                'Register',
                'Helpline',
            ])
            ->text('Powered by Speso');
    }

    public function callback(Record $record): void
    {
        $details = Customer::query()
            ->where('phone_number', $context->get('phone_number'))
            ->latest()
            ->first();

        $record->set('details', $details);
    }
}

Back Navigation

Add a Back attribute alongside your Transition attributes to let users return to whichever state they came from. Laravel USSD tracks a per-session history stack automatically: every time a Transition moves a user to a new state, the state they're leaving is pushed onto the stack, and a matching Back pops it back off.

<?php

namespace App\Ussd\States;

use App\Ussd\States\CheckoutState;
use Speso\Ussd\Attributes\Back;
use Speso\Ussd\Attributes\Transition;
use Speso\Ussd\Contracts\State;
use Speso\Ussd\Decisions\Equal;
use Speso\Ussd\Menu;

#[Back(match: new Equal('0'))]
#[Transition(to: CheckoutState::class, match: new Equal(1))]
class CartState implements State
{
    public function render(): Menu
    {
        return Menu::build()
            ->line('1. Checkout')
            ->text('0. Back');
    }
}

If there's nothing on the stack (for example, a user presses "back" on the very first screen), the Back attribute is simply ignored for that input and any other matching Transition on the same state still applies. Back also accepts an optional callback, resolved and called the same way as on Transition, if you need to run logic when a user navigates back.

Paginating a States

You can use Paginate attributes to paginate a very long list of items that would not fit on a page. Paginate is actually just transition, back to the same state.

<?php

namespace App\Ussd\States;

use App\Models\Customer;
use Speso\Ussd\Attributes\Paginate;
use Speso\Ussd\Contracts\State;
use Speso\Ussd\Decisions\Equal;
use Speso\Ussd\Decisions\Fallback;
use Speso\Ussd\Menu;
use Speso\Ussd\Context;
use Speso\Ussd\Record;


#[Paginate(next: new Equal('#'), previous: new Equal('0'), callback: [self::class, 'callback'])]
#[Transition(to: HelplineState::class, match: new Fallback())]
class AvailableCountriesState implements State
{
    public function render(Record $record): Menu
    {
        $page = $record->get('countries_page', 1);

        return Menu::build()
            ->line('Banc')
            ->listing([
                'Angola',
                'Algeria',
                'Cameroon',
                'DR Congo',
                'Ghana',
                'Egypt',
                'Kenya',
                'Uganda',
                'Zimbabwe',
            ], page: $page, perPage: 3)
            ->text('Powered by Speso');
    }

    public function callback(Context $context, Record $record): void
    {
        $page = $record->get('countries_page', 1);

        if ('#' === $context->input()) {
            $record->set('countries_page', $page + 1);
        } else {
            $record->set('countries_page', $page - 1);
        }
    }
}

WithPagination Helper

Pagination is common for USSD application so there are inbuilt helper traits to help make it simple.

<?php

namespace App\Ussd\States;

use App\Models\Customer;
use Speso\Ussd\Attributes\Paginate;
use Speso\Ussd\Contracts\State;
use Speso\Ussd\Decisions\Equal;
use Speso\Ussd\Decisions\Fallback;
use Speso\Ussd\Menu;
use Speso\Ussd\Record;
use Speso\Ussd\Traits\WithPagination;


#[Paginate(next: new Equal('#'), previous: new Equal('0'))]
#[Transition(to: HelplineState::class, match: new Fallback())]
class AvailableCountriesState implements State
{
    use WithPagination;

    public function render(Record $record): Menu
    {
        $page = $record->get('countries_page', 1);

        return Menu::build()
            ->when($this->isFirstPage(), fn (Menu $menu) => $menu->line('Banc'))
            ->listing($this->getItems(), page: $this->currentPage(), perPage: $this->perPage())
            ->when($this->hasPreviousPage(), fn (Menu $menu) => $menu->line('0. Previous Page'))
            ->when($this->hasNextPage(), fn (Menu $menu) => $menu->line('#. Next Page'))
            ->when($this->isLastPage(), fn (Menu $menu) => $menu->line('Powered by Speso'));
    }

    public function getItems(): array
    {
        return [
            'Angola',
            'Algeria',
            'Cameroon',
            'DR Congo',
            'Ghana',
            'Egypt',
            'Kenya',
            'Uganda',
            'Zimbabwe',
        ];
    }

    public function perPage(): int
    {
        return 3;
    }
}

Truncating a States

You can use Truncate attributes to limit the number of characters that should be return to the USSD application. This helps ensure your USSD application is in conformity with the character limit of your USSD Provider. This is very useful for dynamic content where you can not be certain on the total characters.

<?php

namespace App\Ussd\States;

use App\Models\Customer;
use Speso\Ussd\Attributes\Transition;
use Speso\Ussd\Contracts\State;
use Speso\Ussd\Decisions\Equal;
use Speso\Ussd\Decisions\Fallback;
use Speso\Ussd\Menu;
use Speso\Ussd\Record;


#[Transition(to: RegisterState::class, match: new Equal(1))]
#[Truncate(limit: 80, end: '#. More.', more: new Equal('#'))]
class DetailsState implements State
{
    public function render(Record $record): Menu
    {
        return Menu::build()
            ->line('Banc')
            ->format('Name: %s', $record->name)
            ->lineBreak()
            ->line('Your post')
            ->line($record->get('post'))
            ->text('Powered by Speso');
    }
}

Terminating a States

To indicate that a USSD application should Terminate, use the Terminate attribute.

<?php

namespace App\Ussd\States;

use Speso\Ussd\Attributes\Terminate;
use Speso\Ussd\Contracts\State;
use Speso\Ussd\Menu;

#[Terminate]
class HelplineState implements State
{
    public function render(): Menu
    {
        return Menu::build()
            ->line('Helpline')
            ->listing([
                'email: info@banc.co',
                'phone: +233 241 122 333'
            ]);
    }
}

Visualizing a Flow

Once a flow grows past a handful of states, it helps to see the whole thing at a glance. ussd:graph walks the Transition, Back and Terminate attributes starting from a given state and prints a Mermaid state diagram you can paste into anything that renders Mermaid (GitHub, GitLab, Notion, the Mermaid Live Editor, etc.).

php artisan ussd:graph "App\Ussd\States\WelcomeState"

Use --output to write the diagram to a file instead of the console:

php artisan ussd:graph "App\Ussd\States\WelcomeState" --output=ussd-flow.mmd

A Transition targeting an Action class (rather than a State) is rendered as a leaf node labelled (dynamic), since an action's next state is decided at runtime and can't be determined statically. A Back attribute is drawn as an edge to a shared "Previous state" node, since where it leads depends on the session's actual navigation history rather than being fixed in the flow's definition.

Clone this wiki locally