obs/esdk-obs-php
PHP SDK for Huawei Cloud OBS (Object Storage Service). Provides client APIs for buckets, objects, multipart uploads, ACLs, lifecycle rules, and signed URLs, making it easy to integrate scalable cloud storage into Laravel or plain PHP applications. Includes examples and configuration support.
Install the official Huawei Cloud OBS SDK:
composer require obs/esdk-obs-php
Credentials come from the Huawei Cloud console (IAM → My Credentials → Access Keys), or preferably from environment variables / a secrets manager. The minimum viable client:
use Obs\ObsClient;
$client = new ObsClient([
'key' => config('filesystems.disks.obs.key'),
'secret' => config('filesystems.disks.obs.secret'),
'endpoint' => 'https://obs.cn-north-4.myhuaweicloud.com', // must match bucket region
]);
$client->putObject([
'Bucket' => 'my-bucket',
'Key' => 'avatars/user-1.png',
'Body' => file_get_contents($path),
]);
First use case to sanity-check: upload a file, then getObject it back and read ->getBody(). Second: generate a presigned GET URL with createSignedUrl and confirm it downloads in a browser. If those two work, credentials, endpoint, region, and bucket policy are all correct — which is 90% of setup failures.
Look first at the examples/ directory in the repo (it ships a full set of runnable samples) and the ObsClient class method list — it mirrors the OBS REST API 1:1 (putObject, getObject, listObjects, createBucket, deleteObject, copyObject, createSignedUrl, etc.).
Bind a singleton in a Laravel service provider. The client wraps a Guzzle handler and is safe to reuse across requests; constructing it per call wastes TLS handshakes.
$this->app->singleton(ObsClient::class, fn () => new ObsClient([
'key' => config('filesystems.disks.obs.key'),
'secret' => config('filesystems.disks.obs.secret'),
'endpoint' => config('filesystems.disks.obs.endpoint'),
]));
Wrap it in a small storage service, not raw calls. Controller code should call $storage->put($key, $contents, $mime) so you can swap OBS for S3 locally and centralize key generation, prefixing, and error translation.
Custom Laravel filesystem driver is the highest-leverage integration if you use Storage::disk('obs'), Spatie Media Library, or ->store() calls. There is no first-party adapter in this package; either write a League\Flysystem\Adapter implementation around putObject/getObject/deleteObject, or adopt a community filesystem bridge. Once wired, existing code treats OBS like any other disk.
Presigned URLs for private buckets. Keep buckets private and hand out short-lived URLs — this is the standard pattern for user uploads/downloads:
$url = $client->createSignedUrl([
'Method' => 'PUT', // or 'GET' for downloads
'Bucket' => 'my-bucket',
'Key' => "uploads/{$uuid}",
'Expires' => 900, // seconds
'Headers' => ['Content-Type' => 'image/png'],
]);
Stream large files, don't buffer them. For uploads over a few MB use Body as a stream resource (fopen($path, 'r')) so Guzzle chunks it, and for very large objects use the multipart helpers (initiateMultipartUpload / uploadPart / completeMultipartUpload) or the SDK's uploadFile convenience method so you get parallel part uploads and resumability.
Queue non-critical writes. Uploads, deletions, and thumbnailing are ideal job payloads. Store the OBS key on the model, dispatch a job, and use the queue's retry/backoff for transient 5xx or throttling responses.
Prefix-based organization. Use keys like {tenant}/{model}/{uuid}-{filename} — OBS is a flat keyspace, so prefixes are your "folders" and make listObjects with Prefix + MaxKeys + Marker cheap and predictable for cleanup jobs and audits.
obs.cn-north-4.myhuaweicloud.com will fail for a bucket in another region; use the region-specific endpoint, not the generic one.$resp['HttpStatusCode'], $resp['ObjectURL'], and $resp->getBody() are the accessors — the body is a Guzzle stream. Read once with (string) $resp['Body'] or getContents(), then close it.ObsException, not return false. Always wrap in try/catch and branch on $e->getExceptionCode() (e.g. NoSuchKey, AccessDenied, SignatureDoesNotMatch) rather than parsing messages.SignatureDoesNotMatch with correct keys almost always means server time is off — sync NTP on the app host.ACL => 'public-read' on the object (or bucket policy) or a CDN in front. Prefer CDN + private bucket for production.autoloader.php and use a non-PSR-4 layout. If classes aren't found, check composer.json's classmap/files autoload or require the SDK autoloader once in a bootstrap file. Composer 2's stricter PSR-4 warnings can be loud here.Content-Type isn't inferred reliably. Set ContentType explicitly (or via Mimetypes in the SDK) or browsers will download instead of render.listObjects is paginated. Loop on IsTruncated with NextMarker; a single call returns max 1000 keys. Deleting "everything under a prefix" needs the same loop.debug/log_path config options when signatures fail — the request/response dumps show the exact canonical request OBS rejected.ObsClient or inject a custom Guzzle handler stack to add retries, request IDs for tracing, or metrics middleware. The SDK exposes its underlying HTTP client config, which is where you add timeouts and connection pooling for high-throughput workloads.How can I help you explore Laravel packages today?