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