diff --git a/readme.md b/readme.md index 3814c8f..973790a 100644 --- a/readme.md +++ b/readme.md @@ -11,7 +11,7 @@ Build Ussd (Unstructured Supplementary Service Data) applications with laravel w You can install the package via composer: ``` bash -composer require sparors/laravel-ussd:^3.0 +composer require sparors/laravel-ussd:dev-3.x ``` Laravel Ussd provides zero configuration out of the box. To publish the config, run the vendor publish command: diff --git a/src/Attributes/Paginate.php b/src/Attributes/Paginate.php new file mode 100644 index 0000000..2c0bf7f --- /dev/null +++ b/src/Attributes/Paginate.php @@ -0,0 +1,16 @@ +getItems()) / $this->perPage()); + } + + public function currentPage(): int + { + if (isset(self::$currentPage)) { + return self::$currentPage; + } + + /** @var Record */ $record = App::make(Record::class); + $pageId = Str::of(get_called_class())->replace('\\', '')->snake()->append('_page')->value(); + $page = $record->get($pageId, 1); + + self::$currentPage = $page; + + return $page; + } + + public function isFirstPage(): int + { + return 1 === $this->currentPage(); + } + + public function isLastPage(): int + { + return $this->currentPage() === $this->lastPage(); + } + + public function hasNextPage(): bool + { + return $this->currentPage() < $this->lastPage(); + } + + public function hasPreviousPage(): bool + { + return $this->currentPage() > 1; + } + + abstract public function getItems(): array; + + abstract public function perPage(): int; + + public function __destruct() + { + self::$currentPage = null; + } +} diff --git a/src/Ussd.php b/src/Ussd.php index 6877ea6..b031074 100644 --- a/src/Ussd.php +++ b/src/Ussd.php @@ -3,24 +3,27 @@ namespace Sparors\Ussd; use Closure; -use DateInterval; -use DateTimeInterface; use Exception; +use DateInterval; use ReflectionClass; +use DateTimeInterface; +use Illuminate\Support\Str; use InvalidArgumentException; use Sparors\Ussd\Contracts\State; use Sparors\Ussd\Contracts\Action; use Illuminate\Support\Facades\App; +use Sparors\Ussd\Tests\PendingTest; use Sparors\Ussd\Contracts\Response; +use Sparors\Ussd\Attributes\Paginate; use Sparors\Ussd\Attributes\Terminate; use Sparors\Ussd\Attributes\Transition; +use Sparors\Ussd\Traits\WithPagination; use Sparors\Ussd\Contracts\Configurator; +use Sparors\Ussd\Contracts\InitialState; use Sparors\Ussd\Contracts\ContinueState; -use Sparors\Ussd\Contracts\ExceptionHandler; use Sparors\Ussd\Contracts\InitialAction; -use Sparors\Ussd\Contracts\InitialState; +use Sparors\Ussd\Contracts\ExceptionHandler; use Sparors\Ussd\Exceptions\NextStateNotFoundException; -use Sparors\Ussd\Tests\PendingTest; class Ussd { @@ -252,8 +255,7 @@ private function operate(): array $state = App::make($nextState); - /** @var Menu */ - $menu = App::call([$state, 'render']); + /** @var Menu */ $menu = App::call([$state, 'render']); return [(string) $menu, $this->terminating($state)]; } @@ -262,6 +264,49 @@ private function next(State $state): string { $reflected = new ReflectionClass($state); + $attributes = $reflected->getAttributes(Paginate::class); + + foreach ($attributes as $attribute) { + $paginate = $attribute->newInstance(); + + foreach(['next', 'previous'] as $key) { + if (is_array($paginate->{$key})) { + $paginate->{$key} = new $paginate->{$key}[0](...array_slice($paginate->{$key}, 1)); + } elseif (is_string($paginate->{$key})) { + $paginate->{$key} = new $paginate->{$key}; + } elseif (is_null($paginate->{$key})) { + continue; + } + + if ($paginate->{$key}->decide($this->context->input())) { + if (class_uses($state)[WithPagination::class] ?? null) { + /** @var WithPagination $state */ + /** @var Record */ $record = App::make(Record::class); + $pageId = Str::of($state::class)->replace('\\', '')->snake()->append('_page')->value(); + $page = $record->get($pageId, 1); + + if ('next' === $key && $state->hasNextPage()) { + $record->set($pageId, $page + 1); + } elseif ('previous' === $key && $state->hasPreviousPage()) { + $record->set($pageId, $page - 1); + } else { + continue; + } + } + + if ($paginate->callback) { + if (is_array($paginate->callback) && is_string($paginate->callback[0])) { + $paginate->callback[0] = App::make($paginate->callback[0]); + } + + App::call($paginate->callback); + } + + return $state::class; + } + } + } + $attributes = $reflected->getAttributes(Transition::class); foreach($attributes as $attribute) { diff --git a/tests/Dummy/IntermediateState.php b/tests/Dummy/IntermediateState.php index d4c0a56..aff6320 100644 --- a/tests/Dummy/IntermediateState.php +++ b/tests/Dummy/IntermediateState.php @@ -2,21 +2,37 @@ namespace Sparors\Ussd\Tests\Dummy; +use Sparors\Ussd\Attributes\Paginate; use Sparors\Ussd\Attributes\Transition; use Sparors\Ussd\Menu; use Sparors\Ussd\Contracts\State; use Sparors\Ussd\Decisions\Equal; use Sparors\Ussd\Record; +use Sparors\Ussd\Traits\WithPagination; +#[Paginate([Equal::class, '#'], [Equal::class, '0'])] #[Transition(GrandAction::class, [Equal::class, 1], DoTheThing::class)] class IntermediateState implements State { + use WithPagination; + public function render(Record $record): Menu { return Menu::build() - ->text('Now see the magic...') + ->text('Pick one...') ->when($record->has('wow'), function (Menu $menu) { - $menu->lineBreak()->text('Booooom!'); - }); + $menu->line('Booooom!'); + }) + ->listing($this->getItems(), page: $this->currentPage(), perPage: $this->perPage()); + } + + public function getItems(): array + { + return ['Foo', 'Bar', 'Baz']; + } + + public function perPage(): int + { + return 2; } } diff --git a/tests/Integration/AssestionTest.php b/tests/Integration/AssestionTest.php index 2b64bd7..0abdf69 100644 --- a/tests/Integration/AssestionTest.php +++ b/tests/Integration/AssestionTest.php @@ -23,12 +23,16 @@ public function test_assestion_runs_successfully() ->assertContextMissing('baz') ->assertRecordMissing('wow') ->input('1') - ->assertSee('Now see the magic...') + ->assertSee('Pick one...') + ->assertSee('Foo') ->assertRecordHas('wow') + ->input('#') + ->assertSee('Pick one...') + ->assertSee('Baz') ->actingAs('benjamin') ->assertSee('In the beginning...') ->input('1') - ->assertSee('Now see the magic...') + ->assertSee('Pick one...') ->actingAs('isaac') ->input('1') ->assertSee('Tadaa...') @@ -41,7 +45,7 @@ public function test_assestion_runs_successfully() ->input('9') ->assertSee('In the beginning...') ->input('1') - ->assertSee('Now see the magic...') + ->assertSee('Pick one...') ->actingAs('isaac'); } } diff --git a/tests/Integration/UssdTest.php b/tests/Integration/UssdTest.php index 4403f0f..6f4cfd3 100644 --- a/tests/Integration/UssdTest.php +++ b/tests/Integration/UssdTest.php @@ -31,7 +31,7 @@ public function test_ussd_runs_successfully() $this->assertEquals( [ - 'message' => "Now see the magic...\nBooooom!", + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", 'terminating' => false ], Ussd::build( @@ -54,6 +54,96 @@ public function test_ussd_runs_successfully() ); } + public function test_ussd_can_paginate() + { + $this->assertEquals( + [ + 'message' => 'In the beginning...', + 'terminating' => false, + ], + Ussd::build( + Context::create('1234', '7890', '1') + ) + ->useInitialState(BeginningState::class) + ->run() + ); + + $this->assertEquals( + [ + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", + 'terminating' => false + ], + Ussd::build( + Context::create('1234', '7890', '1') + ) + ->useInitialState(BeginningState::class) + ->run() + ); + + $this->assertEquals( + [ + 'message' => "Pick one...Booooom!\n3.Baz\n", + 'terminating' => false + ], + Ussd::build( + Context::create('1234', '7890', '#') + ) + ->useInitialState(BeginningState::class) + ->run() + ); + } + + public function test_ussd_can_detect_end_of_paginate() + { + $this->assertEquals( + [ + 'message' => 'In the beginning...', + 'terminating' => false, + ], + Ussd::build( + Context::create('1234', '7890', '1') + ) + ->useInitialState(BeginningState::class) + ->run() + ); + + $this->assertEquals( + [ + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", + 'terminating' => false + ], + Ussd::build( + Context::create('1234', '7890', '1') + ) + ->useInitialState(BeginningState::class) + ->run() + ); + + $this->assertEquals( + [ + 'message' => "Pick one...Booooom!\n3.Baz\n", + 'terminating' => false + ], + Ussd::build( + Context::create('1234', '7890', '#') + ) + ->useInitialState(BeginningState::class) + ->run() + ); + + $this->assertEquals( + [ + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", + 'terminating' => false + ], + Ussd::build( + Context::create('1234', '7890', '0') + ) + ->useInitialState(BeginningState::class) + ->run() + ); + } + public function test_ussd_can_automatically_continue_from_old_session() { $this->assertEquals( @@ -71,7 +161,7 @@ public function test_ussd_can_automatically_continue_from_old_session() $this->assertEquals( [ - 'message' => "Now see the magic...\nBooooom!", + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", 'terminating' => false ], Ussd::build( @@ -113,7 +203,7 @@ public function test_ussd_can_automatically_start_from_old_session() $this->assertEquals( [ - 'message' => "Now see the magic...\nBooooom!", + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", 'terminating' => false ], Ussd::build( @@ -155,7 +245,7 @@ public function test_ussd_can_manually_continue_from_old_session() $this->assertEquals( [ - 'message' => "Now see the magic...\nBooooom!", + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", 'terminating' => false ], Ussd::build( @@ -210,7 +300,7 @@ public function test_ussd_can_manually_start_from_old_session() $this->assertEquals( [ - 'message' => "Now see the magic...\nBooooom!", + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", 'terminating' => false ], Ussd::build( @@ -265,7 +355,7 @@ public function test_ussd_can_automatically_continue_from_multiple_old_session() $this->assertEquals( [ - 'message' => "Now see the magic...\nBooooom!", + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", 'terminating' => false ], Ussd::build( @@ -320,7 +410,7 @@ public function test_ussd_can_manually_continue_from_multiple_old_session() $this->assertEquals( [ - 'message' => "Now see the magic...\nBooooom!", + 'message' => "Pick one...Booooom!\n1.Foo\n2.Bar\n", 'terminating' => false ], Ussd::build(