[
  {
    "path": ".gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n.idea\napolloConfig.*\ntest.php\nvendor"
  },
  {
    "path": "README.md",
    "content": "# [携程Apollo](https://github.com/ctripcorp/apollo)的PHP客户端\n\n## install\nphp version >= 7.0\n```bash\n$ composer require multilinguals/apollo-client\n```\nphp version >= 5.4 , <7.0\n```bash\n$ composer require multilinguals/apollo-client --ignore-platform-reqs\n```\n\n## Features\n- 支持apollo配置变更的实时获取\n- 支持拉取配置后自定义的回调处理\n\n## Usage\n客户端以cli的方式后台启动执行，支持apollo配置的适时获取，并将配置保存在指定的目录供应用程序读取解析\n\n### 客户端示例代码\n```php\n#!/usr/bin/env php\n<?php\nrequire 'vender/autoload.php'; // autoload\nuse Org\\Multilinguals\\Apollo\\Client\\ApolloClient;\n\n//specify address of apollo server\n$server = getenv('CONFIG_SERVER'); // get server address from env\n\n//specify your appid at apollo config server\n$appid = getenv('APPID'); // get appid from env\n\n//specify namespaces of appid at apollo config server\n$namespaces = getenv('NAMESPACE'); // get namespaces from env\n$namespaces = explode(',', $namespaces);\n\n$apollo = new ApolloClient($server, $appid, $namespaces);\n\nif ($clientIp = getenv('CLIENTIP')) {\n    $apollo->setClientIp($clientIp);\n}\n\nini_set('memory_limit','128M');\n$pid = getmypid();\necho \"start [$pid]\\n\";\n$restart = true; //auto start if failed\ndo {\n    $error = $apollo->start();\n    if ($error) echo('error:'.$error.\"\\n\");\n}while($error && $restart);\n```\n\n### 配置管理\n\n拉取的配置默认保存在脚本所在目录，每个namespace的配置以`apolloConfig.{$namespaceName}.php`的方式命名保存\n\n### Docker环境客户端自启动\n\n在docker的启动脚本中加入启动代码，一般的php容器启动脚本是docker-php-entrypoint\n```bash\nif [ -f \"/path/to/start.php\" ]; then\n    apollo_ps=$(ps -aux | grep -c \"php /path/to/start.php\")\n    if [ $apollo_ps -eq 1 ]; then\n        php /path/to/start.php &\n    fi\nfi\n```\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"multilinguals/apollo-client\",\n  \"description\": \"apollo client for php\",\n  \"type\": \"library\",\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"Apollo\",\n    \"Client\"\n  ],\n  \"homepage\": \"https://github.com/multilinguals/apollo-php-client\",\n  \"require\": {\n    \"php\": \"~7.0\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"Org\\\\Multilinguals\\\\Apollo\\\\Client\\\\\": \"src/\"\n    }\n  }\n}"
  },
  {
    "path": "examples/laravel/README.md",
    "content": "### apollo client for laravel\n\n#### 启动apollo客户端\nphp apollo.php"
  },
  {
    "path": "examples/laravel/apollo.php",
    "content": "#!/usr/bin/env php\n<?php\nrequire 'vendor/autoload.php';\nuse Org\\Multilinguals\\Apollo\\Client\\ApolloClient;\n\ndefine('SAVE_DIR', __DIR__); //定义apollo配置本地化存储路径\n\n//指定env模板和文件\ndefine('ENV_DIR', __DIR__.DIRECTORY_SEPARATOR.'env');\ndefine('ENV_TPL', ENV_DIR.DIRECTORY_SEPARATOR.'.env_tpl.php');\ndefine('ENV_FILE', ENV_DIR.DIRECTORY_SEPARATOR.'.env');\n\n//定义apollo配置变更时的回调函数，动态异步更新.env\n$callback = function () {\n    $list = glob(SAVE_DIR.DIRECTORY_SEPARATOR.'apolloConfig.*');\n    $apollo = [];\n    foreach ($list as $l) {\n        $config = require $l;\n        if (is_array($config) && isset($config['configurations'])) {\n            $apollo = array_merge($apollo, $config['configurations']);\n        }\n    }\n    if (!$apollo) {\n        throw new Exception('Load Apollo Config Failed, no config available');\n    }\n    ob_start();\n    include ENV_TPL;\n    $env_config = ob_get_contents();\n    ob_end_clean();\n    file_put_contents(ENV_FILE, $env_config);\n};\n\n//指定apollo的服务地址\n$server = 'http://127.0.0.1:8081';\n\n//指定appid\n$appid = 'demo';\n\n//指定要拉取哪些namespace的配置\n$namespaces = ['application', 'public.mysql', 'public.redis'];\n\n$apollo = new ApolloClient($server, $appid, $namespaces);\n\n//如果需要灰度发布，指定clientIp\n/*\n * $clientIp = '10.160.2.131';\n * if (isset($clientIp) && filter_var($clientIp, FILTER_VALIDATE_IP)) {\n *    $apollo->setClientIp($clientIp);\n * }\n */\n\n//从apollo上拉取的配置默认保存在脚本目录，可自行设置保存目录\n$apollo->save_dir = SAVE_DIR;\n\nini_set('memory_limit','128M');\n$pid = getmypid();\necho \"start [$pid]\\n\";\n$restart = false; //失败自动重启\ndo {\n    $error = $apollo->start($callback); //此处传入回调\n    if ($error) echo('error:'.$error.\"\\n\");\n}while($error && $restart);\n"
  },
  {
    "path": "examples/laravel/env/.env_tpl.php",
    "content": "<?php\necho \"\nDB_HOST={$apollo['mysql.url']}\nDB_PORT={$apollo['mysql.port']}\nDB_DATABASE={$apollo['mysql.db']}\nDB_USERNAME={$apollo['mysql.user']}\nDB_PASSWORD={$apollo['mysql.password']}\n\nREDIS_HOST={$apollo['redis.url']}\nREDIS_PORT={$apollo['redis.port']}\nREDIS_PASSWORD={$apollo['redis.password']}\nREDIS_DB={$apollo['redis.db']}\n\";\n"
  },
  {
    "path": "src/ApolloClient.php",
    "content": "<?php\n\nnamespace Org\\Multilinguals\\Apollo\\Client;\n\nclass ApolloClient\n{\n    protected $configServer; //apollo服务端地址\n    protected $appId; //apollo配置项目的appid\n    protected $cluster = 'default';\n    protected $clientIp = '127.0.0.1'; //绑定IP做灰度发布用\n    protected $notifications = [];\n    protected $pullTimeout = 10; //获取某个namespace配置的请求超时时间\n    protected $intervalTimeout = 60; //每次请求获取apollo配置变更时的超时时间\n    public $save_dir; //配置保存目录\n\n    /**\n     * ApolloClient constructor.\n     * @param string $configServer apollo服务端地址\n     * @param string $appId apollo配置项目的appid\n     * @param array $namespaces apollo配置项目的namespace\n     */\n    public function __construct($configServer, $appId, array $namespaces)\n    {\n        $this->configServer = $configServer;\n        $this->appId = $appId;\n        foreach ($namespaces as $namespace) {\n            $this->notifications[$namespace] = ['namespaceName' => $namespace, 'notificationId' => -1];\n        }\n        $this->save_dir = dirname($_SERVER['SCRIPT_FILENAME']);\n    }\n\n    public function setCluster($cluster)\n    {\n        $this->cluster = $cluster;\n    }\n\n    public function setClientIp($ip)\n    {\n        $this->clientIp = $ip;\n    }\n\n    public function setPullTimeout($pullTimeout) {\n        $pullTimeout = intval($pullTimeout);\n        if ($pullTimeout < 1 || $pullTimeout > 300) {\n            return;\n        }\n        $this->pullTimeout = $pullTimeout;\n    }\n\n    public function setIntervalTimeout($intervalTimeout) {\n        $intervalTimeout = intval($intervalTimeout);\n        if ($intervalTimeout < 1 || $intervalTimeout > 300) {\n            return;\n        }\n        $this->intervalTimeout = $intervalTimeout;\n    }\n\n    private function _getReleaseKey($config_file) {\n        $releaseKey = '';\n        if (file_exists($config_file)) {\n            $last_config = require $config_file;\n            is_array($last_config) && isset($last_config['releaseKey']) && $releaseKey = $last_config['releaseKey'];\n        }\n        return $releaseKey;\n    }\n\n    //获取单个namespace的配置文件路径\n    public function getConfigFile($namespaceName) {\n        return $this->save_dir.DIRECTORY_SEPARATOR.'apolloConfig.'.$namespaceName.'.php';\n    }\n\n    //获取单个namespace的配置-无缓存的方式\n    public function pullConfig($namespaceName) {\n        $base_api = rtrim($this->configServer, '/').'/configs/'.$this->appId.'/'.$this->cluster.'/';\n        $api = $base_api.$namespaceName;\n\n        $args = [];\n        $args['ip'] = $this->clientIp;\n        $config_file = $this->getConfigFile($namespaceName);\n        $args['releaseKey'] = $this->_getReleaseKey($config_file);\n\n        $api .= '?' . http_build_query($args);\n\n        $ch = curl_init($api);\n        curl_setopt($ch, CURLOPT_TIMEOUT, $this->pullTimeout);\n        curl_setopt($ch, CURLOPT_HEADER, false);\n        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n\n        $body = curl_exec($ch);\n        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n        $error = curl_error($ch);\n        curl_close($ch);\n\n        if ($httpCode == 200) {\n            $result = json_decode($body, true);\n            $content = '<?php return ' . var_export($result, true) . ';';\n            file_put_contents($config_file, $content);\n        }elseif ($httpCode != 304) {\n            echo $body ?: $error.\"\\n\";\n            return false;\n        }\n        return true;\n    }\n\n    //获取多个namespace的配置-无缓存的方式\n    public function pullConfigBatch(array $namespaceNames) {\n        if (! $namespaceNames) return [];\n        $multi_ch = curl_multi_init();\n        $request_list = [];\n        $base_url = rtrim($this->configServer, '/').'/configs/'.$this->appId.'/'.$this->cluster.'/';\n        $query_args = [];\n        $query_args['ip'] = $this->clientIp;\n        foreach ($namespaceNames as $namespaceName) {\n            $request = [];\n            $config_file = $this->getConfigFile($namespaceName);\n            $request_url = $base_url.$namespaceName;\n            $query_args['releaseKey'] = $this->_getReleaseKey($config_file);\n            $query_string = '?'.http_build_query($query_args);\n            $request_url .= $query_string;\n            $ch = curl_init($request_url);\n            curl_setopt($ch, CURLOPT_TIMEOUT, $this->pullTimeout);\n            curl_setopt($ch, CURLOPT_HEADER, false);\n            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n            $request['ch'] = $ch;\n            $request['config_file'] = $config_file;\n            $request_list[$namespaceName] = $request;\n            curl_multi_add_handle($multi_ch, $ch);\n        }\n\n        $active = null;\n        // 执行批处理句柄\n        do {\n            $mrc = curl_multi_exec($multi_ch, $active);\n        } while ($mrc == CURLM_CALL_MULTI_PERFORM);\n\n        while ($active && $mrc == CURLM_OK) {\n            if (curl_multi_select($multi_ch) == -1) {\n                usleep(100);\n            }\n            do {\n                $mrc = curl_multi_exec($multi_ch, $active);\n            } while ($mrc == CURLM_CALL_MULTI_PERFORM);\n            \n        }\n\n        // 获取结果\n        $response_list = [];\n        foreach ($request_list as $namespaceName => $req) {\n            $response_list[$namespaceName] = true;\n            $result = curl_multi_getcontent($req['ch']);\n            $code = curl_getinfo($req['ch'], CURLINFO_HTTP_CODE);\n            $error = curl_error($req['ch']);\n            curl_multi_remove_handle($multi_ch,$req['ch']);\n            curl_close($req['ch']);\n            if ($code == 200) {\n                $result = json_decode($result, true);\n                $content = '<?php return '.var_export($result, true).';';\n                file_put_contents($req['config_file'], $content);\n            }elseif ($code != 304) {\n                echo 'pull config of namespace['.$namespaceName.'] error:'.($result ?: $error).\"\\n\";\n                $response_list[$namespaceName] = false;\n            }\n        }\n        curl_multi_close($multi_ch);\n        return $response_list;\n    }\n\n    protected function _listenChange(&$ch, $callback = null) {\n        $base_url = rtrim($this->configServer, '/').'/notifications/v2?';\n        $params = [];\n        $params['appId'] = $this->appId;\n        $params['cluster'] = $this->cluster;\n        do {\n            $params['notifications'] = json_encode(array_values($this->notifications));\n            $query = http_build_query($params);\n            curl_setopt($ch, CURLOPT_URL, $base_url.$query);\n            $response = curl_exec($ch);\n            $httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);\n            $error = curl_error($ch);\n            if ($httpCode == 200) {\n                $res = json_decode($response, true);\n                $change_list = [];\n                foreach ($res as $r) {\n                    if ($r['notificationId'] != $this->notifications[$r['namespaceName']]['notificationId']) {\n                        $change_list[$r['namespaceName']] = $r['notificationId'];\n                    }\n                }\n                $response_list = $this->pullConfigBatch(array_keys($change_list));\n                foreach ($response_list as $namespaceName => $result) {\n                    $result && ($this->notifications[$namespaceName]['notificationId'] = $change_list[$namespaceName]);\n                }\n                //如果定义了配置变更的回调，比如重新整合配置，则执行回调\n                ($callback instanceof \\Closure) && call_user_func($callback);\n            }elseif ($httpCode != 304) {\n                throw new \\Exception($response ?: $error);\n            }\n        }while (true);\n    }\n\n    /**\n     * @param $callback 监听到配置变更时的回调处理\n     * @return mixed\n     */\n    public function start($callback = null) {\n        $ch = curl_init();\n        curl_setopt($ch, CURLOPT_TIMEOUT, $this->intervalTimeout);\n        curl_setopt($ch, CURLOPT_HEADER, false);\n        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n        try {\n            $this->_listenChange($ch, $callback);\n        }catch (\\Exception $e) {\n            curl_close($ch);\n            return $e->getMessage();\n        }\n    }\n}\n"
  }
]