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

Qcloud Cos Client Laravel Package

overtrue/qcloud-cos-client

A PHP client for Tencent Cloud Object Storage (COS) that offers a clean API for uploading, downloading, and managing files and buckets. Built on Guzzle with optional Laravel integration for quick cloud storage setup.

Deep Wiki
Context7

Getting Started

Install and wire up the client

composer require overtrue/qcloud-cos-client

The package is a thin, framework-agnostic wrapper around Tencent Cloud COS's HTTP API. You construct a Client with a config array (region, credentials, bucket) and call object-level methods on it.

use Overtrue\CosClient\Client;

$client = new Client([
    'region'     => env('COS_REGION', 'ap-guangzhou'),
    'app_id'     => env('COS_APP_ID'),
    'secret_id'  => env('COS_SECRET_ID'),
    'secret_key' => env('COS_SECRET_KEY'),
    'bucket'     => env('COS_BUCKET'), // name only; app_id is appended
]);

$client->putObject('avatars/42.png', file_get_contents($path), [
    'ContentType' => 'image/png',
]);

$url = $client->getObjectUrl('avatars/42.png');

Where to look first

  1. README.md — the API surface is small, but examples are the primary documentation. Treat them as the spec.
  2. src/Client.php (or equivalent) — this is where the real contract lives. Read the method signatures before designing around them; there is no formal API docs site.
  3. src/Exceptions/ — knowing which exception types map to 403/404/5xx will shape your error handling.
  4. If you're on Laravel, look at overtrue/laravel-filesystem-cos first — it consumes this client and gives you a Storage::disk('cos') driver, which is what most day-to-day code should actually use.

First use case to validate the integration

Upload one object, fetch it back, and generate a URL — in a tinker session or a throwaway route, before writing any abstraction:

$client->putObject('healthcheck/ping.txt', 'pong');
$body = (string) $client->getObject('healthcheck/ping.txt')->getBody();
// expect: pong

If that round-trip works, credentials, region, bucket naming, and signing are all correct. Everything else is detail.

Implementation Patterns

Register it as a singleton, not a per-call new

The client holds credentials and (depending on version) an underlying HTTP client. Build it once in a service provider and inject it:

$this->app->singleton(Client::class, fn () => new Client(config('services.cos')));

Then resolve via the container. Avoid constructing it in loops or jobs — you lose connection reuse and make config changes non-cacheable.

Prefer the Laravel filesystem adapter for CRUD, drop to the raw client for COS-specifics

For day-to-day storage work (put, get, exists, delete, url, temporaryUrl), the filesystem adapter is the ergonomic path and keeps you portable. Reach for overtrue/qcloud-cos-client directly when you need COS-only features: object ACLs, custom headers, copy/move semantics, multipart control, or lifecycle-adjacent operations the adapter doesn't surface.

// Everyday path
Storage::disk('cos')->put('reports/2024/q1.pdf', $pdf);

// Escape hatch for COS-specific behaviour
app(Client::class)->putObject('reports/2024/q1.pdf', $pdf, [
    'ACL' => 'public-read',
    'ContentType' => 'application/pdf',
    'ContentDisposition' => 'attachment; filename="q1.pdf"',
]);

Centralize key construction

Object keys are just strings, so tenant/environment prefixes get ad-hoc and then collide. Build them:

final class ObjectKey
{
    public static function for(string $area, string $path): string
    {
        return trim(sprintf('%s/%s/%s', config('services.cos.env'), $area, ltrim($path, '/')), '/');
    }
}

One rule: no leading slash, no .., no double slashes. COS treats the key literally, and a stray leading / creates a file whose name begins with a slash that your listing code will then have to special-case.

Separate "upload" from "serve"

The common production shape is: upload synchronously (small files) or via a queued job (large files), store the key in your database — never the URL — and generate URLs at render time. URLs embed region/domain and, for private buckets, expire; persisting them guarantees stale links.

// Store the key
$media->update(['path' => $key]);

// Derive the URL on read
$url = Storage::disk('cos')->url($media->path);
// or, for private buckets with a TTL:
$url = $client->getObjectUrl($media->path, now()->addMinutes(30));

Stream downloads rather than buffering

getObject returns a PSR-7 response. For file downloads, hand the body stream to the framework's response instead of (string)-casting it:

$response = $client->getObject($key);

return response()->streamDownload(
    fn () => fpassthru($response->getBody()->detach()),
    basename($key)
);

Casting to string is fine for small text, but a 200 MB PDF will exhaust memory_limit.

Wrap the client behind your own interface

