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.
This commit is contained in:
parent
af416b1b7c
commit
2ac8578fe0
6 changed files with 260 additions and 0 deletions
|
|
@ -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();
|
||||
|
|
|
|||
74
src/Presentation/Console/Commands/ListClientsCommand.php
Normal file
74
src/Presentation/Console/Commands/ListClientsCommand.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Console\Commands;
|
||||
|
||||
use Application\Security\Usecases\FindAllClients\FindAllClients;
|
||||
use Application\Security\Usecases\FindAllClients\Input;
|
||||
use Illuminate\Console\Command;
|
||||
use Presentation\Console\Presenters\Clients\ClientRow;
|
||||
use Presentation\Console\Presenters\Clients\ListClientsPresenter;
|
||||
use Presentation\Console\Presenters\Clients\ListClientsView;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseBuilder;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
|
||||
use Shared\Pagination;
|
||||
|
||||
final class ListClientsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'client:list
|
||||
{--page=1 : The page number}
|
||||
{--per-page=25 : The number of clients per page}';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'List all registered API clients';
|
||||
|
||||
public function handle(
|
||||
FindAllClients $usecase,
|
||||
UsecaseExecutor $executor,
|
||||
ListClientsPresenter $presenter,
|
||||
): int {
|
||||
$pagination = $this->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)
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/Presentation/Console/Presenters/Clients/ClientRow.php
Normal file
17
src/Presentation/Console/Presenters/Clients/ClientRow.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Console\Presenters\Clients;
|
||||
|
||||
final readonly class ClientRow
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $name,
|
||||
public string $identifier,
|
||||
public string $status,
|
||||
public string $lastUpdatedAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Console\Presenters\Clients;
|
||||
|
||||
use Application\Security\Usecases\FindAllClients\Output;
|
||||
use Application\Security\Usecases\FindAllClients\PaginatedClient;
|
||||
use Shared\DDD\Usecase\Output as UsecaseOutput;
|
||||
use Shared\DDD\Usecase\Presenter;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements Presenter<Output, ListClientsView> */
|
||||
final readonly class ListClientsPresenter implements Presenter
|
||||
{
|
||||
#[\Override]
|
||||
public function present(UsecaseOutput $output): object
|
||||
{
|
||||
Assert::isInstanceOf($output, Output::class);
|
||||
|
||||
/** @var list<ClientRow> $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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Console\Presenters\Clients;
|
||||
|
||||
final readonly class ListClientsView
|
||||
{
|
||||
/**
|
||||
* @param list<string> $headers
|
||||
* @param list<ClientRow> $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))";
|
||||
}
|
||||
}
|
||||
90
tests/Feature/Console/Commands/ListClientsCommandTest.php
Normal file
90
tests/Feature/Console/Commands/ListClientsCommandTest.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Console\Commands;
|
||||
|
||||
use Infra\Database\Models\ApiClient;
|
||||
use Presentation\Console\Commands\ListClientsCommand;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(ListClientsCommand::class);
|
||||
|
||||
describe('ListClientsCommand', function (): void {
|
||||
it('outputs message when no clients exist', function (): void {
|
||||
// GIVEN
|
||||
ApiClient::query()->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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue