Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 4ae6befc7b
422 changed files with 47225 additions and 0 deletions

View 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();
}
}