feat(core): add tenant system store contracts and configuration
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -12,12 +12,14 @@ class TenantConfiguration extends JsonSerializableObject
|
||||
protected TenantAuthentication $authentication;
|
||||
protected TenantSecurity $security;
|
||||
protected TenantFirewall $firewall;
|
||||
protected TenantStores $stores;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->authentication = new TenantAuthentication();
|
||||
$this->security = new TenantSecurity();
|
||||
$this->firewall = new TenantFirewall();
|
||||
$this->stores = new TenantStores();
|
||||
}
|
||||
|
||||
public function authentication(): TenantAuthentication {
|
||||
@@ -32,4 +34,8 @@ class TenantConfiguration extends JsonSerializableObject
|
||||
return $this->firewall;
|
||||
}
|
||||
|
||||
public function stores(): TenantStores {
|
||||
return $this->stores;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Models\Tenant;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KTXF\Json\JsonSerializableObject;
|
||||
use KTXF\SystemStore\StoreReference;
|
||||
|
||||
/**
|
||||
* Logical system stores configured for a tenant.
|
||||
*/
|
||||
final class TenantStores extends JsonSerializableObject
|
||||
{
|
||||
/** @var array<string, StoreReference> */
|
||||
private array $entries = [];
|
||||
|
||||
public function jsonDeserialize(array|string $data): static
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
}
|
||||
|
||||
$this->entries = [];
|
||||
foreach ($data as $name => $store) {
|
||||
if (!is_string($name) || preg_match('/^[a-z][a-z0-9-]*$/', $name) !== 1) {
|
||||
throw new InvalidArgumentException('Invalid logical system-store name');
|
||||
}
|
||||
if (!is_array($store)) {
|
||||
throw new InvalidArgumentException('Invalid tenant store configuration entry');
|
||||
}
|
||||
$this->entries[$name] = StoreReference::fromArray($store);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_map(
|
||||
static fn(StoreReference $store): array => $store->toArray(),
|
||||
$this->entries,
|
||||
);
|
||||
}
|
||||
|
||||
public function store(string $name): ?StoreReference
|
||||
{
|
||||
return $this->entries[$name] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, StoreReference> */
|
||||
public function all(): array
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\SystemStore;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Provider and service selected for one logical tenant system store.
|
||||
*/
|
||||
final readonly class StoreReference
|
||||
{
|
||||
public function __construct(
|
||||
public string $provider,
|
||||
public string|int $service,
|
||||
public string $namespace,
|
||||
) {
|
||||
if ($this->provider === '') {
|
||||
throw new InvalidArgumentException('System-store provider cannot be empty');
|
||||
}
|
||||
|
||||
if ($this->service === '') {
|
||||
throw new InvalidArgumentException('System-store service cannot be empty');
|
||||
}
|
||||
|
||||
if ($this->namespace === '') {
|
||||
throw new InvalidArgumentException('System-store namespace cannot be empty');
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$provider = $data['provider'] ?? null;
|
||||
$service = $data['service'] ?? null;
|
||||
$namespace = $data['namespace'] ?? null;
|
||||
|
||||
if (!is_string($provider) || (!is_string($service) && !is_int($service)) || !is_string($namespace)) {
|
||||
throw new InvalidArgumentException('Invalid system-store reference');
|
||||
}
|
||||
|
||||
return new self($provider, $service, $namespace);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'provider' => $this->provider,
|
||||
'service' => $this->service,
|
||||
'namespace' => $this->namespace,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\SystemStore;
|
||||
|
||||
use KTXF\Resource\BinaryResource;
|
||||
|
||||
/**
|
||||
* Tenant-scoped access to logical internal stores.
|
||||
*/
|
||||
interface SystemStoreManagerInterface
|
||||
{
|
||||
public function stat(string $tenantId, string $store, string $key): ?BlobInfo;
|
||||
|
||||
public function read(string $tenantId, string $store, string $key): ?BinaryResource;
|
||||
|
||||
public function write(
|
||||
string $tenantId,
|
||||
string $store,
|
||||
string $key,
|
||||
BinaryResource $content,
|
||||
array $metadata = [],
|
||||
?WriteCondition $condition = null,
|
||||
): BlobInfo;
|
||||
|
||||
public function delete(
|
||||
string $tenantId,
|
||||
string $store,
|
||||
string $key,
|
||||
?WriteCondition $condition = null,
|
||||
): bool;
|
||||
|
||||
/**
|
||||
* @return iterable<BlobInfo>
|
||||
*/
|
||||
public function list(string $tenantId, string $store, string $prefix = ''): iterable;
|
||||
}
|
||||
@@ -6,12 +6,15 @@ namespace KTXT\Unit\SystemStore;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use InvalidArgumentException;
|
||||
use KTXC\Models\Tenant\TenantConfiguration;
|
||||
use KTXF\Resource\Provider\ProviderInterface;
|
||||
use KTXF\Resource\Provider\ResourceProviderBaseInterface;
|
||||
use KTXF\Resource\Provider\ResourceServiceBaseInterface;
|
||||
use KTXF\SystemStore\BlobInfo;
|
||||
use KTXF\SystemStore\Provider\ProviderBaseInterface;
|
||||
use KTXF\SystemStore\Service\SystemStoreServiceInterface;
|
||||
use KTXF\SystemStore\StoreReference;
|
||||
use KTXF\SystemStore\SystemStoreManagerInterface;
|
||||
use KTXF\SystemStore\WriteCondition;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -41,6 +44,17 @@ final class SystemStoreContractsTest extends TestCase
|
||||
self::assertCount(1, $method->getParameters());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function managerContractExposesTenantScopedStoreOperations(): void
|
||||
{
|
||||
$manager = new ReflectionClass(SystemStoreManagerInterface::class);
|
||||
|
||||
self::assertSame(
|
||||
['stat', 'read', 'write', 'delete', 'list'],
|
||||
array_map(static fn($method): string => $method->getName(), $manager->getMethods()),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function createsSupportedWriteConditions(): void
|
||||
{
|
||||
@@ -60,7 +74,7 @@ final class SystemStoreContractsTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function serializesMetadata(): void
|
||||
public function exposesBlobInformation(): void
|
||||
{
|
||||
$metadata = new BlobInfo(
|
||||
key: 'previews/example.webp',
|
||||
@@ -71,13 +85,46 @@ final class SystemStoreContractsTest extends TestCase
|
||||
attributes: ['variant' => 'popover'],
|
||||
);
|
||||
|
||||
self::assertSame([
|
||||
'key' => 'previews/example.webp',
|
||||
'mime' => 'image/webp',
|
||||
'size' => 123,
|
||||
'etag' => 'revision-1',
|
||||
'modifiedAt' => '2026-08-28T12:00:00+00:00',
|
||||
'attributes' => ['variant' => 'popover'],
|
||||
], $metadata->jsonSerialize());
|
||||
self::assertSame('previews/example.webp', $metadata->key);
|
||||
self::assertSame('image/webp', $metadata->mimeType);
|
||||
self::assertSame(123, $metadata->size);
|
||||
self::assertSame('revision-1', $metadata->etag);
|
||||
self::assertSame(['variant' => 'popover'], $metadata->attributes);
|
||||
self::assertNotInstanceOf(\JsonSerializable::class, $metadata);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function parsesLogicalStoresFromTenantConfiguration(): void
|
||||
{
|
||||
$data = [
|
||||
'stores' => [
|
||||
'previews' => [
|
||||
'provider' => 's3',
|
||||
'service' => 'preview-storage',
|
||||
'namespace' => 'previews',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$configuration = (new TenantConfiguration())->jsonDeserialize($data);
|
||||
|
||||
self::assertInstanceOf(StoreReference::class, $configuration->stores()->store('previews'));
|
||||
self::assertSame('s3', $configuration->stores()->store('previews')?->provider);
|
||||
self::assertSame($data['stores'], $configuration->jsonSerialize()['stores']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejectsInvalidLogicalStoreNames(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
(new TenantConfiguration())->jsonDeserialize([
|
||||
'stores' => [
|
||||
'../previews' => [
|
||||
'provider' => 's3',
|
||||
'service' => 'preview-storage',
|
||||
'namespace' => 'previews',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user