refactor(health): remove toArray from HealthCheckResult DTO and encapsulate serialization in HealthCheckResponse

This commit is contained in:
Jérémy Munsch 2026-08-27 15:19:36 +02:00
parent 403ea2c6c2
commit 197aedbc5e
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
8 changed files with 221 additions and 93 deletions

View file

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Application\Health\Dto;
use Application\Health\Enums\HealthStatus;
use Shared\Time\Utc;
final readonly class HealthCheckResult
{
@ -20,62 +19,4 @@ final readonly class HealthCheckResult
{
return $this->status === HealthStatus::HEALTHY;
}
/**
* @return array{
* status: 'healthy'|'unhealthy',
* timestamp: string,
* checks: array{
* database_default: array{status: 'up'|'down', connection: string, message?: string|null},
* database_arcadia: array{status: 'up'|'down', connection: string, message?: string|null},
* cache: array{status: 'up'|'down', store: string, message?: string|null},
* mail_smtp: array{status: 'up'|'down', transport: string, message?: string|null}
* }
* }
*/
public function toArray(): array
{
$defaultDb = [
'status' => $this->checks->databaseDefault->status->value,
'connection' => $this->checks->databaseDefault->connection,
];
if ($this->checks->databaseDefault->message !== null) {
$defaultDb['message'] = $this->checks->databaseDefault->message;
}
$arcadiaDb = [
'status' => $this->checks->databaseArcadia->status->value,
'connection' => $this->checks->databaseArcadia->connection,
];
if ($this->checks->databaseArcadia->message !== null) {
$arcadiaDb['message'] = $this->checks->databaseArcadia->message;
}
$cache = [
'status' => $this->checks->cache->status->value,
'store' => $this->checks->cache->store,
];
if ($this->checks->cache->message !== null) {
$cache['message'] = $this->checks->cache->message;
}
$mail = [
'status' => $this->checks->mailSmtp->status->value,
'transport' => $this->checks->mailSmtp->transport,
];
if ($this->checks->mailSmtp->message !== null) {
$mail['message'] = $this->checks->mailSmtp->message;
}
return [
'status' => $this->status->value,
'timestamp' => Utc::toRfc3339($this->timestamp),
'checks' => [
'database_default' => $defaultDb,
'database_arcadia' => $arcadiaDb,
'cache' => $cache,
'mail_smtp' => $mail,
],
];
}
}

View file

