aliyuncs/oss-sdk-php
Alibaba Cloud OSS SDK for PHP lets you upload, download, and manage any files on Alibaba Cloud Object Storage from PHP 5.3+ applications via Composer, requiring the cURL extension.
Install and confirm your runtime first. composer require aliyuncs/oss-sdk-php (current line is 2.7.x). The package needs PHP with the cURL extension; the 2.7.3 release is the one that carries PHP 8.5 compatibility fixes, so pin "aliyuncs/oss-sdk-php": "^2.7.3" rather than a loose ~2.0 if you're on modern PHP.
Where to look first, in order:
README.md — the "Common classes" table and "Handle returned results" section. There are exactly two classes you care about day to day: OSS\OssClient and OSS\Core\OssException. Everything else is OSS\Model\* response DTOs (BucketListInfo, ObjectListInfo, Headers, etc.).samples/ — a runnable catalogue of every API. It's the fastest way to learn the shape of an endpoint you've never called (samples/Config.php + php RunAll.php). Treat it as the real documentation; the prose docs upstream are thin and mostly Chinese.CHANGELOG.md — the only reliable way to know whether an operation exists in the version you have. Lots of important behaviour (strictObjectName, filePathCompatible, path-style, signature v4, credentials providers) landed in 2.6.0/2.7.0 and silently changes how object names and URLs are built.First use case worth wiring up: private bucket + signed URL, because it's the one that changes your architecture immediately.
// config/services.php
'oss' => [
'key' => env('OSS_ACCESS_KEY_ID'),
'secret' => env('OSS_ACCESS_KEY_SECRET'),
'endpoint' => env('OSS_ENDPOINT'), // oss-cn-hangzhou.aliyuncs.com — no bucket, no path
'bucket' => env('OSS_BUCKET'),
],
// App\Providers\OssServiceProvider::register()
$this->app->singleton(OssClient::class, function ($app) {
$c = $app['config']['services.oss'];
$client = new OssClient($c['key'], $c['secret'], $c['endpoint']);
$client->setConnectTimeout(10);
$client->setTimeout(120);
return $client;
});
$client->putObject($bucket, "avatars/{$user->id}.png", $binary, [
OssClient::OSS_HEADERS => [
OssClient::OSS_CONTENT_TYPE => 'image/png',
OssClient::OSS_CACHE_CONTROL_HEADER => 'public, max-age=31536000',
],
]);
$url = $client->signUrl($bucket, "avatars/{$user->id}.png", 3600);
That's a working upload plus a time-limited read URL without ever flipping the bucket to public-read. Validate the credentials path immediately — a wrong endpoint or region produces SignatureDoesNotMatch or a cURL failure, not a helpful message.
Wrap the client; don't call it from controllers. OssClient is a wide, untyped API that throws one generic exception. Introduce a thin ObjectStorage interface (put, putFile, temporaryUrl, exists, delete, paginate) and bind the OSS implementation to it. This gives you a fake for tests, a migration seam to the V2 SDK or S3, and one place to add logging/retry. Register OssClient as a singleton — it's stateless and cheap to reuse, but constructing it per request re-parses config and re-validates the endpoint.
Pick the upload method by size, not by habit. These are different code paths in the SDK, not syntactic sugar:
| Scenario | Call |
|---|---|
| Small string/blob (< a few MB) | putObject($bucket, $key, $content, $options) |
| Local file of any size | putObjectFile() / uploadFile() |
| Large file, resumable, parallel | multipartUploadFile() (5 MB min part, 10 000 part max) |
| Append-only log/stream | appendObject() with the returned nextPosition |
| Open handle / stream | putObject with a stream resource (supported since 2.4.0) |
Above ~100 MB, always go multipart — a single-PUT timeout leaves you with no resume point.
Stream downloads instead of buffering. getObject() returns the body in memory. For anything user-downloadable, use getObjectToLocalFile($bucket, $key, $tmpPath) (or the OSS_FILE_DOWNLOAD option) and then response()->download($tmpPath, $name)->deleteFileAfterSend(true). Avoids the classic PHP memory-exhaustion 500 on a 200 MB PDF.
Private-first, signed URLs for delivery. signUrl($bucket, $key, $expiresSeconds, $options) is your default read path. Since 2.7.2 you can steer the response with response-content-disposition / response-content-type options, which is how you force a download filename without a second copy of the object. For high-volume delivery, front the bucket with CDN and sign against the CNAME endpoint.
Use listObjectsV2() for listings, with prefix + delimiter for folder UIs. Results cap at 1 000 keys; loop on the continuation token. There is no built-in paginator/iterator in V1 — that's a real ergonomic gap and the main reason to consider V2 for greenfield listing-heavy work. Never enumerate a whole bucket to find one file; use doesObjectExist() or getSimplifiedObjectMeta().
Let the queue own the transfer. Uploading in a request cycle means PHP max_execution_time becomes your effective size limit. Dispatch a job with the temp path (not the file contents), upload inside the job, and store the object key on the model so the object key is deterministic and the job is idempotent — putObject overwrites, so retries are safe (multipart completeMultipartUpload is not; guard it).
Exploit server-side processing instead of building thumbnails. The image service (x-oss-process=image/resize,w_200) lets you generate derivatives at read time and cache them at the CDN. If you're generating thumbnails on upload today, this is often an outright deletion of a queue worker. processObject() exposes the same capability from PHP.
Consider Flysystem for the 80% case. If your team lives in Storage::disk('oss')->put(...), a Flysystem adapter over this SDK gives you the Laravel-native API and you drop to the raw client only for signing, multipart, and OSS-specific features. The trade-off is one more unmaintained-in-practice dependency; for a small surface area the wrapper interface above is often better.
One exception type, many meanings. Everything throws OSS\Core\OssException. Branch on getErrorCode() (NoSuchKey, AccessDenied, SignatureDoesNotMatch, RequestTimeTooSkewed, EntityTooLarge) and always log getRequestId() — Alibaba support will ask for it and it's unrecoverable after the fact. RequestTimeTooSkewed is almost always clock drift on your app server, not a config error.
There is no automatic retry. This is the single biggest difference from the V2 SDK, which advertises built-in retries and a transfer manager. In V1 you own retry policy: wrap idempotent reads/puts in a bounded backoff, and be careful with appendObject and multipart completion, which are not safely repeatable. Laravel's job $tries / backoff() covers most of this if transfers live in jobs.
Multipart uploads leak money. If a job dies between initiateMultipartUpload and completeMultipartUpload, the parts sit in the bucket and are billed. Add a lifecycle rule with AbortIncompleteMultipartUpload on every bucket you write to — this is a config task, not a code task, and it's routinely skipped.
Endpoint and bucket rules. The endpoint must be the region endpoint only (oss-cn-hangzhou.aliyuncs.com), never the bucket hostname, unless you intentionally pass the CNAME flag to the constructor. A bucket whose region doesn't match the endpoint produces signature errors that look like credential problems. Passing a full https:// URL where the SDK expects a host is a common source of silent isCName misdetection.
Set timeouts explicitly for uploads. Defaults are tuned for small API calls. setConnectTimeout(10) and setTimeout(120) (higher for large single PUTs) prevents both hung workers and spurious failures on slow links.
Content-Type is your responsibility, and it matters twice. Omit it and OSS stores application/octet-stream — browsers download instead of rendering, and CDNs won't compress. Combine with Cache-Control on immutable assets. Also note Expect: 100-continue is disabled since 2.4.0; if you're behind an aggressive proxy, that's already handled.
Don't trust ETag as an MD5. OSS_CHECK_MD5 (put/append) is useful integrity checking, but ETags are not plain MD5s for multipart objects or SSE-KMS-encrypted objects. Comparing an ETag to md5_file() will produce false alarms exactly on the large uploads where you most want verification.
Object naming is where the 2.7.0 options bite. strictObjectName, filePathCompatible, and path-style addressing change how keys are encoded and how URLs are constructed. If you're storing user-supplied filenames, normalise them yourself (strip leading slashes, reject .., decide on URL-encoding) rather than relying on SDK defaults; the '0'-as-object-name bug that existed pre-2.4.0 is a good reminder that key handling has historically been fragile.
Credentials: the expiry trap. STS/ram-role credentials need the securityToken constructor argument (or a credentials provider). In a long-running queue worker, cached tokens can expire mid-process — resolve credentials per job, not per worker boot. Environment-variable credentials providers exist since 2.7.0 and are the cleanest fit for container deployments.
Signature version matters for newer regions/buckets. Signature v4 support arrived in 2.7.0; some newer OSS deployments require it. If you see persistent signing failures with correct keys, check this before debugging anything else. forcePathStyle (2.7.2) is the escape hatch for S3-compatible/proxy setups.
New projects should at least evaluate the V2 SDK. Upstream itself flags alibabacloud-oss-php-sdk-v2 as the rewrite: retries, paginators, transfer managers, better error handling. V1's advantage is a decade of production use and a very stable API. Pick deliberately rather than by default — but if you pick V1, budget for the retry/pagination code you'll write yourself.
Test against a real bucket, not mocks. The SDK's value is in signing and XML parsing, which mocks can't validate. Set OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET / OSS_ENDPOINT / OSS_BUCKET in CI (the same variables the package's own test suite uses), point at a throwaway bucket, and keep your wrapper interface as the seam for unit tests that shouldn't touch the network.
How can I help you explore Laravel packages today?