From c288122373fcbe273bdd25651a2b35c02ecd14c9 Mon Sep 17 00:00:00 2001 From: Odboy Date: Mon, 27 Jul 2026 21:17:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(php-client):=20=E5=AE=9E=E7=8E=B0=20PHP=20?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=20SDK=20=E5=8F=8A=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.php | 14 ++ README.md | 69 ++++++-- README_DOCKERFILE.md | 12 ++ kenaito-conf-php-client-demo/app.php | 82 ++++++++++ kenaito-conf-php-client/README.md | 102 ++++++++++++ kenaito-conf-php-client/autoload.php | 6 + kenaito-conf-php-client/composer.json | 23 +++ kenaito-conf-php-client/src/Cache.php | 32 ++++ kenaito-conf-php-client/src/Client.php | 192 +++++++++++++++++++++++ kenaito-conf-php-client/src/Config.php | 27 ++++ kenaito-conf-php-client/src/Listener.php | 24 +++ 11 files changed, 572 insertions(+), 11 deletions(-) create mode 100644 Dockerfile.php create mode 100644 kenaito-conf-php-client-demo/app.php create mode 100644 kenaito-conf-php-client/README.md create mode 100644 kenaito-conf-php-client/autoload.php create mode 100644 kenaito-conf-php-client/composer.json create mode 100644 kenaito-conf-php-client/src/Cache.php create mode 100644 kenaito-conf-php-client/src/Client.php create mode 100644 kenaito-conf-php-client/src/Config.php create mode 100644 kenaito-conf-php-client/src/Listener.php diff --git a/Dockerfile.php b/Dockerfile.php new file mode 100644 index 0000000..0c3489f --- /dev/null +++ b/Dockerfile.php @@ -0,0 +1,14 @@ +FROM php:8.3-cli + +WORKDIR /kenaito-conf-php-client-demo + +# Install required extensions +RUN docker-php-ext-install pcntl + +# Copy client library +COPY kenaito-conf-php-client/ /kenaito-conf-php-client/ + +# Copy demo +COPY kenaito-conf-php-client-demo/app.php /kenaito-conf-php-client-demo/app.php + +ENTRYPOINT ["php", "app.php"] diff --git a/README.md b/README.md index dcbbb65..48a845b 100644 --- a/README.md +++ b/README.md @@ -24,20 +24,12 @@ kenaito-conf/ ├── kenaito-conf-nodejs-client-demo/ # Node.js 客户端示例(端口 8083) ├── kenaito-conf-perl-client/ # Perl 客户端 SDK(Perl 5.16+) ├── kenaito-conf-perl-client-demo/ # Perl 客户端示例 -├── kenaito-conf-php-client/ # (未支持)PHP 客户端 SDK(PHP 8.0+) -├── kenaito-conf-php-client-demo/ # (未支持)PHP 客户端示例 +├── kenaito-conf-php-client/ # PHP 客户端 SDK(PHP 7.4+) +├── kenaito-conf-php-client-demo/ # PHP 客户端示例 ├── kenaito-conf-dotnet-client/ # .NET 客户端 SDK(.NET 6.0+) ├── kenaito-conf-dotnet-client-demo/ # .NET 客户端示例 -├── kenaito-conf-c-client/ # (未支持)C 客户端 SDK(libcurl + OpenSSL) -├── kenaito-conf-c-client-demo/ # (未支持)C 客户端示例 -├── kenaito-conf-cpp-client/ # (未支持)C++ 客户端 SDK(libcurl + OpenSSL) -├── kenaito-conf-cpp-client-demo/ # (未支持)C++ 客户端示例 ├── kenaito-conf-lua-client/ # Lua 客户端 SDK(Lua 5.1+) -├── kenaito-conf-lua-client-demo/ # Lua 客户端示例 -├── kenaito-conf-rust-client/ # (未支持)Rust 客户端 SDK(Rust 1.70+) -├── kenaito-conf-rust-client-demo/ # (未支持)Rust 客户端示例 -├── kenaito-conf-ruby-client/ # (未支持)Ruby 客户端 SDK(Ruby 2.7+) -└── kenaito-conf-ruby-client-demo/ # (未支持)Ruby 客户端示例 +└── kenaito-conf-lua-client-demo/ # Lua 客户端示例 ``` ## 快速开始 @@ -370,6 +362,61 @@ client:sync_once() client:stop() ``` +### PHP 客户端 + +适用于 PHP 7.4+ 项目。 + +**1. 安装** + +```bash +cd kenaito-conf-php-client +composer install +``` + +或在项目中引入: + +```bash +composer require kenaito/conf-client +``` + +**2. 使用** + +```php +require_once 'vendor/autoload.php'; + +use Kenaito\Conf\Config; +use Kenaito\Conf\Client; + +$config = new Config([ + 'baseUrl' => 'http://localhost:8080', + 'appName' => 'my-app', + 'env' => 'daily', + 'syncInterval' => 10, +]); + +$client = new Client($config); + +// 监听配置变更 +$client->onChange(function(array $changed) { + foreach ($changed as $key => $value) { + echo "config changed: $key = $value\n"; + } +}); + +// 启动客户端(拉取配置) +$client->start(); + +// 获取配置 +$value = $client->get('database.host'); +$all = $client->getAll(); + +// 手动同步一次 +$client->syncOnce(); + +// 停止 +$client->stop(); +``` + ## API 概览 ### 管理端 API(需 JWT 认证) diff --git a/README_DOCKERFILE.md b/README_DOCKERFILE.md index 2194ae4..a26e39d 100644 --- a/README_DOCKERFILE.md +++ b/README_DOCKERFILE.md @@ -98,6 +98,18 @@ docker run -it --rm \ kenaito-conf-lua-demo ``` +## PHP 客户端 + +```bash +docker build -f Dockerfile.php -t kenaito-conf-php-demo . + +docker run -it --rm \ + -e KENAITO_BASE_URL=http://host.docker.internal:8080 \ + -e KENAITO_APP_NAME=kenaito-springboot-demo \ + -e KENAITO_ENV=daily \ + kenaito-conf-php-demo +``` + ## Spring Boot 2 客户端(测试通过) ```bash diff --git a/kenaito-conf-php-client-demo/app.php b/kenaito-conf-php-client-demo/app.php new file mode 100644 index 0000000..bdee6c2 --- /dev/null +++ b/kenaito-conf-php-client-demo/app.php @@ -0,0 +1,82 @@ + getEnvVar('DATABASE_HOST', '192.168.1.1'), + 'database.port' => getEnvVar('DATABASE_PORT', '8888'), + 'database.name' => getEnvVar('DATABASE_NAME', 'xxxx'), + 'database.user' => getEnvVar('DATABASE_USER', 'xxxx'), + 'database.password' => getEnvVar('DATABASE_PASSWORD', 'xxxxxxxx'), +]; + +$config = new Config([ + 'baseUrl' => getEnvVar('KENAITO_BASE_URL', 'http://localhost:8080'), + 'appName' => getEnvVar('KENAITO_APP_NAME', 'kenaito-springboot-demo'), + 'env' => getEnvVar('KENAITO_ENV', 'daily'), + 'version' => getEnvVar('KENAITO_VERSION', 'latest'), + 'token' => getEnvVar('KENAITO_TOKEN', ''), + 'syncInterval' => (int)getEnvVar('KENAITO_SYNC_INTERVAL', '10'), + 'cacheDir' => getEnvVar('KENAITO_CACHE_DIR', '.'), +]); + +$client = new Client($config); + +// 监听配置变更 +$client->onChange(function(array $changed) use (&$configValues) { + echo "Config changed:\n"; + foreach ($changed as $key => $value) { + echo " $key = $value\n"; + if (isset($configValues[$key])) { + $configValues[$key] = $value; + echo " [updated] $key = $value\n"; + } + } +}); + +// 启动客户端 +try { + $client->start(); + $remote = $client->getAll(); + foreach ($configValues as $key => $value) { + if (isset($remote[$key])) { + $configValues[$key] = $remote[$key]; + } + } + echo "loaded " . count($remote) . " configs from remote\n\n"; +} catch (\Throwable $e) { + echo "Failed to init config client, using local defaults: " . $e->getMessage() . "\n\n"; +} + +// 显示当前配置 +echo "Current configs:\n"; +ksort($configValues); +foreach ($configValues as $key => $value) { + echo " $key = $value\n"; +} + +echo "\nListening for config changes... (press Ctrl+C to stop)\n"; + +// 信号处理 +pcntl_signal(SIGINT, function() use ($client) { + echo "\nShutting down...\n"; + $client->stop(); + exit(0); +}); + +// 同步循环 +while (!$client->stop) { + pcntl_signal_dispatch(); + $client->syncOnce(); + sleep($config->syncInterval); +} diff --git a/kenaito-conf-php-client/README.md b/kenaito-conf-php-client/README.md new file mode 100644 index 0000000..8cc4bce --- /dev/null +++ b/kenaito-conf-php-client/README.md @@ -0,0 +1,102 @@ +# Kenaito Config Center PHP Client + +PHP 客户端 SDK,适用于 PHP 7.4+ 项目。 + +## 安装 + +### 使用 Composer(推荐) + +```bash +cd kenaito-conf-php-client +composer install +``` + +在你的项目中引入: + +```bash +composer require kenaito/conf-client +``` + +### 手动引入 + +```php +require_once '/path/to/kenaito-conf-php-client/autoload.php'; +``` + +## 使用 + +```php +require_once 'vendor/autoload.php'; + +use Kenaito\Conf\Config; +use Kenaito\Conf\Client; + +$config = new Config([ + 'baseUrl' => 'http://localhost:8080', + 'appName' => 'my-app', + 'env' => 'daily', + 'syncInterval' => 10, +]); + +$client = new Client($config); + +// 监听配置变更 +$client->onChange(function(array $changed) { + foreach ($changed as $key => $value) { + echo "config changed: $key = $value\n"; + } +}); + +// 启动客户端(拉取配置) +$client->start(); + +// 获取配置 +$value = $client->get('database.host'); +$all = $client->getAll(); + +// 手动同步一次 +$client->syncOnce(); + +// 停止 +$client->stop(); +``` + +## 配置项 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `baseUrl` | 配置中心地址 | `http://localhost:8080` | +| `appName` | 应用名称 | (必填) | +| `env` | 环境标识 | `dev` | +| `version` | 配置版本 | `latest` | +| `token` | 应用 Token | 空 | +| `syncInterval` | 同步间隔(秒) | `10` | +| `ignoreGlobal` | 忽略全局配置 | `false` | +| `cacheDir` | 缓存目录 | `.` | + +## 依赖 + +- PHP 7.4+ +- ext-curl +- ext-json + +## Demo + +```bash +cd kenaito-conf-php-client-demo +php app.php +``` + +## 环境变量 + +Demo 支持以下环境变量: + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `KENAITO_BASE_URL` | 配置中心地址 | `http://localhost:8080` | +| `KENAITO_APP_NAME` | 应用名称 | `kenaito-springboot-demo` | +| `KENAITO_ENV` | 环境标识 | `daily` | +| `KENAITO_VERSION` | 配置版本 | `latest` | +| `KENAITO_TOKEN` | 应用 Token | 空 | +| `KENAITO_SYNC_INTERVAL` | 同步间隔(秒) | `10` | +| `KENAITO_CACHE_DIR` | 缓存目录 | `.` | diff --git a/kenaito-conf-php-client/autoload.php b/kenaito-conf-php-client/autoload.php new file mode 100644 index 0000000..6a88419 --- /dev/null +++ b/kenaito-conf-php-client/autoload.php @@ -0,0 +1,6 @@ +=7.4", + "ext-curl": "*", + "ext-json": "*" + }, + "autoload": { + "psr-4": { + "Kenaito\\Conf\\": "src/" + } + } +} diff --git a/kenaito-conf-php-client/src/Cache.php b/kenaito-conf-php-client/src/Cache.php new file mode 100644 index 0000000..f1f9300 --- /dev/null +++ b/kenaito-conf-php-client/src/Cache.php @@ -0,0 +1,32 @@ +cacheDir = $cacheDir; + } + + public function write(array $data): bool + { + $path = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_FILE; + $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + return file_put_contents($path, $json) !== false; + } + + public function read(): ?array + { + $path = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_FILE; + if (!file_exists($path)) { + return null; + } + $json = file_get_contents($path); + $data = json_decode($json, true); + return is_array($data) ? $data : null; + } +} diff --git a/kenaito-conf-php-client/src/Client.php b/kenaito-conf-php-client/src/Client.php new file mode 100644 index 0000000..4bb4630 --- /dev/null +++ b/kenaito-conf-php-client/src/Client.php @@ -0,0 +1,192 @@ +config = $config; + $this->cache = new Cache($config->cacheDir); + $this->listener = new Listener(); + } + + public function onChange(callable $handler): void + { + $this->listener->onChange($handler); + } + + public function get(string $key): ?string + { + return $this->configs[$key] ?? null; + } + + public function getAll(): array + { + return $this->configs; + } + + public function start(): void + { + if ($this->started) { + return; + } + $this->started = true; + + try { + $this->fetchAll(); + } catch (\Throwable $e) { + error_log("[ConfigClient] fetch all configs error: " . $e->getMessage() . ", trying cache"); + $cached = $this->cache->read(); + if ($cached !== null) { + $this->applyConfigs($cached); + error_log("[ConfigClient] loaded " . count($this->configs) . " configs from cache"); + } else { + throw new \RuntimeException("fetch configs failed and no cache: " . $e->getMessage(), 0, $e); + } + } + } + + public function stop(): void + { + $this->stop = true; + } + + public function syncOnce(): void + { + try { + $this->fetchChanged(); + } catch (\Throwable $e) { + error_log("[ConfigClient] sync error: " . $e->getMessage()); + } + } + + private function httpPost(string $url, array $payload): array + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($response === false) { + throw new \RuntimeException("HTTP request failed: " . $error); + } + + if ($httpCode !== 200) { + throw new \RuntimeException("HTTP " . $httpCode); + } + + $data = json_decode($response, true); + if (!is_array($data)) { + throw new \RuntimeException("JSON decode error"); + } + + return $data; + } + + private function fetchAll(): void + { + $url = $this->config->baseUrl . '/api/fetchAllConfigs'; + $payload = [ + 'appName' => $this->config->appName, + 'env' => $this->config->env, + 'version' => $this->config->version, + 'ignoreGlobal' => $this->config->ignoreGlobal, + 'token' => $this->config->token, + ]; + + $data = $this->httpPost($url, $payload); + + if (isset($data['code']) && $data['code'] !== 0) { + throw new \RuntimeException("server error: " . ($data['msg'] ?? 'unknown')); + } + + if (isset($data['latest.fetch.timestamp'])) { + $this->lastTime = (int)$data['latest.fetch.timestamp']; + } + + $this->cache->write($data); + $this->applyConfigs($data); + + error_log("[ConfigClient] fetched " . count($this->configs) . " configs"); + } + + private function fetchChanged(): void + { + $url = $this->config->baseUrl . '/api/fetchChangedConfigs'; + $payload = [ + 'appName' => $this->config->appName, + 'env' => $this->config->env, + 'version' => $this->config->version, + 'lastTime' => $this->lastTime, + 'ignoreGlobal' => $this->config->ignoreGlobal, + 'token' => $this->config->token, + ]; + + $result = $this->httpPost($url, $payload); + + if (isset($result['code']) && $result['code'] !== 0) { + throw new \RuntimeException("server error code: " . $result['code']); + } + + $changedData = $result['data'] ?? []; + + if (isset($changedData['latest.fetch.timestamp'])) { + $this->lastTime = (int)$changedData['latest.fetch.timestamp']; + unset($changedData['latest.fetch.timestamp']); + } + + // 处理删除的配置 + if (isset($changedData['deletedKeys']) && $changedData['deletedKeys'] !== '') { + $deletedKeys = explode(',', $changedData['deletedKeys']); + foreach ($deletedKeys as $key) { + unset($this->configs[$key]); + } + error_log("[ConfigClient] " . count($deletedKeys) . " configs deleted: " . implode(', ', $deletedKeys)); + unset($changedData['deletedKeys']); + } + + // 处理变更的配置 + if (!empty($changedData)) { + $changed = []; + foreach ($changedData as $key => $value) { + if (is_string($value)) { + $this->configs[$key] = $value; + $changed[$key] = $value; + } + } + + if (!empty($changed)) { + $this->listener->notify($changed); + error_log("[ConfigClient] " . count($changed) . " configs changed: " . implode(', ', array_keys($changed))); + } + } + } + + private function applyConfigs(array $data): void + { + foreach ($data as $key => $value) { + if (strpos($key, 'kenaito.') === 0 || $key === 'latest.fetch.timestamp') { + continue; + } + if (is_string($value)) { + $this->configs[$key] = $value; + } + } + } +} diff --git a/kenaito-conf-php-client/src/Config.php b/kenaito-conf-php-client/src/Config.php new file mode 100644 index 0000000..3ae85fa --- /dev/null +++ b/kenaito-conf-php-client/src/Config.php @@ -0,0 +1,27 @@ +baseUrl = $options['baseUrl'] ?? 'http://localhost:8080'; + $this->appName = $options['appName'] ?? ''; + $this->env = $options['env'] ?? 'dev'; + $this->version = $options['version'] ?? 'latest'; + $this->token = $options['token'] ?? ''; + $this->syncInterval = $options['syncInterval'] ?? 10; + $this->ignoreGlobal = $options['ignoreGlobal'] ?? false; + $this->cacheDir = $options['cacheDir'] ?? '.'; + } +} diff --git a/kenaito-conf-php-client/src/Listener.php b/kenaito-conf-php-client/src/Listener.php new file mode 100644 index 0000000..cb5b0f2 --- /dev/null +++ b/kenaito-conf-php-client/src/Listener.php @@ -0,0 +1,24 @@ +handlers[] = $handler; + } + + public function notify(array $changed): void + { + foreach ($this->handlers as $handler) { + try { + $handler($changed); + } catch (\Throwable $e) { + error_log("[ConfigClient] change handler error: " . $e->getMessage()); + } + } + } +}