Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cos Sdk V5 Laravel Package

qcloud/cos-sdk-v5

Official Tencent Cloud COS SDK for PHP. Provides a simple API for object storage: upload, download, copy, delete, batch operations, bucket and object management, multipart uploads, signatures, and temporary credentials. Composer-friendly and easy to integrate into Laravel/PHP apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require qcloud/cos-sdk-v5:^2.6.17

There is no service provider, no facade, no publishable config — it is a plain SDK. Everything below is wiring you own. v2.6.17 is additive (two new CI/AIGC media capabilities and a packaging note), so it is a safe in-place upgrade from any 2.6.x.

Where to look first

  • Qcloud\Cos\Client — the single entry point. Its __call-ish surface maps to ~40 COS REST APIs (putObject, getObject, headObject, listObjects, copyObject, deleteObject, deleteObjects, upload, download, getObjectUrl, putObjectAcl, …), plus the Cloud Infinite (CI) media helpers you reach for transcoding.
  • Qcloud\Cos\Exception\SpaceException / ServiceResponseException — what you'll actually be catching.
  • The README's config block: region, schema, credentials.{secretId,secretKey,token}.

Minimal config — put this in config/services.php, not in code:

'cos' => [
    'region'      => env('COS_REGION', 'ap-guangzhou'),
    'bucket'      => env('COS_BUCKET'),          // e.g. mybucket-1250000000
    'credentials' => [
        'secretId'  => env('COS_SECRET_ID'),
        'secretKey' => env('COS_SECRET_KEY'),
        'token'     => env('COS_SESSION_TOKEN'),  // only for STS temp creds
    ],
],

Bind it once in AppServiceProvider::register():

$this->app->singleton(\Qcloud\Cos\Client::class, function ($app) {
    $c = config('services.cos');
    return new \Qcloud\Cos\Client([
        'region'      => $c['region'],
        'schema'      => 'https',
        'credentials' => $c['credentials'],
    ]);
});

The client is stateless and safe to singleton — do this rather than new Client() per request, otherwise you pay the Guzzle client construction cost on every call.

First use case — upload a user avatar and return a time-limited URL:

$cos = app(\Qcloud\Cos\Client::class);
$bucket = config('services.cos.bucket');

$cos->putObject([
    'Bucket'      => $bucket,
    'Key'         => "avatars/{$user->id}.jpg",
    'Body'        => fopen($path, 'rb'),
    'ContentType' => 'image/jpeg',
    'ACL'         => 'private',
]);

$url = $cos->getObjectUrl($bucket, "avatars/{$user->id}.jpg", '+30 minutes');

Implementation Patterns

Wrap, don't sprinkle

Injecting Client directly into ten controllers makes bucket/key conventions unenforceable. Put a thin CosStorage service on top that owns bucket selection, key prefixing, and error translation:

class CosStorage
{
    public function __construct(private \Qcloud\Cos\Client $cos, private array $cfg) {}

    public function put(string $key, $stream, array $opts = []): string
    {
        $this->cos->putObject($opts + [
            'Bucket' => $this->cfg['bucket'],
            'Key'    => $key,
            'Body'   => $stream,
        ]);
        return $key;
    }

    public function temporaryUrl(string $key, int $minutes = 30): string
    {
        return $this->cos->getObjectUrl($this->cfg['bucket'], $key, "+{$minutes} minutes");
    }
}

This also gives you one place to swap in a fake for tests.

Direct-to-COS uploads (the pattern most apps actually want)

Don't proxy multi-megabyte uploads through PHP-FPM. Issue a presigned PUT and let the browser/CLI upload:

$cmd = $cos->getCommand('PutObject', [
    'Bucket' => $bucket,
    'Key'    => $key,
    'ContentType' => $contentType,
]);
$url = $cos->getPresignedUrl($cmd, '+15 minutes');

