├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── README.md ├── composer.json └── src ├── BucketClient.php ├── CiClient.php ├── Client.php ├── Config.php ├── Exceptions ├── ClientException.php ├── Exception.php ├── InvalidArgumentException.php ├── InvalidConfigException.php └── ServerException.php ├── Http └── Response.php ├── JobClient.php ├── Middleware ├── CreateRequestSignature.php └── SetContentMd5.php ├── ObjectClient.php ├── ServiceClient.php ├── Signature.php ├── Support └── XML.php └── Traits └── CreatesHttpClient.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | 11 | [*.{vue,js,scss}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | end_of_line = lf 16 | insert_final_newline = true 17 | trim_trailing_whitespace = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [overtrue] 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | phpunit: 6 | name: PHP-${{ matrix.php_version }}-${{ matrix.perfer }} 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php_version: 12 | - 7.4 13 | - 8.0 14 | - 8.1 15 | - 8.2 16 | - 8.3 17 | - 8.4 18 | perfer: 19 | - stable 20 | steps: 21 | - uses: actions/checkout@master 22 | - name: Install Dependencies 23 | run: composer update --prefer-dist --no-interaction --no-suggest --prefer-${{ matrix.perfer }} 24 | - name: Run PHPUnit 25 | run: ./vendor/bin/phpunit 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

QCloud COS Client

4 | 5 | 对象存储(Cloud Object Storage,COS)是腾讯云提供的一种存储海量文件的分布式存储服务,具有高扩展性、低成本、可靠安全等优点。通过控制台、API、SDK 和工具等多样化方式,用户可简单、快速地接入 COS,进行多格式文件的上传、下载和管理,实现海量数据存储和管理。 6 | 7 | > :star: 官方文档:https://cloud.tencent.com/document/product/436 8 | 9 | [![Sponsor me](https://github.com/overtrue/overtrue/blob/master/sponsor-me-button-s.svg?raw=true)](https://github.com/sponsors/overtrue) 10 | 11 | ## 安装 12 | 13 | 环境要求: 14 | 15 | - PHP >= 8.0 16 | - ext-libxml 17 | - ext-simplexml 18 | - ext-json 19 | - ext-dom 20 | 21 | ```shell 22 | $ composer require overtrue/qcloud-cos-client -vvv 23 | ``` 24 | 25 | ## 配置 26 | 27 | 配置前请了解官方名词解释:[文档中心 > 对象存储 > API 文档 > 简介:术语信息](https://cloud.tencent.com/document/product/436/7751#.E6.9C.AF.E8.AF.AD.E4.BF.A1.E6.81.AF) 28 | 29 | ```php 30 | $config = [ 31 | // 必填,app_id、secret_id、secret_key 32 | // 可在个人秘钥管理页查看:https://console.cloud.tencent.com/capi 33 | 'app_id' => 10020201024, 34 | 'secret_id' => 'AKIDsiQzQla780mQxLLU2GJCxxxxxxxxxxx', 35 | 'secret_key' => 'b0GMH2c2NXWKxPhy77xhHgwxxxxxxxxxxx', 36 | 37 | // 可选(批量处理接口必填),腾讯云账号 ID 38 | // 可在腾讯云控制台账号信息中查看:https://console.cloud.tencent.com/developer 39 | 'uin' => '10000*******', 40 | 41 | // 可选,地域列表请查看 https://cloud.tencent.com/document/product/436/6224 42 | 'region' => 'ap-guangzhou', 43 | 44 | // 可选,仅在调用不同的接口时按场景必填 45 | 'bucket' => 'example', // 使用 Bucket 接口时必填 46 | 47 | // 可选,签名有效期,默认 60 分钟 48 | 'signature_expires' => '+60 minutes', 49 | 50 | // 可选,guzzle 配置 51 | // 参考:https://docs.guzzlephp.org/en/7.0/request-options.html 52 | 'guzzle' => [ 53 | // ... 54 | ], 55 | ]; 56 | ``` 57 | 58 | ## 使用 59 | 60 | 您可以分两种方式使用此 SDK: 61 | 62 | - **ServiceClient、BucketClient、ObjectClient、JobClient** - 封装了具体 API 的类调用指定业务的 API。 63 | - **Client** - 基于最基础的 HTTP 类封装调用 COS 全部 API。 64 | 65 | 在使用前我们强烈建议您仔细阅读[官方 API 文档](https://cloud.tencent.com/document/product/436),以减少不必要的时间浪费。 66 | 67 | ### 返回值 68 | 69 | 所有的接口调用都会返回 [`Overtrue\CosClient\Http\Response`](https://github.com/overtrue/qcloud-cos-client/blob/master/src/Http/Response.php) 对象,该对象提供了以下便捷方法: 70 | 71 | ```php 72 | array|null $response->toArray(); // 获取响应内容数组转换结果 73 | object $response->toObject(); // 获取对象格式的返回值 74 | bool $response->isXML(); // 检测返回内容是否为 XML 75 | string $response->getContents(); // 获取原始返回内容 76 | ``` 77 | 78 | 你也可以直接把 `$response` 当成数组访问:`$response['ListBucketResult']` 79 | 80 | ## ServiceClient 81 | 82 | ```php 83 | use Overtrue\CosClient\ServiceClient; 84 | 85 | $config = [ 86 | // 请参考配置说明 87 | ]; 88 | $service = new ServiceClient($config); 89 | 90 | $service->getBuckets(); 91 | $service->getBuckets('ap-guangzhou'); 92 | ``` 93 | 94 | ## JobClient 95 | 96 | ```php 97 | use Overtrue\CosClient\JobClient; 98 | 99 | $config = [ 100 | // 请参考配置说明 101 | ]; 102 | 103 | $job = new JobClient($config); 104 | 105 | ## API 106 | 107 | $job->getJobs(array $query = []); 108 | $job->createJob(array $body); 109 | $job->describeJob(string $id); 110 | $job->updateJobPriority(string $id, int $priority); 111 | $job->updateJobStatus(string $id, array $query); 112 | ``` 113 | 114 | ## BucketClient 115 | 116 | ```php 117 | use Overtrue\CosClient\BucketClient; 118 | 119 | $config = [ 120 | // 请参考配置说明 121 | 'bucket' => 'example', 122 | 'region' => 'ap-guangzhou', 123 | ]; 124 | 125 | $bucket = new BucketClient($config); 126 | 127 | ## API 128 | 129 | $bucket->putBucket(array $body); 130 | $bucket->headBucket(); 131 | $bucket->deleteBucket(); 132 | $bucket->getObjects(array $query = []); 133 | $bucket->getObjectVersions(array $query = []); 134 | 135 | // Versions 136 | $bucket->putVersions(array $body); 137 | $bucket->getVersions(); 138 | 139 | // ACL 140 | $bucket->putACL(array $body, array $headers = []) 141 | $bucket->getACL(); 142 | 143 | // CORS 144 | $bucket->putCORS(array $body); 145 | $bucket->getCORS(); 146 | $bucket->deleteCORS(); 147 | 148 | // Lifecycle 149 | $bucket->putLifecycle(array $body); 150 | $bucket->getLifecycle(); 151 | $bucket->deleteLifecycle(); 152 | 153 | // Policy 154 | $bucket->putPolicy(array $body); 155 | $bucket->getPolicy(); 156 | $bucket->deletePolicy(); 157 | 158 | // Referer 159 | $bucket->putReferer(array $body); 160 | $bucket->getReferer(); 161 | 162 | // Taging 163 | $bucket->putTaging(array $body); 164 | $bucket->getTaging(); 165 | $bucket->deleteTaging(); 166 | 167 | // Website 168 | $bucket->putWebsite(array $body); 169 | $bucket->getWebsite(); 170 | $bucket->deleteWebsite(); 171 | 172 | // Inventory 173 | $bucket->putInventory(string $id, array $body) 174 | $bucket->getInventory(string $id) 175 | $bucket->getInventoryConfigurations(?string $nextContinuationToken = null) 176 | $bucket->deleteInventory(string $id) 177 | 178 | // Versioning 179 | $bucket->putVersioning(array $body); 180 | $bucket->getVersioning(); 181 | 182 | // Replication 183 | $bucket->putReplication(array $body); 184 | $bucket->getReplication(); 185 | $bucket->deleteReplication(); 186 | 187 | // Logging 188 | $bucket->putLogging(array $body); 189 | $bucket->getLogging(); 190 | 191 | // Accelerate 192 | $bucket->putAccelerate(array $body); 193 | $bucket->getAccelerate(); 194 | 195 | // Encryption 196 | $bucket->putEncryption(array $body); 197 | $bucket->getEncryption(); 198 | $bucket->deleteEncryption(); 199 | ``` 200 | 201 | ## ObjectClient 202 | 203 | ```php 204 | use Overtrue\CosClient\ObjectClient; 205 | 206 | $config = [ 207 | // 请参考配置说明 208 | 'bucket' => 'example', 209 | 'region' => 'ap-guangzhou', 210 | ]); 211 | 212 | $object = new ObjectClient($config); 213 | 214 | $object->putObject(string $key, string $body, array $headers = []); 215 | $object->copyObject(string $key, array $headers = []); 216 | $object->getObject(string $key, array $query = [], array $headers = []); 217 | $object->headObject(string $key, string $versionId, array $headers = []); 218 | $object->restoreObject(string $key, string $versionId, array $body); 219 | $object->selectObjectContents(string $key, array $body); 220 | $object->deleteObject(string $key, string $versionId); 221 | $object->deleteObjects(array $body); 222 | 223 | $object->putObjectACL(string $key, array $body, array $headers = []); 224 | $object->getObjectACL(string $key); 225 | 226 | $object->putObjectTagging(string $key, string $versionId, array $body); 227 | $object->getObjectTagging(string $key, string $versionId); 228 | $object->deleteObjectTagging(string $key, string $versionId); 229 | 230 | $object->createUploadId(string $key, array $headers = []); 231 | $object->putPart(string $key, int $partNumber, string $uploadId, string $body, array $headers = []); 232 | $object->copyPart(string $key, int $partNumber, string $uploadId, array $headers = []); 233 | $object->markUploadAsCompleted(string $key, string $uploadId, array $body); 234 | $object->markUploadAsAborted(string $key, string $uploadId); 235 | $object->getUploadJobs(array $query = []); 236 | $object->getUploadedParts(string $key, string $uploadId, array $query = []); 237 | 238 | $object->getObjectUrl(string $key) 239 | $object->getObjectSignedUrl(string $key, string $expires = '+60 minutes') 240 | ``` 241 | 242 | ## 异常处理 243 | 244 | ```php 245 | use Overtrue\CosClient\BucketClient; 246 | 247 | $client = new BucketClient([ 248 | 'app_id' => 123456789, 249 | 'secret_id' => 'AKIDsiQzQla780mQxLLUxxxxxxx', 250 | 'secret_key' => 'b0GMH2c2NXWKxPhy77xxxxxxxx', 251 | 'region' => 'ap-guangzhou', 252 | 'bucket' => 'example', 253 | ]); 254 | 255 | try { 256 | $client->getObjects(); 257 | } catch(\Throwable $e) { 258 | var_dump($e->getResponse()->toArray()); 259 | } 260 | ``` 261 | 262 | 其中 `$e->getResponse()` 为 `\Overtrue\CosClient\Http\Response` 示例,你也可以通过 `$e->getRequest()` 获取请求对象。 263 | 264 | ## 测试 265 | 266 | 你可以使用类提供的 `spy` 方法来创建一个测试对象: 267 | 268 | ```php 269 | 270 | use Overtrue\CosClient\Http\Response; 271 | use Overtrue\CosClient\ServiceClient; 272 | 273 | $service = ServiceClient::spy(); 274 | 275 | $mockResponse = Response::create(200, [], ' 276 | 277 | 278 | examplebucket1-1250000000 279 | ap-beijing 280 | 2019-05-24T11:49:50Z 281 | 282 | 283 | '); 284 | 285 | $service->shouldReceive('listBuckets') 286 | ->with('zp-guangzhou') 287 | ->once() 288 | ->andReturn($mockResponse); 289 | ``` 290 | 291 | 更多测试写法请阅读:[Mockery 官方文档](http://docs.mockery.io/en/latest/index.html) 292 | 293 | ## :heart: Sponsor me 294 | 295 | [![Sponsor me](https://github.com/overtrue/overtrue/blob/master/sponsor-me.svg?raw=true)](https://github.com/sponsors/overtrue) 296 | 297 | 如果你喜欢我的项目并想支持它,[点击这里 :heart:](https://github.com/sponsors/overtrue) 298 | 299 | 300 | ## Project supported by JetBrains 301 | 302 | Many thanks to Jetbrains for kindly providing a license for me to work on this and other open-source projects. 303 | 304 | [![](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=https://github.com/overtrue) 305 | 306 | ## Contributing 307 | 308 | You can contribute in one of three ways: 309 | 310 | 1. File bug reports using the [issue tracker](https://github.com/vendor/package/issues). 311 | 2. Answer questions or fix bugs on the [issue tracker](https://github.com/vendor/package/issues). 312 | 3. Contribute new features or update the wiki. 313 | 314 | _The code contribution process is not very formal. You just need to make sure that you follow the PSR-0, PSR-1, and PSR-2 coding guidelines. Any new code contributions must be accompanied by unit tests where applicable._ 315 | 316 | ## License 317 | 318 | MIT 319 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overtrue/qcloud-cos-client", 3 | "description": "Client of QCloud.com COS", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "overtrue", 8 | "email": "anzhengchao@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=8.0.2", 13 | "psr/http-message": "^1.0|^2.0", 14 | "guzzlehttp/guzzle": "^7.4", 15 | "ext-libxml": "*", 16 | "ext-simplexml": "*", 17 | "ext-json": "*", 18 | "ext-dom": "*", 19 | "thenorthmemory/xml": "^1.0" 20 | }, 21 | "require-dev": { 22 | "brainmaestro/composer-git-hooks": "^2.8", 23 | "friendsofphp/php-cs-fixer": "^3.5", 24 | "mockery/mockery": "^1.0", 25 | "phpunit/phpunit": "^9.5", 26 | "monolog/monolog": "^2.5", 27 | "laravel/pint": "^1.2" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Overtrue\\CosClient\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Overtrue\\CosClient\\Tests\\": "tests" 37 | } 38 | }, 39 | "extra": { 40 | "hooks": { 41 | "pre-commit": [ 42 | "composer test", 43 | "composer check-style" 44 | ], 45 | "pre-push": [ 46 | "composer test", 47 | "composer check-style" 48 | ] 49 | } 50 | }, 51 | "scripts": { 52 | "post-update-cmd": [ 53 | "cghooks update" 54 | ], 55 | "post-merge": "composer install", 56 | "post-install-cmd": [ 57 | "cghooks add --ignore-lock", 58 | "cghooks update" 59 | ], 60 | "cghooks": "vendor/bin/cghooks", 61 | "check-style": "vendor/bin/pint --test", 62 | "fix-style": "vendor/bin/pint", 63 | "test": "vendor/bin/phpunit" 64 | }, 65 | "scripts-descriptions": { 66 | "test": "Run all tests.", 67 | "check-style": "Run style checks (only dry run - no fixing!).", 68 | "fix-style": "Run style checks and fix violations." 69 | } 70 | } -------------------------------------------------------------------------------- /src/BucketClient.php: -------------------------------------------------------------------------------- 1 | put('/', empty($body) ? [] : [ 14 | 'body' => XML::fromArray($body), 15 | ]); 16 | } 17 | 18 | public function headBucket(): Http\Response 19 | { 20 | return $this->head('/'); 21 | } 22 | 23 | public function deleteBucket(): Http\Response 24 | { 25 | return $this->delete('/'); 26 | } 27 | 28 | public function getObjects(array $query = []): Http\Response 29 | { 30 | return $this->get('/', \compact('query')); 31 | } 32 | 33 | public function getObjectVersions(array $query = []): Http\Response 34 | { 35 | return $this->get('/?versions', \compact('query')); 36 | } 37 | 38 | public function putACL(array $body = [], array $headers = []): Http\Response 39 | { 40 | return $this->put('/?acl', \array_filter([ 41 | 'headers' => $headers, 42 | 'body' => XML::fromArray($body), 43 | ])); 44 | } 45 | 46 | public function getACL(): Http\Response 47 | { 48 | return $this->get('/?acl'); 49 | } 50 | 51 | public function putCORS(array $body): Http\Response 52 | { 53 | return $this->put('/?cors', [ 54 | 'body' => XML::fromArray($body), 55 | ]); 56 | } 57 | 58 | public function getCORS(): Http\Response 59 | { 60 | return $this->get('/?cors'); 61 | } 62 | 63 | public function deleteCORS(): Http\Response 64 | { 65 | return $this->delete('/?cors'); 66 | } 67 | 68 | public function putLifecycle(array $body): Http\Response 69 | { 70 | return $this->put('/?lifecycle', [ 71 | 'body' => XML::fromArray($body), 72 | ]); 73 | } 74 | 75 | public function getLifecycle(): Http\Response 76 | { 77 | return $this->get('/?lifecycle'); 78 | } 79 | 80 | public function deleteLifecycle(): Http\Response 81 | { 82 | return $this->delete('/?lifecycle'); 83 | } 84 | 85 | public function putPolicy(array $body): Http\Response 86 | { 87 | return $this->put('/?policy', ['json' => $body]); 88 | } 89 | 90 | public function getPolicy(): Http\Response 91 | { 92 | return $this->get('/?policy'); 93 | } 94 | 95 | public function deletePolicy(): Http\Response 96 | { 97 | return $this->delete('/?policy'); 98 | } 99 | 100 | public function putReferer(array $body): Http\Response 101 | { 102 | return $this->put('/?referer', [ 103 | 'body' => XML::fromArray($body), 104 | ]); 105 | } 106 | 107 | public function getReferer(): Http\Response 108 | { 109 | return $this->get('/?referer'); 110 | } 111 | 112 | public function putTagging(array $body): Http\Response 113 | { 114 | return $this->put('/?tagging', [ 115 | 'body' => XML::fromArray($body), 116 | ]); 117 | } 118 | 119 | public function getTagging(): Http\Response 120 | { 121 | return $this->get('/?tagging'); 122 | } 123 | 124 | public function deleteTagging(): Http\Response 125 | { 126 | return $this->delete('/?tagging'); 127 | } 128 | 129 | public function putWebsite(array $body): Http\Response 130 | { 131 | return $this->put('/?website', [ 132 | 'body' => XML::fromArray($body), 133 | ]); 134 | } 135 | 136 | public function getWebsite(): Http\Response 137 | { 138 | return $this->get('/?website'); 139 | } 140 | 141 | public function deleteWebsite(): Http\Response 142 | { 143 | return $this->delete('/?website'); 144 | } 145 | 146 | public function putInventory(string $id, array $body): Http\Response 147 | { 148 | return $this->put(\sprintf('/?inventory&id=%s', $id), [ 149 | 'body' => XML::fromArray($body), 150 | ]); 151 | } 152 | 153 | public function getInventory(string $id): Http\Response 154 | { 155 | return $this->get(\sprintf('/?inventory&id=%s', $id)); 156 | } 157 | 158 | public function getInventoryConfigurations(?string $nextContinuationToken = null): Http\Response 159 | { 160 | return $this->get(\sprintf('/?inventory&continuation-token=%s', $nextContinuationToken)); 161 | } 162 | 163 | public function deleteInventory(string $id): Http\Response 164 | { 165 | return $this->delete(\sprintf('/?inventory&id=%s', $id)); 166 | } 167 | 168 | public function putVersioning(array $body): Http\Response 169 | { 170 | return $this->put('/?versioning', [ 171 | 'body' => XML::fromArray($body), 172 | ]); 173 | } 174 | 175 | public function getVersioning(): Http\Response 176 | { 177 | return $this->get('/?versioning'); 178 | } 179 | 180 | public function putReplication(array $body): Http\Response 181 | { 182 | return $this->put('/?replication', [ 183 | 'body' => XML::fromArray($body), 184 | ]); 185 | } 186 | 187 | public function getReplication(): Http\Response 188 | { 189 | return $this->get('/?replication'); 190 | } 191 | 192 | public function deleteReplication(): Http\Response 193 | { 194 | return $this->delete('/?replication'); 195 | } 196 | 197 | public function putLogging(array $body): Http\Response 198 | { 199 | return $this->put('/?logging', [ 200 | 'body' => XML::fromArray($body), 201 | ]); 202 | } 203 | 204 | public function getLogging(): Http\Response 205 | { 206 | return $this->get('/?logging'); 207 | } 208 | 209 | public function putAccelerate(array $body): Http\Response 210 | { 211 | return $this->put('/?accelerate', [ 212 | 'body' => XML::fromArray($body), 213 | ]); 214 | } 215 | 216 | public function getAccelerate(): Http\Response 217 | { 218 | return $this->get('/?accelerate'); 219 | } 220 | 221 | public function putEncryption(array $body): Http\Response 222 | { 223 | return $this->put('/?encryption', [ 224 | 'body' => XML::fromArray($body), 225 | ]); 226 | } 227 | 228 | public function getEncryption(): Http\Response 229 | { 230 | return $this->get('/?encryption'); 231 | } 232 | 233 | public function deleteEncryption(): Http\Response 234 | { 235 | return $this->delete('/?encryption'); 236 | } 237 | } -------------------------------------------------------------------------------- /src/CiClient.php: -------------------------------------------------------------------------------- 1 | -.ci..myqcloud.com'; 10 | 11 | protected array $requiredConfigKeys = ['bucket']; 12 | 13 | public function detectImage(array $body): Http\Response 14 | { 15 | return $this->post('/image/auditing', [ 16 | 'body' => XML::fromArray($body), 17 | ]); 18 | } 19 | 20 | public function getImageJob(string $jobId): Http\Response 21 | { 22 | return $this->get(\sprintf('/image/auditing/%s', $jobId)); 23 | } 24 | 25 | public function detectVideo(array $body): Http\Response 26 | { 27 | return $this->post('/video/auditing', [ 28 | 'body' => XML::fromArray($body), 29 | ]); 30 | } 31 | 32 | public function getVideoJob(string $jobId): Http\Response 33 | { 34 | return $this->get(\sprintf('/video/auditing/%s', $jobId)); 35 | } 36 | 37 | public function detectAudio(array $body): Http\Response 38 | { 39 | return $this->post('/audio/auditing', [ 40 | 'body' => XML::fromArray($body), 41 | ]); 42 | } 43 | 44 | public function getAudioJob(string $jobId): Http\Response 45 | { 46 | return $this->get(\sprintf('/audio/auditing/%s', $jobId)); 47 | } 48 | 49 | public function detectText(array $body): Http\Response 50 | { 51 | return $this->post('/text/auditing', [ 52 | 'body' => XML::fromArray($body), 53 | ]); 54 | } 55 | 56 | public function getTextJob(string $jobId): Http\Response 57 | { 58 | return $this->get(\sprintf('/text/auditing/%s', $jobId)); 59 | } 60 | 61 | public function detectDocument(array $body): Http\Response 62 | { 63 | return $this->post('/document/auditing', [ 64 | 'body' => XML::fromArray($body), 65 | ]); 66 | } 67 | 68 | public function getDocumentJob(string $jobId): Http\Response 69 | { 70 | return $this->get(\sprintf('/document/auditing/%s', $jobId)); 71 | } 72 | 73 | public function detectWebPage(array $body): Http\Response 74 | { 75 | return $this->post('/webpage/auditing', [ 76 | 'body' => XML::fromArray($body), 77 | ]); 78 | } 79 | 80 | public function getWebPageJob(string $jobId): Http\Response 81 | { 82 | return $this->get(\sprintf('/webpage/auditing/%s', $jobId)); 83 | } 84 | 85 | public function detectLiveVideo(array $body): Http\Response 86 | { 87 | return $this->post('/video/auditing', [ 88 | 'body' => XML::fromArray($body), 89 | ]); 90 | } 91 | 92 | public function getLiveVideoJob(string $jobId): Http\Response 93 | { 94 | return $this->get(\sprintf('/video/auditing/%s', $jobId)); 95 | } 96 | 97 | public function deleteLiveVideoJob(string $jobId): Http\Response 98 | { 99 | return $this->post(\sprintf('/video/cancel_auditing/%s', $jobId)); 100 | } 101 | 102 | public function reportBadcase(array $body): Http\Response 103 | { 104 | return $this->post('/report/badcase', [ 105 | 'body' => XML::fromArray($body), 106 | ]); 107 | } 108 | 109 | public function detectVirus(array $body): Http\Response 110 | { 111 | return $this->post('/virus/detect', [ 112 | 'body' => XML::fromArray($body), 113 | ]); 114 | } 115 | 116 | public function getVirusJob(string $jobId): Http\Response 117 | { 118 | return $this->get(\sprintf('/virus/detect/%s', $jobId)); 119 | } 120 | 121 | public function createMediaTranscodeJobs(array $body) 122 | { 123 | $body = XML::fromArray($body, 'Request'); 124 | 125 | return $this->post('/jobs', [ 126 | 'headers' => [ 127 | 'Content-Type' => 'application/xml', 128 | ], 129 | 'body' => $body, 130 | ]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | -.cos..myqcloud.com'; 41 | 42 | protected \GuzzleHttp\Client $client; 43 | 44 | protected array $requiredConfigKeys = []; 45 | 46 | /** 47 | * @throws \Overtrue\CosClient\Exceptions\InvalidConfigException 48 | */ 49 | public function __construct(array|Config $config) 50 | { 51 | $this->config = $this->normalizeConfig($config); 52 | 53 | $this->configureDomain(); 54 | $this->configureMiddlewares(); 55 | $this->configureHttpClientOptions(); 56 | } 57 | 58 | public function getSchema(): string 59 | { 60 | return $this->config->get('use_https', true) ? 'https' : 'http'; 61 | } 62 | 63 | public function getAppId(): int 64 | { 65 | return $this->config->get('app_id', 0); 66 | } 67 | 68 | public function getSecretId(): string 69 | { 70 | return $this->config->get('secret_id', ''); 71 | } 72 | 73 | public function getSecretKey(): string 74 | { 75 | return $this->config->get('secret_key', ''); 76 | } 77 | 78 | public function getConfig(): Config 79 | { 80 | return $this->config; 81 | } 82 | 83 | public function getHttpClient(): \GuzzleHttp\Client 84 | { 85 | return $this->client ?? $this->client = $this->createHttpClient(); 86 | } 87 | 88 | /** 89 | * @throws ServerException 90 | * @throws Exception 91 | * @throws ClientException 92 | */ 93 | public function __call($method, $arguments) 94 | { 95 | try { 96 | return new Response(\call_user_func_array([$this->getHttpClient(), $method], $arguments)); 97 | } catch (\GuzzleHttp\Exception\ClientException $e) { 98 | throw new ClientException($e); 99 | } catch (\GuzzleHttp\Exception\ServerException $e) { 100 | throw new ServerException($e); 101 | } catch (\Throwable $e) { 102 | throw new Exception($e->getMessage(), $e->getCode(), $e->getPrevious()); 103 | } 104 | } 105 | 106 | protected function configureDomain(): static 107 | { 108 | $replacements = [ 109 | '' => $this->config->get('uin'), 110 | '' => $this->config->get('app_id'), 111 | '' => $this->config->get('region') ?? self::DEFAULT_REGION, 112 | '' => $this->config->get('bucket'), 113 | ]; 114 | 115 | $domain = $this->config->get('domain'); 116 | 117 | $this->domain = trim($domain ?: str_replace(array_keys($replacements), $replacements, $this->domain), '/'); 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * @throws \Overtrue\CosClient\Exceptions\InvalidConfigException 124 | */ 125 | public function normalizeConfig(array|Config $config): Config 126 | { 127 | if (is_array($config)) { 128 | $config = new Config($config); 129 | } 130 | 131 | $requiredKeys = ['app_id', 'secret_id', 'secret_key', ...$this->requiredConfigKeys]; 132 | 133 | foreach ($requiredKeys as $key) { 134 | if ($config->missing($key)) { 135 | throw new InvalidConfigException(sprintf('%s was required.', implode(', ', $requiredKeys))); 136 | } 137 | } 138 | 139 | return $config; 140 | } 141 | 142 | public function configureMiddlewares(): void 143 | { 144 | $this->pushMiddleware( 145 | new CreateRequestSignature( 146 | $this->getSecretId(), 147 | $this->getSecretKey(), 148 | $this->config->get('signature_expires') 149 | ) 150 | ); 151 | 152 | $this->pushMiddleware(new SetContentMd5()); 153 | } 154 | 155 | public static function spy(): Client|\Mockery\MockInterface|\Mockery\LegacyMockInterface 156 | { 157 | return \Mockery::mock(static::class); 158 | } 159 | 160 | public static function partialMock(): \Mockery\MockInterface 161 | { 162 | $mock = \Mockery::mock(static::class)->makePartial(); 163 | $mock->shouldReceive('getHttpClient')->andReturn(\Mockery::mock(\GuzzleHttp\Client::class)); 164 | 165 | return $mock; 166 | } 167 | 168 | public static function partialMockWithConfig(array|Config $config, array $methods = ['get', 'post', 'patch', 'put', 'delete']): \Mockery\MockInterface 169 | { 170 | if (\is_array($config)) { 171 | $config = new Config($config); 172 | } 173 | 174 | $mock = \Mockery::mock(static::class.\sprintf('[%s]', \implode(',', $methods)), [$config]); 175 | $mock->shouldReceive('getHttpClient')->andReturn(\Mockery::mock(\GuzzleHttp\Client::class)); 176 | 177 | return $mock; 178 | } 179 | 180 | protected function configureHttpClientOptions(): void 181 | { 182 | $this->setBaseUri(\sprintf('%s://%s/', $this->getSchema(), $this->domain)); 183 | $this->mergeHttpClientOptions($this->config->get('guzzle', [ 184 | 'headers' => [ 185 | 'User-Agent' => 'overtrue/qcloud-cos-client:'.\GuzzleHttp\Client::MAJOR_VERSION, 186 | ], 187 | ])); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | options; 18 | 19 | if (isset($config[$key])) { 20 | return $config[$key]; 21 | } 22 | 23 | foreach (explode('.', $key) as $segment) { 24 | if (! is_array($config) || ! array_key_exists($segment, $config)) { 25 | return $default; 26 | } 27 | $config = $config[$segment]; 28 | } 29 | 30 | return $config; 31 | } 32 | 33 | public function set(string $key, mixed $value): array 34 | { 35 | $keys = explode('.', $key); 36 | $config = &$this->options; 37 | 38 | while (count($keys) > 1) { 39 | $key = array_shift($keys); 40 | if (! isset($config[$key]) || ! is_array($config[$key])) { 41 | $config[$key] = []; 42 | } 43 | $config = &$config[$key]; 44 | } 45 | 46 | $config[array_shift($keys)] = $value; 47 | 48 | return $config; 49 | } 50 | 51 | public function has(string $key): bool 52 | { 53 | return (bool) $this->get($key); 54 | } 55 | 56 | public function missing(string $key): bool 57 | { 58 | return ! $this->has($key); 59 | } 60 | 61 | #[Pure] 62 | public function extend(array $options): Config 63 | { 64 | return new Config(\array_merge($this->options, $options)); 65 | } 66 | 67 | public function offsetExists($offset): bool 68 | { 69 | return array_key_exists($offset, $this->options); 70 | } 71 | 72 | public function offsetGet(mixed $offset): mixed 73 | { 74 | return $this->get($offset); 75 | } 76 | 77 | public function offsetSet(mixed $offset, mixed $value): void 78 | { 79 | $this->set($offset, $value); 80 | } 81 | 82 | public function offsetUnset(mixed $offset): void 83 | { 84 | $this->set($offset, null); 85 | } 86 | 87 | public function jsonSerialize(): array 88 | { 89 | return $this->options; 90 | } 91 | 92 | public function __toString() 93 | { 94 | return \json_encode($this, \JSON_UNESCAPED_UNICODE); 95 | } 96 | } -------------------------------------------------------------------------------- /src/Exceptions/ClientException.php: -------------------------------------------------------------------------------- 1 | guzzleClientException = $guzzleServerException; 17 | 18 | parent::__construct($guzzleServerException->getMessage(), $guzzleServerException->getCode(), $guzzleServerException->getPrevious()); 19 | } 20 | 21 | public function getResponse(): ResponseInterface 22 | { 23 | return new Response($this->guzzleClientException->getResponse()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | guzzleServerException = $guzzleServerException; 17 | 18 | parent::__construct($guzzleServerException->getMessage(), $guzzleServerException->getCode(), $guzzleServerException->getPrevious()); 19 | } 20 | 21 | public function getResponse(): ResponseInterface 22 | { 23 | return new Response($this->guzzleServerException->getResponse()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Response.php: -------------------------------------------------------------------------------- 1 | getStatusCode(), 17 | $response->getHeaders(), 18 | $response->getBody(), 19 | $response->getProtocolVersion(), 20 | $response->getReasonPhrase() 21 | ); 22 | } 23 | 24 | public function toArray() 25 | { 26 | if (! \is_null($this->arrayResult)) { 27 | return $this->arrayResult; 28 | } 29 | 30 | $contents = $this->getContents(); 31 | 32 | if (empty($contents)) { 33 | return $this->arrayResult = null; 34 | } 35 | 36 | return $this->arrayResult = $this->isXML() ? XML::toArray($contents) : \json_decode($contents, true); 37 | } 38 | 39 | public function toObject(): ?object 40 | { 41 | return \json_decode(\json_encode($this->toArray())); 42 | } 43 | 44 | public function isXML(): bool 45 | { 46 | return \strpos($this->getHeaderLine('content-type'), 'xml') > 0; 47 | } 48 | 49 | public function jsonSerialize(): mixed 50 | { 51 | try { 52 | return $this->toArray(); 53 | } catch (\Exception $e) { 54 | return []; 55 | } 56 | } 57 | 58 | public function offsetExists(mixed $offset): bool 59 | { 60 | return \array_key_exists($offset, $this->toArray()); 61 | } 62 | 63 | public function offsetGet(mixed $offset): mixed 64 | { 65 | return $this->toArray()[$offset] ?? null; 66 | } 67 | 68 | public function offsetSet(mixed $offset, mixed $value): void 69 | { 70 | } 71 | 72 | public function offsetUnset(mixed $offset): void 73 | { 74 | } 75 | 76 | public static function create( 77 | $status = 200, 78 | array $headers = [], 79 | $body = null, 80 | $version = '1.1', 81 | $reason = null 82 | ): Response { 83 | return new self(new \GuzzleHttp\Psr7\Response($status, $headers, $body, $version, $reason)); 84 | } 85 | 86 | public function toString(): string 87 | { 88 | return $this->getContents(); 89 | } 90 | 91 | public function getContents(): string 92 | { 93 | $this->getBody()->rewind(); 94 | 95 | return $this->getBody()->getContents(); 96 | } 97 | 98 | #[Pure] 99 | final public function isInformational(): bool 100 | { 101 | return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; 102 | } 103 | 104 | #[Pure] 105 | final public function isSuccessful(): bool 106 | { 107 | return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; 108 | } 109 | 110 | #[Pure] 111 | final public function isRedirection(): bool 112 | { 113 | return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; 114 | } 115 | 116 | #[Pure] 117 | final public function isClientError(): bool 118 | { 119 | return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; 120 | } 121 | 122 | #[Pure] 123 | final public function isServerError(): bool 124 | { 125 | return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; 126 | } 127 | 128 | #[Pure] 129 | final public function isOk(): bool 130 | { 131 | return $this->getStatusCode() === 200; 132 | } 133 | 134 | #[Pure] 135 | final public function isForbidden(): bool 136 | { 137 | return $this->getStatusCode() === 403; 138 | } 139 | 140 | #[Pure] 141 | final public function isNotFound(): bool 142 | { 143 | return $this->getStatusCode() === 404; 144 | } 145 | 146 | #[Pure] 147 | final public function isEmpty(): bool 148 | { 149 | return \in_array($this->getStatusCode(), [204, 304]) || empty($this->getContents()); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/JobClient.php: -------------------------------------------------------------------------------- 1 | .cos-control..myqcloud.com'; 10 | 11 | protected array $requiredConfigKeys = ['uin', 'region']; 12 | 13 | public function __construct(array|Config $config) 14 | { 15 | parent::__construct($config); 16 | 17 | $this->setHeader('x-cos-appid', $this->config->get('app_id')); 18 | } 19 | 20 | public function getJobs(array $query = []): Http\Response 21 | { 22 | return $this->get('/jobs', [ 23 | 'query' => $query, 24 | ]); 25 | } 26 | 27 | public function createJob(array $body): Http\Response 28 | { 29 | return $this->post('/jobs', [ 30 | 'body' => XML::fromArray($body), 31 | ]); 32 | } 33 | 34 | public function describeJob(string $id): Http\Response 35 | { 36 | return $this->get(\sprintf('/jobs/%s', $id)); 37 | } 38 | 39 | public function updateJobPriority(string $id, int $priority): Http\Response 40 | { 41 | return $this->post(\sprintf('/jobs/%s/priority', $id), [ 42 | 'query' => [ 43 | 'priority' => $priority, 44 | ], 45 | ]); 46 | } 47 | 48 | public function updateJobStatus(string $id, array $query): Http\Response 49 | { 50 | return $this->post(\sprintf('/jobs/%s/status', $id), \compact('query')); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Middleware/CreateRequestSignature.php: -------------------------------------------------------------------------------- 1 | withHeader( 21 | 'Authorization', 22 | (new Signature($this->secretId, $this->secretKey)) 23 | ->createAuthorizationHeader($request, $this->signatureExpires) 24 | ); 25 | 26 | return $handler($request, $options); 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Middleware/SetContentMd5.php: -------------------------------------------------------------------------------- 1 | withHeader( 13 | 'Content-MD5', 14 | base64_encode(md5($request->getBody()->getContents(), true)) 15 | ); 16 | 17 | return $handler($request, $options); 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ObjectClient.php: -------------------------------------------------------------------------------- 1 | put(\rawurlencode($key), \compact('body', 'headers')); 15 | } 16 | 17 | /** 18 | * @throws \Overtrue\CosClient\Exceptions\InvalidArgumentException 19 | */ 20 | public function copyObject(string $key, array $headers): Http\Response 21 | { 22 | if (empty($headers['x-cos-copy-source'])) { 23 | throw new InvalidArgumentException('Missing required header: x-cos-copy-source'); 24 | } 25 | 26 | if (($headers['x-cos-metadata-directive'] ?? 'Copy') === 'Replaced' && empty($headers['Content-Type'])) { 27 | throw new InvalidArgumentException('Missing required header: Content-Type'); 28 | } 29 | 30 | return $this->put(\rawurlencode($key), array_filter(\compact('headers'))); 31 | } 32 | 33 | /** 34 | * @see https://docs.guzzlephp.org/en/stable/request-options.html#multipart 35 | */ 36 | public function postObject(array $multipart): Http\Response 37 | { 38 | return $this->post('/', \compact('multipart')); 39 | } 40 | 41 | public function getObject(string $key, array $query = [], array $headers = []): Http\Response 42 | { 43 | return $this->get(\rawurlencode($key), \compact('query', 'headers')); 44 | } 45 | 46 | public function headObject(string $key, ?string $versionId = null, array $headers = []): Http\Response 47 | { 48 | return $this->head(\rawurlencode($key), [ 49 | 'query' => \compact('versionId'), 50 | 'headers' => $headers, 51 | ]); 52 | } 53 | 54 | public function deleteObject(string $key, ?string $versionId = null): Http\Response 55 | { 56 | return $this->delete(\rawurlencode($key), [ 57 | 'query' => \compact('versionId'), 58 | ]); 59 | } 60 | 61 | public function deleteObjects(array $body): Http\Response 62 | { 63 | if (array_key_first($body) == 'Delete') { 64 | $body = $body['Delete']; 65 | } 66 | 67 | return $this->post('/?delete', ['body' => XML::fromArray($body, 'Delete')]); 68 | } 69 | 70 | public function optionsObject(string $key): Http\Response 71 | { 72 | return $this->options(\rawurlencode($key)); 73 | } 74 | 75 | public function restoreObject(string $key, array $body, ?string $versionId = null): Http\Response 76 | { 77 | if (array_key_first($body) == 'RestoreRequest') { 78 | $body = $body['RestoreRequest']; 79 | } 80 | 81 | return $this->post(\rawurlencode($key), [ 82 | 'query' => [ 83 | 'restore' => '', 84 | 'versionId' => $versionId, 85 | ], 86 | 'body' => XML::fromArray($body, 'RestoreRequest'), 87 | ]); 88 | } 89 | 90 | public function selectObjectContents(string $key, array $body): Http\Response 91 | { 92 | if (array_key_first($body) == 'SelectRequest') { 93 | $body = $body['SelectRequest']; 94 | } 95 | 96 | return $this->post(\rawurlencode($key), [ 97 | 'query' => [ 98 | 'select' => '', 99 | 'select-type' => 2, 100 | ], 101 | 'body' => XML::fromArray($body, 'SelectRequest'), 102 | ]); 103 | } 104 | 105 | public function putObjectACL(string $key, array $body, array $headers = []): Http\Response 106 | { 107 | if (array_key_first($body) == 'AccessControlPolicy') { 108 | $body = $body['AccessControlPolicy']; 109 | } 110 | 111 | return $this->put(\rawurlencode($key), [ 112 | 'query' => [ 113 | 'acl' => '', 114 | ], 115 | 'body' => XML::fromArray($body, 'AccessControlPolicy'), 116 | 'headers' => $headers, 117 | ]); 118 | } 119 | 120 | public function getObjectACL(string $key): Http\Response 121 | { 122 | return $this->get(\rawurlencode($key), [ 123 | 'query' => [ 124 | 'acl' => '', 125 | ], 126 | ]); 127 | } 128 | 129 | public function putObjectTagging(string $key, array $body, ?string $versionId = null): Http\Response 130 | { 131 | if (array_key_first($body) == 'Tagging') { 132 | $body = $body['Tagging']; 133 | } 134 | 135 | return $this->put(\rawurlencode($key), [ 136 | 'query' => [ 137 | 'tagging' => '', 138 | 'VersionId' => $versionId, 139 | ], 140 | 'body' => XML::fromArray($body, 'Tagging'), 141 | ]); 142 | } 143 | 144 | public function getObjectTagging(string $key, ?string $versionId = null): Http\Response 145 | { 146 | return $this->get(\rawurlencode($key), [ 147 | 'query' => [ 148 | 'tagging' => '', 149 | 'VersionId' => $versionId, 150 | ], 151 | ]); 152 | } 153 | 154 | public function deleteObjectTagging(string $key, ?string $versionId = null): Http\Response 155 | { 156 | return $this->delete(\rawurlencode($key), [ 157 | 'query' => [ 158 | 'tagging' => '', 159 | 'VersionId' => $versionId, 160 | ], 161 | ]); 162 | } 163 | 164 | /** 165 | * @throws InvalidArgumentException 166 | */ 167 | public function createUploadId(string $key, array $headers): Http\Response 168 | { 169 | if (empty($headers['Content-Type'])) { 170 | throw new InvalidArgumentException('Missing required headers: Content-Type'); 171 | } 172 | 173 | return $this->post(\rawurlencode($key), [ 174 | 'query' => [ 175 | 'uploads' => '', 176 | ], 177 | 'headers' => $headers, 178 | ]); 179 | } 180 | 181 | public function uploadPart(string $key, int $partNumber, string $uploadId, string $body, array $headers = []): Http\Response 182 | { 183 | return $this->putPart(...\func_get_args()); 184 | } 185 | 186 | public function putPart(string $key, int $partNumber, string $uploadId, string $body, array $headers = []): Http\Response 187 | { 188 | return $this->put(\rawurlencode($key), [ 189 | 'query' => \compact('partNumber', 'uploadId'), 190 | 'headers' => $headers, 191 | 'body' => $body, 192 | ]); 193 | } 194 | 195 | /** 196 | * @throws InvalidArgumentException 197 | */ 198 | public function copyPart(string $key, int $partNumber, string $uploadId, array $headers = []): Http\Response 199 | { 200 | if (empty($headers['x-cos-copy-source'])) { 201 | throw new InvalidArgumentException('Missing required header: x-cos-copy-source'); 202 | } 203 | 204 | return $this->put(\rawurlencode($key), [ 205 | 'query' => \compact('partNumber', 'uploadId'), 206 | 'headers' => $headers, 207 | ]); 208 | } 209 | 210 | public function markUploadAsCompleted(string $key, string $uploadId, array $body): Http\Response 211 | { 212 | if (array_key_first($body) == 'CompleteMultipartUpload') { 213 | $body = $body['CompleteMultipartUpload']; 214 | } 215 | 216 | return $this->post(\rawurlencode($key), [ 217 | 'query' => [ 218 | 'uploadId' => $uploadId, 219 | ], 220 | 'body' => XML::fromArray($body, 'CompleteMultipartUpload', 'Part'), 221 | ]); 222 | } 223 | 224 | public function markUploadAsAborted(string $key, string $uploadId): Http\Response 225 | { 226 | return $this->delete(\rawurlencode($key), [ 227 | 'query' => [ 228 | 'uploadId' => $uploadId, 229 | ], 230 | ]); 231 | } 232 | 233 | public function getUploadJobs(array $query = []): Http\Response 234 | { 235 | return $this->get('/?uploads', \compact('query')); 236 | } 237 | 238 | public function getUploadedParts(string $key, string $uploadId, array $query = []): Http\Response 239 | { 240 | $query['uploadId'] = $uploadId; 241 | 242 | return $this->get(\rawurlencode($key), compact('query')); 243 | } 244 | 245 | public function getObjectUrl(string $key): string 246 | { 247 | return \sprintf('%s/%s', \rtrim($this->getBaseUri(), '/'), \ltrim($key, '/')); 248 | } 249 | 250 | public function getObjectSignedUrl(string $key, ?string $expires = '+60 minutes'): string 251 | { 252 | $objectUrl = $this->getObjectUrl($key); 253 | $signature = new Signature($this->config['secret_id'], $this->config['secret_key']); 254 | $request = new Request('GET', $objectUrl); 255 | 256 | return \strval((new Uri($objectUrl))->withQuery(\http_build_query(['sign' => $signature->createAuthorizationHeader($request, $expires)]))); 257 | } 258 | 259 | public function detectImage(string $key, array $query = []): Http\Response 260 | { 261 | $query['ci-process'] = 'sensitive-content-recognition'; 262 | 263 | return $this->getObject($key, $query); 264 | } 265 | } -------------------------------------------------------------------------------- /src/ServiceClient.php: -------------------------------------------------------------------------------- 1 | get($uri); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Signature.php: -------------------------------------------------------------------------------- 1 | getMethod())."\n".urldecode($request->getUri()->getPath())."\n". 40 | implode('&', array_values($queryToBeSigned)). 41 | "\n".\http_build_query($headersToBeSigned)."\n" 42 | ); 43 | 44 | $stringToSign = \sprintf("sha1\n%s\n%s\n", $signTime, $httpStringHashed); 45 | $signature = hash_hmac('sha1', $stringToSign, hash_hmac('sha1', $signTime, $this->secretKey)); 46 | 47 | return \sprintf( 48 | 'q-sign-algorithm=sha1&q-ak=%s&q-sign-time=%s&q-key-time=%s&q-header-list=%s&q-url-param-list=%s&q-signature=%s', 49 | $this->accessKey, 50 | $signTime, 51 | $signTime, 52 | implode(';', array_keys($headersToBeSigned)), 53 | implode(';', array_keys($queryToBeSigned)), 54 | $signature 55 | ); 56 | } 57 | 58 | protected static function getHeadersToBeSigned(RequestInterface $request): array 59 | { 60 | $headers = []; 61 | foreach ($request->getHeaders() as $header => $value) { 62 | $header = strtolower(urlencode($header)); 63 | 64 | if (str_contains($header, 'x-cos-') || \in_array($header, self::SIGN_HEADERS)) { 65 | $headers[$header] = $value[0]; 66 | } 67 | } 68 | 69 | ksort($headers); 70 | 71 | return $headers; 72 | } 73 | 74 | protected static function getQueryToBeSigned(RequestInterface $request): array 75 | { 76 | $query = []; 77 | foreach (explode('&', $request->getUri()->getQuery()) as $item) { 78 | if (! empty($item)) { 79 | $segments = explode('=', $item); 80 | $key = strtolower($segments[0]); 81 | if (count($segments) >= 2) { 82 | $value = $segments[1]; 83 | } else { 84 | $value = ''; 85 | } 86 | $query[$key] = $key.'='.$value; 87 | } 88 | } 89 | ksort($query); 90 | 91 | return $query; 92 | } 93 | 94 | protected static function getTimeSegments(int|string|\DateTimeInterface $expires = '+60 minutes'): string 95 | { 96 | $timezone = \date_default_timezone_get(); 97 | 98 | date_default_timezone_set('Asia/Shanghai'); 99 | 100 | // '900'/900 101 | if (is_numeric($expires)) { 102 | $expires = abs($expires); 103 | } 104 | 105 | $expires = match (true) { 106 | // 900/1700001234 107 | is_int($expires) => $expires >= time() ? $expires : time() + $expires, 108 | // '+60 minutes'/'2023-01-01 00:00:00' 109 | is_string($expires) => strtotime($expires), 110 | // new \DateTime('2023-01-01 00:00:00') 111 | $expires instanceof \DateTimeInterface => $expires->getTimestamp(), 112 | default => time() + 60, 113 | }; 114 | 115 | $signTime = \sprintf('%s;%s', time() - 60, $expires); 116 | 117 | date_default_timezone_set($timezone); 118 | 119 | return $signTime; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Support/XML.php: -------------------------------------------------------------------------------- 1 | $this->getHandlerStack(), 20 | ], $this->options, $options)); 21 | } 22 | 23 | public function setHttpClientOptions(array $options): static 24 | { 25 | $this->options = $options; 26 | 27 | return $this; 28 | } 29 | 30 | public function mergeHttpClientOptions(array $options): static 31 | { 32 | $this->options = array_merge($this->options, $options); 33 | 34 | return $this; 35 | } 36 | 37 | public function getBaseUri() 38 | { 39 | return $this->options['base_uri']; 40 | } 41 | 42 | public function setBaseUri(string $baseUri): self 43 | { 44 | $this->options['base_uri'] = $baseUri; 45 | 46 | return $this; 47 | } 48 | 49 | public function setHeaders(array $headers): static 50 | { 51 | foreach ($headers as $name => $value) { 52 | $this->setHeader($name, $value); 53 | } 54 | 55 | return $this; 56 | } 57 | 58 | public function setHeader(string $name, string $value): static 59 | { 60 | if (empty($this->options['headers'])) { 61 | $this->options['headers'] = []; 62 | } 63 | 64 | $this->options['headers'][$name] = $value; 65 | 66 | return $this; 67 | } 68 | 69 | public function getHttpClientOptions(): array 70 | { 71 | return $this->options; 72 | } 73 | 74 | public function pushMiddleware(callable $middleware, ?string $name = null): static 75 | { 76 | if (! is_null($name)) { 77 | $this->middlewares[$name] = $middleware; 78 | } else { 79 | $this->middlewares[] = $middleware; 80 | } 81 | 82 | return $this; 83 | } 84 | 85 | public function getMiddlewares(): array 86 | { 87 | return $this->middlewares; 88 | } 89 | 90 | public function setMiddlewares(array $middlewares): static 91 | { 92 | $this->middlewares = $middlewares; 93 | 94 | return $this; 95 | } 96 | 97 | public function setHandlerStack(HandlerStack $handlerStack): static 98 | { 99 | $this->handlerStack = $handlerStack; 100 | 101 | return $this; 102 | } 103 | 104 | public function getHandlerStack(): HandlerStack 105 | { 106 | if ($this->handlerStack) { 107 | return $this->handlerStack; 108 | } 109 | 110 | $this->handlerStack = HandlerStack::create(); 111 | 112 | foreach ($this->middlewares as $name => $middleware) { 113 | $this->handlerStack->unshift($middleware, $name); 114 | } 115 | 116 | return $this->handlerStack; 117 | } 118 | } --------------------------------------------------------------------------------