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 */ 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 $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; } }