Return $url to the front end; it does PUT $url with the raw body. Caveat: the signature covers the method, key, and headers you signed — if you sign ContentType, the client must send exactly that header or you get SignatureDoesNotMatch.

Large files: use upload(), not putObject()

putObject holds the whole payload in memory and dies past a few hundred MB. upload() does multipart automatically:

$cos->upload(
    $bucket,
    $key,
    fopen($tmpPath, 'rb'),
    ['partSize' => 10 * 1024 * 1024, 'concurrency' => 5]
);

partSize must be ≥1 MB (and ≥5 MB for all but the last part); concurrency maps to parallel Guzzle requests. Tune partSize down and concurrency up if you're memory-constrained; tune the reverse on thin bandwidth.

Downloads

Don't echo $result['Body'] for anything large — it's a PSR-7 stream. Either hand it to StreamedResponse:

return response()->streamDownload(function () use ($result) {
    $body = $result['Body'];
    while (! $body->eof()) { echo $body->read(8192); }
}, $filename, ['Content-Type' => $result['ContentType']]);

or use getObject(['SaveAs' => $localPath]) in a queued job for offline processing.

Prefixes instead of folders

COS has no directories. Model your "folders" as users/{id}/, exports/{tenant}/ prefixes and list with Prefix + Delimiter => '/' to get a fake directory listing. Paginate deliberately:

$marker = null;
do {
    $r = $cos->listObjects([
        'Bucket' => $bucket, 'Prefix' => $prefix, 'MaxKeys' => 1000,
        'Marker' => $marker,
    ]);
    $marker = $r['NextMarker'] ?? null;
    // process $r['Contents']
} while ($r['IsTruncated'] && $marker);

Note MaxKeys caps at 1000 server-side regardless of what you pass.

Queue the heavy work

Move copy, delete-many, and post-processing into jobs. The SDK blocks on network I/O; a 500-object deleteObjects batch inside a web request will blow your timeout. Also set 'timeout' => 30 and 'connect_timeout' => 5 in the client config — the defaults are effectively unbounded for slow regions.

Media jobs and AIGC metadata (new in 2.6.17)

If you transcode/transmux audio or video through CI, 2.6.17 lets you attach AIGCMetadata to the transcode/transmux job request, and adds a query API for the AIGC metadata tag on a stored media object. Practical use: labelling AI-generated audio/video at the pipeline boundary (Chinese AIGC disclosure rules make this a compliance requirement, not a nice-to-have) and reading the label back when serving or auditing an asset. The option rides along with the job's Input/Operation payload, so thread it through your transcode wrapper rather than at call sites:

public function transcode(string $key, array $operation, array $aigc = []): array
{
    return $this->cos->ciMediaTranscode([
        'Bucket'    => $this->cfg['bucket'],
        'Input'     => ['Object' => $key],
        'Operation' => $operation,
    ] + $aigc);   // ['AIGCMetadata' => [...]]
}

This is a narrow vertical feature — if you only store user uploads, ignore it entirely. It is additive and does not change any existing signature. Check vendor/qcloud/cos-sdk-v5/src/ for the exact key shape, since the release notes don't spell it out and there is no doc beyond the COS REST reference.

Storage facade, if your team insists on it

The SDK has no Flysystem adapter. Add overtrue/flysystem-cos (which depends on this SDK) if you need Storage::disk('cos')->put(...). Be aware you then maintain two config surfaces and lose access to COS-specific options (storage class, ACLs, tagging, and now AIGC metadata) unless the adapter passes them through.

Access control

Default to private and mint URLs. Use ACL => 'public-read' only for genuinely public assets (you can also set a bucket policy). For per-user isolation, key prefixing plus presigned URLs is simpler and safer than per-object ACLs.


Gotchas and Tips

Region mismatch is the #1 support ticket. The region in config must match the bucket's actual region, not the nearest one to your users. A mismatch manifests as a 301/PermanentRedirect or a confusing signature error — not a clear "wrong region" message. Parse the x-cos-request-id header when escalating.

