mdn-backend / app /Domain /Config /ConfigService.php
internationalscholarsprogram's picture
feat(membership): admin-driven account creation, real estate tier/investment work, in-progress monorepo migration state
b2dcf0f
Raw
History Blame Contribute Delete
3.68 kB
<?php
namespace App\Domain\Config;
use App\Domain\Audit\AuditService;
use App\Infrastructure\Ids\UuidGenerator;
use App\Shared\Exceptions\ApiException;
class ConfigService
{
public function __construct(
private readonly ConfigRepository $settings,
private readonly UuidGenerator $ids,
private readonly AuditService $audit,
) {
}
public function getNamespace(string $namespace): array
{
return [
'namespace' => $namespace,
'settings' => array_map(fn (array $row): array => $this->payload($row), $this->settings->listNamespace($namespace)),
];
}
public function update(string $namespace, string $key, mixed $value, string $actorUserId): array
{
$this->validateValue($namespace, $key, $value);
$id = $this->settings->createVersion($this->ids->generate(), $namespace, $key, $value);
$this->audit->record('CONFIG_SETTING_UPDATED', $actorUserId, null, 'configuration_settings', $id, 'SUCCESS', [
'namespace' => $namespace,
'key' => $key,
]);
return ['setting' => $this->payload((array) $this->settings->latest($namespace, $key))];
}
public function publish(string $namespace, string $key, string $actorUserId): array
{
$setting = $this->settings->latest($namespace, $key);
if ($setting === null) {
throw new ApiException('CONFIG_NOT_FOUND', 'Configuration setting was not found.', [], 404);
}
$this->settings->publish($setting->id, $actorUserId);
$this->audit->record('CONFIG_SETTING_PUBLISHED', $actorUserId, null, 'configuration_settings', $setting->id, 'SUCCESS', [
'namespace' => $namespace,
'key' => $key,
]);
return ['setting' => $this->payload((array) $this->settings->latest($namespace, $key))];
}
public function versions(string $namespace, string $key): array
{
return [
'namespace' => $namespace,
'key' => $key,
'versions' => array_map(fn (array $row): array => $this->payload($row), $this->settings->versions($namespace, $key)),
];
}
private function payload(array $row): array
{
return [
'id' => $row['id'],
'namespace' => $row['namespace'],
'key' => $row['setting_key'],
'value' => json_decode($row['setting_value'], true, flags: JSON_THROW_ON_ERROR),
'status' => $row['status'],
'version_number' => (int) $row['version_number'],
'published_at' => $row['published_at'],
'updated_at' => $row['updated_at'],
];
}
private function validateValue(string $namespace, string $key, mixed $value): void
{
if (str_contains(strtolower($key), 'secret') || str_contains(strtolower($key), 'password')) {
throw new ApiException('SECRET_VALUE_NOT_ALLOWED', 'Raw secrets must not be stored in configuration settings.', [], 422);
}
if (str_starts_with($namespace.'.'.$key, 'fees.') && (! is_numeric($value) || (float) $value < 0)) {
throw new ApiException('INVALID_CONFIG_VALUE', 'Fee values must be non-negative numbers.', [], 422);
}
if (str_contains($key, 'days') && (! is_int($value) || $value < 0)) {
throw new ApiException('INVALID_CONFIG_VALUE', 'Day values must be non-negative integers.', [], 422);
}
if (str_contains($key, 'minutes') && (! is_int($value) || $value < 1)) {
throw new ApiException('INVALID_CONFIG_VALUE', 'Minute values must be positive integers.', [], 422);
}
}
}