├── .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 | [](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 | [](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://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 | }
--------------------------------------------------------------------------------