Given the package's size (45 stars, no linked repository metadata, a single maintainer), the cheapest insurance is a narrow port:

interface ObjectStore
{
    public function put(string $key, $contents, array $options = []): void;
    public function get(string $key): string;
    public function delete(string $key): void;
    public function url(string $key, ?\DateTimeInterface $expires = null): string;
}

Back it with overtrue/qcloud-cos-client today. If the package stalls, you swap one adapter class instead of grepping for Client across the codebase. It also makes tests trivial: an in-memory implementation replaces COS entirely.

Test against a fake, smoke-test against a real bucket

Don't hit COS in the unit suite. Use the ObjectStore fake for behaviour, and keep a single opt-in integration test (env-gated credentials) that runs the upload/download round-trip above in CI nightly. COS has no local emulator as convenient as MinIO-for-S3, so the real-bucket smoke test is doing real work.

Handle errors at the boundary

COS returns errors as HTTP status + XML body (NoSuchKey, AccessDenied, SignatureDoesNotMatch, RequestTimeTooSkewed). Wrap calls in a job-level try/catch and translate to your domain's vocabulary:

try {
    $client->getObject($key);
} catch (\Throwable $e) {
    if (str_contains($e->getMessage(), 'NoSuchKey')) {
        throw new MediaMissing($key);
    }
    throw $e;
}

Retry on 5xx and connection timeouts; do not blindly retry 4xx — signature and permission errors are deterministic and will burn time.

Gotchas and Tips

Bucket names and regions must match exactly. COS bucket names carry the APPID suffix (my-bucket-1250000000). Depending on how the client assembles the host, passing the full name where it expects the bare name (or vice versa) yields confusing 403s rather than "bucket not found". Cross-check bucket, app_id, and region against the console — a bucket in ap-shanghai addressed as ap-guangzhou fails with a signature/host error, not a helpful message.

Signatures are clock-sensitive. RequestTimeTooSkewed / SignatureDoesNotMatch on a server whose clock has drifted is a classic 3 a.m. failure. Ensure NTP is running in containers and on CI runners. This is not a package bug and no amount of retrying fixes it.

Content-Type defaults are silently wrong. If you don't pass one, objects may be stored as application/octet-stream, so images download instead of rendering inline and browsers behave oddly. Set ContentType explicitly, or detect with mime_content_type()/Flysystem's detector before upload. Same for ContentDisposition if filenames matter to users.

ACL is not inherited the way you expect. Private is the safe default; a plain getObjectUrl() on a private bucket returns a URL that 403s. Either set ACL at upload time or use a signed URL with an expiry — and remember signed URLs leak if logged, so keep them out of access logs and error trackers.

PSR-7 streams are single-use. Once you've read or detached the body, it's gone. If you need both a hash and a passthrough, tee the stream or hash the source before uploading. Casting a large body to string twice is a memory bug waiting to happen.

Large uploads need multipart, and strings are not free. Passing a 500 MB string to putObject puts the whole thing in PHP memory. For anything sizeable, upload from a file path/resource if the client supports it, or split via multipart APIs. Set a queue + timeout budget for these jobs; default HTTP timeouts are often too short for slow uplinks.

Trailing-slash and "directory" semantics are fake. COS has flat keys. putObject('a/b/', '') creates a zero-byte object named a/b/, not a folder. Listing behaviour around delimiters differs from a filesystem — don't build logic that assumes directories exist.

Laravel config caching bites. env() calls inside the client config only work if you resolve config from config/services.php at boot. After php artisan config:cache, ad-hoc env() calls elsewhere return null and you get inexplicable auth failures locally-vs-production.

Pin the version and read the changelog before upgrading. The package has a small user base and no linked upstream repository in the metadata available here, so treat minor releases as potentially breaking. Also note the metadata itself is inconsistent — the listed "last release" date (2026-10-20) is in the future relative to any sane build, which means the release data you're looking at is unreliable. Verify against Packagist and your composer.lock rather than trusting the badge.

Extension points are limited, so isolate early. There's no plugin/middleware ecosystem to lean on. If you need retries, metrics, request logging, or custom HTTP behaviour, implement it in your ObjectStore wrapper or by injecting a configured HTTP client (if the installed version accepts one). Doing this on day one is far cheaper than retrofitting after fifty call sites exist.

Debugging 403s, in order: (1) clock skew, (2) region/bucket mismatch, (3) secret_id vs secret_key swapped or truncated by an env var with special characters, (4) key with a leading slash or wrong case, (5) ACL/signed-URL expiry. Log the request key and the COS error code — the XML body names the problem precisely even when the exception message doesn't.

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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle