09355d14a4
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
226 lines
7.1 KiB
PHP
226 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace KTXC\Stores;
|
|
|
|
use KTXC\Db\DataStore;
|
|
use KTXC\Db\ObjectId;
|
|
use KTXC\Models\Tenant\TenantObject;
|
|
|
|
class TenantStore
|
|
{
|
|
|
|
protected const COLLECTION_NAME = 'tenants';
|
|
|
|
public function __construct(
|
|
protected readonly DataStore $dataStore
|
|
) { }
|
|
|
|
public function list(): array
|
|
{
|
|
$cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find();
|
|
$list = [];
|
|
foreach ($cursor as $entry) {
|
|
$entry = (new TenantObject())->jsonDeserialize((array)$entry);
|
|
$list[$entry->getId()] = $entry;
|
|
}
|
|
return $list;
|
|
}
|
|
|
|
public function fetch(string $identifier): ?TenantObject
|
|
{
|
|
$entry = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne(['identifier' => $identifier]);
|
|
if (!$entry) { return null; }
|
|
return (new TenantObject())->jsonDeserialize((array)$entry);
|
|
}
|
|
|
|
public function fetchByDomain(string $domain): ?TenantObject
|
|
{
|
|
$entry = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne(['domains' => $domain]);
|
|
if (!$entry) { return null; }
|
|
$entity = new TenantObject();
|
|
$entity->jsonDeserialize((array)$entry);
|
|
return $entity;
|
|
}
|
|
|
|
public function deposit(TenantObject $entry): ?TenantObject
|
|
{
|
|
if ($entry->getId()) {
|
|
return $this->update($entry);
|
|
} else {
|
|
return $this->create($entry);
|
|
}
|
|
}
|
|
|
|
private function create(TenantObject $entry): ?TenantObject
|
|
{
|
|
$document = $entry->jsonSerialize();
|
|
unset($document['id']);
|
|
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->insertOne($document);
|
|
$entry->setId((string)$result->getInsertedId());
|
|
return $entry;
|
|
}
|
|
|
|
private function update(TenantObject $entry): ?TenantObject
|
|
{
|
|
$id = $entry->getId();
|
|
if (!$id) { return null; }
|
|
$document = $entry->jsonSerialize();
|
|
unset($document['id']);
|
|
$this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(['_id' => new ObjectId($id)], ['$set' => $document]);
|
|
return $entry;
|
|
}
|
|
|
|
public function destroy(TenantObject $entry): void
|
|
{
|
|
$id = $entry->getId();
|
|
if (!$id) { return; }
|
|
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne(['_id' => new ObjectId($id)]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Configuration Operations
|
|
// =========================================================================
|
|
|
|
public function fetchConfiguration(string $identifier, string $path): ?array
|
|
{
|
|
$entry = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne(
|
|
['identifier' => $identifier],
|
|
['projection' => ['configuration.' . $path => 1]],
|
|
);
|
|
|
|
if (!$entry) {
|
|
return null;
|
|
}
|
|
|
|
$value = $this->readPath($entry, "configuration.{$path}");
|
|
return $value === null ? null : (array) $value;
|
|
}
|
|
|
|
public function storeConfiguration(string $identifier, string $path, array $value): bool
|
|
{
|
|
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
['identifier' => $identifier],
|
|
['$set' => ['configuration.' . $path => $value]],
|
|
);
|
|
|
|
return $result->getMatchedCount() > 0;
|
|
}
|
|
|
|
public function removeConfiguration(string $identifier, string $path): ?bool
|
|
{
|
|
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
['identifier' => $identifier],
|
|
['$unset' => ['configuration.' . $path => '']],
|
|
);
|
|
|
|
if ($result->getMatchedCount() === 0) {
|
|
return null;
|
|
}
|
|
|
|
return $result->getModifiedCount() > 0;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Settings Operations
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Fetch settings for a tenant, optionally filtered to specific keys.
|
|
*
|
|
* @param string $identifier Tenant identifier
|
|
* @param string[] $keys Optional list of keys to return; empty = all
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function fetchSettings(string $identifier, array $keys = []): array
|
|
{
|
|
$entry = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne(
|
|
['identifier' => $identifier],
|
|
['projection' => ['settings' => 1]]
|
|
);
|
|
|
|
$settings = (array)($entry['settings'] ?? []);
|
|
|
|
if (empty($keys)) {
|
|
return $settings;
|
|
}
|
|
|
|
$result = [];
|
|
foreach ($keys as $key) {
|
|
$result[$key] = $settings[$key] ?? null;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Merge-update settings for a tenant using atomic $set on sub-fields.
|
|
*
|
|
* @param string $identifier Tenant identifier
|
|
* @param array<string, mixed> $settings Key-value pairs to persist
|
|
*/
|
|
public function storeSettings(string $identifier, array $settings): bool
|
|
{
|
|
$updates = [];
|
|
foreach ($settings as $key => $value) {
|
|
$updates["settings.{$key}"] = $value;
|
|
}
|
|
|
|
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
['identifier' => $identifier],
|
|
['$set' => $updates]
|
|
);
|
|
|
|
return $result->getMatchedCount() > 0;
|
|
}
|
|
|
|
/**
|
|
* Atomically creates or replaces one logical store reference.
|
|
*
|
|
* @param array{provider: string, service: string|int, namespace: string} $reference
|
|
*/
|
|
public function storeConfigurationStore(string $identifier, string $name, array $reference): bool
|
|
{
|
|
return $this->storeConfiguration($identifier, "stores.{$name}", $reference);
|
|
}
|
|
|
|
/**
|
|
* Atomically removes one logical store reference.
|
|
*/
|
|
public function removeConfigurationStore(string $identifier, string $name): ?bool
|
|
{
|
|
return $this->removeConfiguration($identifier, "stores.{$name}");
|
|
}
|
|
|
|
public function fetchServiceConfiguration(string $identifier, string $name): ?array
|
|
{
|
|
$this->validateServiceName($name);
|
|
return $this->fetchConfiguration($identifier, "services.{$name}");
|
|
}
|
|
|
|
/** Save one service atomically. */
|
|
public function storeServiceConfiguration(string $identifier, string $name, array $configuration): bool
|
|
{
|
|
$this->validateServiceName($name);
|
|
return $this->storeConfiguration($identifier, "services.{$name}", $configuration);
|
|
}
|
|
|
|
private function validateServiceName(string $name): void
|
|
{
|
|
if (preg_match('/^[a-z][a-z0-9_]*$/D', $name) !== 1) {
|
|
throw new \InvalidArgumentException('Invalid service name.');
|
|
}
|
|
}
|
|
|
|
private function readPath(array $data, string $path): mixed
|
|
{
|
|
$value = $data;
|
|
foreach (explode('.', $path) as $segment) {
|
|
if (!is_array($value) || !array_key_exists($segment, $value)) {
|
|
return null;
|
|
}
|
|
$value = $value[$segment];
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
} |