@ -74,7 +74,7 @@ final class HealthCheckJob implements ShouldQueue
LoggerInterface $logger,
): void {
$failedComponents = [];
$checksArray = $result->toArray()['checks'];
$checksArray = $this->formatChecks($result);
foreach ($checksArray as $component => $data) {
if ($data['status'] === 'down') {
@ -109,7 +109,7 @@ final class HealthCheckJob implements ShouldQueue
HealthAlertMailConfig $mailConfig,
LoggerInterface $logger,
): void {
$checksArray = $result->toArray()['checks'];
$checksArray = $this->formatChecks($result);
$logger->info('System health check recovered: all components are healthy.', [
'status' => $result->status->value,
@ -158,9 +158,59 @@ final class HealthCheckJob implements ShouldQueue
$payload = [
'status' => $result->status->value,
'timestamp' => Utc::toRfc3339($result->timestamp),
'checks' => $result->toArray()['checks'],
'checks' => $this->formatChecks($result),
];
File::put($filePath, (string) json_encode($payload, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
}
/**
* @return array{
* database_default: array{status: string, connection: string, message?: string|null},
* database_arcadia: array{status: string, connection: string, message?: string|null},
* cache: array{status: string, store: string, message?: string|null},
* mail_smtp: array{status: string, transport: string, message?: string|null}
* }
*/
private function formatChecks(HealthCheckResult $result): array
{
$defaultDb = [
'status' => $result->checks->databaseDefault->status->value,
'connection' => $result->checks->databaseDefault->connection,
];
if ($result->checks->databaseDefault->message !== null) {
$defaultDb['message'] = $result->checks->databaseDefault->message;
}
$arcadiaDb = [
'status' => $result->checks->databaseArcadia->status->value,
'connection' => $result->checks->databaseArcadia->connection,
];
if ($result->checks->databaseArcadia->message !== null) {
$arcadiaDb['message'] = $result->checks->databaseArcadia->message;
}
$cache = [
'status' => $result->checks->cache->status->value,
'store' => $result->checks->cache->store,
];
if ($result->checks->cache->message !== null) {
$cache['message'] = $result->checks->cache->message;
}
$mail = [
'status' => $result->checks->mailSmtp->status->value,
'transport' => $result->checks->mailSmtp->transport,
];
if ($result->checks->mailSmtp->message !== null) {
$mail['message'] = $result->checks->mailSmtp->message;
}
return [
'database_default' => $defaultDb,
'database_arcadia' => $arcadiaDb,
'cache' => $cache,
'mail_smtp' => $mail,
];
}
}

View file

@ -6,6 +6,7 @@ namespace Presentation\Api\V1\Http\Controllers\Health;
use Application\Health\Contracts\HealthChecker;
use Illuminate\Http\JsonResponse;
use Presentation\Api\V1\Http\Responses\Health\HealthCheckResponse;
final readonly class HealthCheckController
{
@ -18,9 +19,6 @@ final readonly class HealthCheckController
{
$result = $this->healthChecker->check();
return new JsonResponse(
data: $result->toArray(),
status: $result->isHealthy() ? 200 : 503,
);
return (new HealthCheckResponse($result))->present();
}
}

View file

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Presentation\Api\V1\Http\Responses\Health;
use Application\Health\Dto\HealthCheckResult;
use Illuminate\Http\JsonResponse;
use Presentation\Api\V1\Contracts\ApiResponse;
use Shared\Time\Utc;
final readonly class HealthCheckResponse implements ApiResponse
{
public function __construct(
private HealthCheckResult $result,
) {
}
#[\Override]
public function toResponse($request): JsonResponse
{
return $this->present();
}
#[\Override]
public function present(): JsonResponse
{
return new JsonResponse(
data: $this->toArray(),
status: $this->result->isHealthy() ? 200 : 503,
);
}
/**
* @return array{
* status: 'healthy'|'unhealthy',
* timestamp: string,
* checks: array{
* database_default: array{status: 'up'|'down', connection: string, message?: string|null},
* database_arcadia: array{status: 'up'|'down', connection: string, message?: string|null},
* cache: array{status: 'up'|'down', store: string, message?: string|null},
* mail_smtp: array{status: 'up'|'down', transport: string, message?: string|null}
* }
* }
*/
public function toArray(): array
{
$defaultDb = [
'status' => $this->result->checks->databaseDefault->status->value,
'connection' => $this->result->checks->databaseDefault->connection,
];
if ($this->result->checks->databaseDefault->message !== null) {
$defaultDb['message'] = $this->result->checks->databaseDefault->message;
}
$arcadiaDb = [
'status' => $this->result->checks->databaseArcadia->status->value,
'connection' => $this->result->checks->databaseArcadia->connection,
];
if ($this->result->checks->databaseArcadia->message !== null) {
$arcadiaDb['message'] = $this->result->checks->databaseArcadia->message;
}
$cache = [
'status' => $this->result->checks->cache->status->value,
'store' => $this->result->checks->cache->store,
];
if ($this->result->checks->cache->message !== null) {
$cache['message'] = $this->result->checks->cache->message;
}
$mail = [
'status' => $this->result->checks->mailSmtp->status->value,
'transport' => $this->result->checks->mailSmtp->transport,
];
if ($this->result->checks->mailSmtp->message !== null) {
$mail['message'] = $this->result->checks->mailSmtp->message;
}
return [
'status' => $this->result->status->value,
'timestamp' => Utc::toRfc3339($this->result->timestamp),
'checks' => [
'database_default' => $defaultDb,
'database_arcadia' => $arcadiaDb,
'cache' => $cache,
'mail_smtp' => $mail,
],
];
}
}

View file

@ -7,6 +7,7 @@ namespace Presentation\Console\Commands;
use Application\Health\Contracts\HealthChecker;
use Application\Health\Dto\HealthCheckResult;
use Illuminate\Console\Command;
use Presentation\Api\V1\Http\Responses\Health\HealthCheckResponse;
final class HealthCheckCommand extends Command
{
@ -33,7 +34,7 @@ final class HealthCheckCommand extends Command
if ($this->option('json')) {
$this->line((string) json_encode(
$result->toArray(),
(new HealthCheckResponse($result))->toArray(),
\JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES
));

View file

@ -63,10 +63,10 @@ describe('HealthCheckCommand', function (): void {
->assertSuccessful();
});
it('outputs JSON when --json option is passed and fails when unhealthy', function () use (&$originalArcadiaDb): void {
it('outputs JSON when --json option is passed and fails when unhealthy', function () use (&$originalDbDefault): void {
// GIVEN
/** @var \Tests\TestCase $this */
Config::set('database.arcadia_meta_model', 'invalid_arcadia_connection');
Config::set('database.default', 'invalid_connection');
// WHEN
$this->artisan('health:check', ['--json' => true])

View file

@ -100,7 +100,7 @@ describe('Health Check DTOs & Enums Unit Tests', function (): void {
->and($checksDown->isAllUp())->toBeFalse();
});
it('centralizes complete mapping in HealthCheckResult', function (): void {
it('instantiates HealthCheckResult and evaluates isHealthy', function (): void {
// GIVEN
$date = new \DateTimeImmutable('2026-08-21 14:30:00 UTC');
$checks = new HealthChecks(
@ -116,29 +116,10 @@ describe('Health Check DTOs & Enums Unit Tests', function (): void {
checks: $checks,
);
// WHEN
$array = $result->toArray();
// THEN
// WHEN & THEN
expect($result->isHealthy())->toBeFalse()
->and($array['status'])->toBe('unhealthy')
->and($array['timestamp'])->toBe('2026-08-21T14:30:00+00:00')
->and($array['checks']['database_default'])->toBe([
'status' => 'up',
'connection' => 'sqlite',
])
->and($array['checks']['database_arcadia'])->toBe([
'status' => 'down',
'connection' => 'arcadia',
'message' => 'Timeout connecting to DB',
])
->and($array['checks']['cache'])->toBe([
'status' => 'up',
'store' => 'database',
])
->and($array['checks']['mail_smtp'])->toBe([
'status' => 'up',
'transport' => 'smtp',
]);
->and($result->status)->toBe(HealthStatus::UNHEALTHY)
->and($result->timestamp)->toBe($date)
->and($result->checks->databaseArcadia->message)->toBe('Timeout connecting to DB');
});
});

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Presentation\Api\V1\Http\Responses\Health;
use Application\Health\Dto\CacheCheckResult;
use Application\Health\Dto\DatabaseCheckResult;
use Application\Health\Dto\HealthCheckResult;
use Application\Health\Dto\HealthChecks;
use Application\Health\Dto\MailCheckResult;
use Application\Health\Enums\ComponentStatus;
use Application\Health\Enums\HealthStatus;
use Presentation\Api\V1\Http\Responses\Health\HealthCheckResponse;
covers(HealthCheckResponse::class);
describe('HealthCheckResponse Unit Test', function (): void {
it('serializes healthy HealthCheckResult into 200 JsonResponse and array', function (): void {
// GIVEN
$date = new \DateTimeImmutable('2026-08-27 15:00:00 UTC');
$checks = new HealthChecks(
databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'),
databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'),
cache: new CacheCheckResult(ComponentStatus::UP, 'file'),
mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'),
);
$result = new HealthCheckResult(HealthStatus::HEALTHY, $date, $checks);
$response = new HealthCheckResponse($result);
// WHEN
$array = $response->toArray();
$json = $response->present();
// THEN
expect($json->getStatusCode())->toBe(200)
->and($array['status'])->toBe('healthy')
->and($array['timestamp'])->toBe('2026-08-27T15:00:00+00:00')
->and($array['checks']['database_default']['status'])->toBe('up')
->and($array['checks']['database_arcadia']['status'])->toBe('up');
});
it('serializes unhealthy HealthCheckResult into 503 JsonResponse and array', function (): void {
// GIVEN
$date = new \DateTimeImmutable('2026-08-27 15:00:00 UTC');
$checks = new HealthChecks(
databaseDefault: new DatabaseCheckResult(ComponentStatus::DOWN, 'sqlite', 'Connection refused'),
databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'),
cache: new CacheCheckResult(ComponentStatus::UP, 'file'),
mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'),
);
$result = new HealthCheckResult(HealthStatus::UNHEALTHY, $date, $checks);
$response = new HealthCheckResponse($result);
// WHEN
$array = $response->toArray();
$json = $response->present();
// THEN
expect($json->getStatusCode())->toBe(503)
->and($array['status'])->toBe('unhealthy')
->and($array['checks']['database_default']['message'])->toBe('Connection refused');
});
});