From 2ac8578fe0afd2d48ece6c3703c47f1052ae6d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Tue, 18 Aug 2026 12:50:55 +0200 Subject: [PATCH] feat(console): add ListClients command and related presenters - Implemented ListClientsCommand to list API clients with pagination. - Created ClientRow, ListClientsPresenter, and ListClientsView for structured output. - Added tests for ListClientsCommand to ensure correct functionality. --- bootstrap/app.php | 3 + .../Console/Commands/ListClientsCommand.php | 74 +++++++++++++++ .../Console/Presenters/Clients/ClientRow.php | 17 ++++ .../Clients/ListClientsPresenter.php | 44 +++++++++ .../Presenters/Clients/ListClientsView.php | 32 +++++++ .../Commands/ListClientsCommandTest.php | 90 +++++++++++++++++++ 6 files changed, 260 insertions(+) create mode 100644 src/Presentation/Console/Commands/ListClientsCommand.php create mode 100644 src/Presentation/Console/Presenters/Clients/ClientRow.php create mode 100644 src/Presentation/Console/Presenters/Clients/ListClientsPresenter.php create mode 100644 src/Presentation/Console/Presenters/Clients/ListClientsView.php create mode 100644 tests/Feature/Console/Commands/ListClientsCommandTest.php diff --git a/bootstrap/app.php b/bootstrap/app.php index 6835c00..99aef5f 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -12,6 +12,9 @@ $app = Application::configure(Env::get('APP_BASE_PATH', dirname(__DIR__))) commands: __DIR__ . '/../routes/console.php', health: '/up', ) + ->withCommands([ + __DIR__ . '/../src/Presentation/Console/Commands', + ]) ->withMiddleware(require __DIR__ . '/middlewares.php') ->withExceptions(require __DIR__ . '/exceptions.php') ->create(); diff --git a/src/Presentation/Console/Commands/ListClientsCommand.php b/src/Presentation/Console/Commands/ListClientsCommand.php new file mode 100644 index 0000000..4097553 --- /dev/null +++ b/src/Presentation/Console/Commands/ListClientsCommand.php @@ -0,0 +1,74 @@ +getPaginationFromOptions(); + $input = new Input($pagination); + + $view = UsecaseBuilder::using($executor) + ->do($usecase) + ->from($input) + ->with($presenter) + ->run(); + + return $this->renderView($view); + } + + private function renderView(ListClientsView $view): int + { + if ($view->isEmpty()) { + $this->info('No clients found.'); + + return self::SUCCESS; + } + + $tableRows = array_map( + static fn (ClientRow $row): array => array_values(get_object_vars($row)), + $view->rows + ); + + $this->table($view->headers, $tableRows); + $this->info($view->paginationSummary()); + + return self::SUCCESS; + } + + private function getPaginationFromOptions(): Pagination + { + return Pagination::ofPage( + max(1, (int) $this->option('page')), + clamp(1, (int) $this->option('per-page'), Pagination::MAX_PAGE_SIZE) + ); + } +} diff --git a/src/Presentation/Console/Presenters/Clients/ClientRow.php b/src/Presentation/Console/Presenters/Clients/ClientRow.php new file mode 100644 index 0000000..bd6926a --- /dev/null +++ b/src/Presentation/Console/Presenters/Clients/ClientRow.php @@ -0,0 +1,17 @@ + */ +final readonly class ListClientsPresenter implements Presenter +{ + #[\Override] + public function present(UsecaseOutput $output): object + { + Assert::isInstanceOf($output, Output::class); + + /** @var list $rows */ + $rows = array_values(array_map( + static fn (PaginatedClient $client): ClientRow => new ClientRow( + id: $client->id->value, + name: $client->name->value, + identifier: $client->identifier->value, + status: $client->isActive ? 'Active' : 'Inactive', + lastUpdatedAt: $client->updatedAt->format('Y-m-d H:i:s'), + ), + $output->clients + )); + + $totalPages = $output->perPage > 0 ? (int) ceil($output->total / $output->perPage) : 0; + + return new ListClientsView( + headers: ['ID', 'Name', 'Identifier', 'Status', 'Last Updated At'], + rows: $rows, + total: $output->total, + page: $output->page, + perPage: $output->perPage, + totalPages: $totalPages, + ); + } +} diff --git a/src/Presentation/Console/Presenters/Clients/ListClientsView.php b/src/Presentation/Console/Presenters/Clients/ListClientsView.php new file mode 100644 index 0000000..3ecef9b --- /dev/null +++ b/src/Presentation/Console/Presenters/Clients/ListClientsView.php @@ -0,0 +1,32 @@ + $headers + * @param list $rows + */ + public function __construct( + public array $headers, + public array $rows, + public int $total, + public int $page, + public int $perPage, + public int $totalPages, + ) { + } + + public function isEmpty(): bool + { + return $this->total === 0; + } + + public function paginationSummary(): string + { + return "Showing page {$this->page}/{$this->totalPages} (Total: {$this->total} client(s))"; + } +} diff --git a/tests/Feature/Console/Commands/ListClientsCommandTest.php b/tests/Feature/Console/Commands/ListClientsCommandTest.php new file mode 100644 index 0000000..d962d59 --- /dev/null +++ b/tests/Feature/Console/Commands/ListClientsCommandTest.php @@ -0,0 +1,90 @@ +delete(); + + // WHEN & THEN + $this->artisan('client:list') + ->expectsOutput('No clients found.') + ->assertSuccessful(); + }); + + it('lists clients in a table when clients exist', function (): void { + // GIVEN + ApiClient::query()->delete(); + + $client1 = ApiClient::query()->create([ + 'id' => (string) Uuid::uuid7(), + 'name' => 'Gateway Alpha', + 'identifier' => 'arcadia_id_' . str_repeat('a', 32), + 'secret' => 'arcadia_sec_' . str_repeat('b', 86), + 'is_active' => true, + ]); + + $client2 = ApiClient::query()->create([ + 'id' => (string) Uuid::uuid7(), + 'name' => 'Gateway Beta', + 'identifier' => 'arcadia_id_' . str_repeat('c', 32), + 'secret' => 'arcadia_sec_' . str_repeat('d', 86), + 'is_active' => false, + ]); + + // WHEN & THEN + $this->artisan('client:list') + ->expectsTable( + ['ID', 'Name', 'Identifier', 'Status', 'Created At', 'Updated At'], + [ + [ + (string) $client1->id, + (string) $client1->name->value, + (string) $client1->identifier->value, + 'Active', + $client1->created_at->format('Y-m-d H:i:s'), + $client1->updated_at->format('Y-m-d H:i:s'), + ], + [ + (string) $client2->id, + (string) $client2->name->value, + (string) $client2->identifier->value, + 'Inactive', + $client2->created_at->format('Y-m-d H:i:s'), + $client2->updated_at->format('Y-m-d H:i:s'), + ], + ] + ) + ->expectsOutput('Showing page 1/1 (Total: 2 client(s))') + ->assertSuccessful(); + }); + + it('respects pagination options', function (): void { + // GIVEN + ApiClient::query()->delete(); + + for ($i = 1; $i <= 3; $i++) { + ApiClient::query()->create([ + 'id' => (string) Uuid::uuid7(), + 'name' => "Client {$i}", + 'identifier' => 'arcadia_id_' . str_repeat((string) $i, 32), + 'secret' => 'arcadia_sec_' . str_repeat((string) $i, 86), + 'is_active' => true, + ]); + } + + // WHEN & THEN + $this->artisan('client:list', ['--page' => '2', '--per-page' => '2']) + ->expectsOutput('Showing page 2/2 (Total: 3 client(s))') + ->assertSuccessful(); + }); +});