Guzzle conflicts — now with two shipped builds. The SDK pulls guzzlehttp/guzzle (Laravel/Socialite/aws-sdk do too), so a composer update can produce an unsatisfiable constraint or silently downgrade Guzzle app-wide. Check composer why guzzlehttp/guzzle after install and pin accordingly. Since 2.6.17 the upstream project is explicit about the split: PHP ≥5.6 and <7.2.5 must use the cos-sdk-v5-6.tar.gz build (Guzzle 6), PHP ≥7.2.5 uses cos-sdk-v5-7.tar.gz (Guzzle 7). In practice that means: any Laravel 10/11 app (PHP 8.x) is squarely in the Guzzle 7 camp and unaffected; only if you maintain a legacy Laravel 5.x/7.0 app on PHP 7.1 do you have to install the Guzzle 6 variant by hand rather than from Packagist, and then you own the vendored copy. Decide once, document it, and don't let composer require pick the variant for you on a mixed-fleet CI matrix.

Clock skew breaks signatures. The SDK signs with local time. A container with a drifted clock produces SignatureDoesNotMatch with no other symptom. Keep NTP running; if you can't, check the error's ServerTime versus your container's date.

STS tokens are optional in the config array but mandatory in the header. When using temporary credentials, token must be present — omit it and you get InvalidAccessKeyId even though the secretId is valid. Credentials from environment injection are common here; make sure the token propagates to queue workers too.

Error handling is exception-based and not Laravel-shaped. ServiceResponseException::getStatusCode() gives HTTP, getErrorCode() gives the COS code (NoSuchKey, AccessDenied, RequestTimeTooSkewed). Catch it in your wrapper and translate to a domain exception so callers don't import SDK classes:

try { ... } catch (\Qcloud\Cos\Exception\ServiceResponseException $e) {
    if ($e->getStatusCode() === 404) { return null; }
    throw new StorageException($e->getErrorCode(), 0, $e);
}

Note headObject on a missing key throws rather than returning false — wrap it if you need an exists() helper.

Key encoding is on you. Keys with spaces, +, #, or non-ASCII must be URL-encoded consistently between the presigned URL you generate and the key you later read. Normalize/sanitize at write time (strip to [A-Za-z0-9/_.-]) and you'll never debug this.

Presigned URL expiry has a ceiling. +30 minutes style relative strings are supported, but don't expect arbitrary far-future expiry to be honored — signatures are time-boxed. Long-lived sharing should be a signed redirect through your app, not a 30-day URL.

Debugging: set 'debug' => true in the client config to get Guzzle's transfer dump (request line, headers, cURL verbose). Invaluable for the first integration; never leave it on in production — it logs the Authorization header. Note that debug output shape differs between the Guzzle 6 and Guzzle 7 builds, so old log-scraping regexes will break if you migrate a legacy app.

Bucket names are global and include your APPID. The name is yourname-1250000000, lowercase, 3–63 chars, no underscores. A locally-valid-looking name will fail at the global namespace level, and the error message won't mention the APPID requirement.

No composer.lock drama, but low bus factor. ~193 stars and essentially no Laravel ecosystem adoption means: no auto-discovery, no community facade, no Stack Overflow corpus. Release notes arrive in Chinese and, as with 2.6.17, often describe narrow CI features with no PHP usage example — so pin an explicit version, read the diff before upgrading, and budget time for the SDK source under vendor/qcloud/cos-sdk-v5/src/. It's readable, and the per-API option keys (e.g. StorageClass, CacheControl, Metadata, ContentDisposition, AIGCMetadata) are only documented there and in the COS REST docs.

Extension point: the client accepts a 'http' array passed straight to Guzzle. That's your hook for proxies, custom handlers, and mock injection in tests (MockHandler) — it's the cleanest way to unit-test code that touches COS without a live bucket.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style