refactor(health): remove toArray from HealthCheckResult DTO and encapsulate serialization in HealthCheckResponse
This commit is contained in:
parent
403ea2c6c2
commit
197aedbc5e
8 changed files with 221 additions and 93 deletions
|
|
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue