Initial Version
This commit is contained in:
228
core/lib/Service/ConfigurationService.php
Normal file
228
core/lib/Service/ConfigurationService.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\Db\DataStore;
|
||||
use KTXC\Db\Collection;
|
||||
use KTXC\Db\UTCDateTime;
|
||||
use KTXC\SessionTenant;
|
||||
|
||||
class ConfigurationService
|
||||
{
|
||||
// Service constants
|
||||
private const TABLE_NAME = 'system_configuration';
|
||||
// Type constants for configuration values
|
||||
public const TYPE_NULL = 0;
|
||||
public const TYPE_STRING = 1;
|
||||
public const TYPE_INTEGER = 2;
|
||||
public const TYPE_FLOAT = 3;
|
||||
public const TYPE_BOOLEAN = 4;
|
||||
public const TYPE_ARRAY = 5;
|
||||
public const TYPE_JSON = 6;
|
||||
|
||||
private Collection $collection;
|
||||
|
||||
public function __construct(
|
||||
DataStore $store,
|
||||
private readonly SessionTenant $tenant
|
||||
) {
|
||||
// DataStore provides selectCollection method
|
||||
$this->collection = $store->selectCollection(self::TABLE_NAME);
|
||||
$this->collection->createIndex(['did' => 1, 'path' => 1, 'key' => 1], ['unique' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a configuration value by path and key
|
||||
*/
|
||||
public function get(string $path, string $key, mixed $default = null, ?string $tenant = null): mixed
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
}
|
||||
|
||||
$doc = $this->collection->findOne(['did' => $tenant, 'path' => $path, 'key' => $key]);
|
||||
if (!$doc) { return $default; }
|
||||
$value = $doc['value'] ?? ($doc['default'] ?? null);
|
||||
if ($value === null) { return $default; }
|
||||
return $this->convertFromDatabase((string)$value, (int)$doc['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a configuration value
|
||||
*/
|
||||
public function set(string $path, string $key, mixed $value, mixed $default = null, ?string $tenant = null): bool
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
}
|
||||
|
||||
$type = $this->determineType($value);
|
||||
$serializedValue = $this->convertToDatabase($value, $type);
|
||||
$serializedDefault = $default !== null ? $this->convertToDatabase($default, $type) : null;
|
||||
$this->collection->updateOne(
|
||||
['did' => $tenant, 'path' => $path, 'key' => $key],
|
||||
['$set' => [
|
||||
'did' => $tenant,
|
||||
'path' => $path,
|
||||
'key' => $key,
|
||||
'value' => $serializedValue,
|
||||
'type' => $type,
|
||||
'default' => $serializedDefault,
|
||||
'updated_at' => $this->bsonUtcDateTime()
|
||||
], '$setOnInsert' => [ 'created_at' => $this->bsonUtcDateTime() ]],
|
||||
['upsert' => true]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configuration values for a specific path
|
||||
*/
|
||||
public function getByPath(?string $path = null, bool $subset = false, ?string $tenant = null): array
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
}
|
||||
|
||||
$filter = ['did' => $tenant];
|
||||
if ($path !== null) {
|
||||
if ($subset) {
|
||||
$filter['$or'] = [
|
||||
['path' => $path],
|
||||
['path' => ['$regex' => '^' . preg_quote($path, '/') . '/']]
|
||||
];
|
||||
} else {
|
||||
$filter['path'] = $path;
|
||||
}
|
||||
}
|
||||
$cursor = $this->collection->find($filter);
|
||||
$configurations = [];
|
||||
foreach ($cursor as $doc) {
|
||||
$value = $doc['value'] ?? ($doc['default'] ?? null);
|
||||
$convertedValue = $value !== null ? $this->convertFromDatabase((string)$value, (int)$doc['type']) : null;
|
||||
$configurations[$doc['path']] = [$doc['key'] => $convertedValue];
|
||||
}
|
||||
return $configurations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a configuration value
|
||||
*/
|
||||
public function delete(string $path, string $key, ?string $tenant = null): bool
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
}
|
||||
|
||||
$this->collection->deleteOne(['did' => $tenant, 'path' => $path, 'key' => $key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all configuration values for a specific path
|
||||
*/
|
||||
public function deleteByPath(string $path, bool $includeSubPaths = false, ?string $tenant = null): bool
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
}
|
||||
|
||||
$filter = ['did' => $tenant];
|
||||
if ($includeSubPaths) {
|
||||
$filter['$or'] = [
|
||||
['path' => $path],
|
||||
['path' => ['$regex' => '^' . preg_quote($path, '/') . '/']]
|
||||
];
|
||||
} else {
|
||||
$filter['path'] = $path;
|
||||
}
|
||||
$this->collection->deleteMany($filter);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a configuration exists
|
||||
*/
|
||||
public function exists(string $path, string $key, ?string $tenant = null): bool
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
}
|
||||
|
||||
return $this->collection->countDocuments(['did' => $tenant, 'path' => $path, 'key' => $key]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the type of a PHP value
|
||||
*/
|
||||
private function determineType(mixed $value): int
|
||||
{
|
||||
return match (true) {
|
||||
is_null($value) => self::TYPE_NULL,
|
||||
is_bool($value) => self::TYPE_BOOLEAN,
|
||||
is_int($value) => self::TYPE_INTEGER,
|
||||
is_float($value) => self::TYPE_FLOAT,
|
||||
is_array($value) => self::TYPE_ARRAY,
|
||||
is_string($value) && $this->isJson($value) => self::TYPE_JSON,
|
||||
default => self::TYPE_STRING
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a PHP value to database format
|
||||
*/
|
||||
private function convertToDatabase(mixed $value, int $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
self::TYPE_NULL => '',
|
||||
self::TYPE_BOOLEAN => $value ? '1' : '0',
|
||||
self::TYPE_INTEGER => (string)$value,
|
||||
self::TYPE_FLOAT => (string)$value,
|
||||
self::TYPE_ARRAY, self::TYPE_JSON => json_encode($value),
|
||||
default => (string)$value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a database value to PHP format
|
||||
*/
|
||||
private function convertFromDatabase(string $value, int $type): mixed
|
||||
{
|
||||
return match ($type) {
|
||||
self::TYPE_NULL => null,
|
||||
self::TYPE_BOOLEAN => $value === '1',
|
||||
self::TYPE_INTEGER => (int)$value,
|
||||
self::TYPE_FLOAT => (float)$value,
|
||||
self::TYPE_ARRAY, self::TYPE_JSON => json_decode($value, true),
|
||||
default => $value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is valid JSON
|
||||
*/
|
||||
private function isJson(string $string): bool
|
||||
{
|
||||
json_decode($string);
|
||||
return json_last_error() === JSON_ERROR_NONE;
|
||||
}
|
||||
/**
|
||||
* Create a UTCDateTime for timestamp fields
|
||||
*/
|
||||
private function bsonUtcDateTime(): UTCDateTime
|
||||
{
|
||||
return UTCDateTime::now();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user