softlayer/objectstorage
PHP client for IBM SoftLayer Object Storage (OpenStack Swift). Provides a simple API for authentication, container management, object uploads/downloads, and metadata, making it easy to add Swift-based object storage to PHP and Laravel apps via Composer.
This is the official-but-dormant SoftLayer Object Storage client — a thin wrapper over the OpenStack Swift (v1) REST API, built on top of php-opencloud's ObjectStore resources. Before you write a line of feature code, confirm it is still the right tool for you: the repo has low adoption (29 stars, 0 dependent packages) and no recent maintenance. If you can use league/flysystem-aws-s3-v3 against a Swift/S3-compatible gateway, or the modern php-opencloud/openstack SDK, do that instead. If you are locked into a SoftLayer/IBM Cloud Object Storage Swift account and need the X-Auth-Token + X-Storage-Url flow it handles, read on.
Install and sanity-check compatibility
composer require softlayer/objectstorage
Expect dependency friction on modern PHP. The package predates PHP 8 and pulls an old Guzzle/php-opencloud stack, so you may need --ignore-platform-reqs, a version pin, or a vendored fork. Run composer why-not php 8.2 and inspect composer.json in the package before committing to it in composer.json.
Auth model — this is not IAM
SoftLayer Object Storage uses your SoftLayer username plus an API key to obtain a Swift auth token and storage endpoint. The two relevant entry points:
SoftLayer\Common\Token::getToken($username, $apiKey) — performs the auth handshake.SoftLayer\ObjectStorage\Client::getClient($token, $endpoint) — returns the storage client.use SoftLayer\Common\Token;
use SoftLayer\ObjectStorage\Client;
$token = Token::getToken(config('objectstorage.username'), config('objectstorage.api_key'));
$client = Client::getClient($token, config('objectstorage.endpoint'));
// endpoint example: https://syd01.objectstorage.softlayer.net/auth/v1.0/
Where to look first
src/SoftLayer/ObjectStorage/Client.php — the factory and the container-level methods (getContainers, getContainer, createContainer).src/SoftLayer/Common/Token.php — cache behavior and expiry handling.OpenStack\ObjectStore\v1\Resource\Container / StorageObject classes (installed via Composer) — these hold the actual object CRUD methods.First use case
The classic day-one win is moving user-generated uploads off the app server: create a container per environment (app-prod-uploads, app-staging-uploads), write uploaded files into it, and keep only the object key in your database. Start there before touching CDN or lifecycle features.
Bind the client as a singleton; don't build it per request call
Put credentials and endpoint in config/objectstorage.php fed by env vars, and register a singleton in a service provider so the token handshake happens at most once per process:
$this->app->singleton(\SoftLayer\ObjectStorage\Client::class, function () {
$token = \Cache::remember('sl_obj_token', 60 * 60 * 12, function () {
return \SoftLayer\Common\Token::getToken(
config('objectstorage.username'),
config('objectstorage.api_key')
);
});
return \SoftLayer\ObjectStorage\Client::getClient($token, config('objectstorage.endpoint'));
});
Inject Client into the services that need it (an ObjectStorageService, a filesystem adapter, a job), never call getClient() from controllers.
A storage service, not scattered calls
Wrap container/object operations in one class so the rest of the app speaks in your domain terms (putAvatar, avatarUrl, deleteExport) and the package-specific types stay in one file. This is also where you swap in a different backend later — the biggest risk with this package is lock-in.
Uploads: stream, don't buffer
StorageObject::setContent() accepts a stream or string. For anything a user can upload, pass a stream so you don't load a 50 MB video into memory:
$container = $client->getContainer('app-prod-uploads');
$object = $container->getObject("avatars/{$user->id}.png");
$object->setContent(fopen($request->file('avatar')->getRealPath(), 'r'));
$object->create(['contentType' => $request->file('avatar')->getMimeType()]);
Note the API shape: create the object on the container, set the content, set content type, then persist. Verify the exact call (create() vs save()) against the installed php-opencloud version — it varies.
Path prefixes instead of containers
Swift containers are flat; / in object names is a pseudo-directory. Keep one bucket per environment and namespace by prefix (users/42/avatar.png, exports/2024-06/...). Listing with a prefix and paginating by marker is cheaper than spinning up containers per feature, and it maps cleanly onto Eloquent if you store object_key plus container on a media table.
Queue the write path
Auth + upload latency makes synchronous uploads a bad fit for request/response. Dispatch a job after the user upload completes locally (or straight from the temp file), and store pending → stored status on the model. This also gives you a natural retry point when Swift returns transient errors.
Filesystem integration options
League\Flysystem\FilesystemAdapter) over Container/StorageObject and register it as a Laravel disk in config/filesystems.php. Then Storage::disk('softlayer')->put(...) works and the rest of the app is provider-agnostic.putPath/getPath/url/delete and no Flysystem dependency. Given the package's age, the smaller the surface area, the better.Serving files
Swift containers can be public, or you proxy through a route (authorize, get the object stream, stream back with the right headers). Public containers give you CDN-friendly URLs; private containers plus signed/proxy routes keep access control in Laravel. Decide up front — retrofitting access checks onto public URLs is painful.
Testing
Bind a fake against the same interface your service exposes. For real integration tests, run a local Swift container (docker run an OpenStack Swift dev image) and point the endpoint at it. That's more reliable than mocking php-opencloud's object graph.
Token expiry is your first 401
Auth tokens are short-lived (historically ~24 h). If you cache the token as shown above, make sure the TTL is under the expiry and that you refresh on a 401 — a cached expired token produces confusing 401s that look like credential failures. Also, the auth endpoint and storage endpoint are different hosts; passing the wrong one yields 401 or 404.
Abandoned dependency = platform risk
Assume no upstream fixes. Pin the version, and vendor/fork if you must patch it. Check composer.json PHP and Guzzle constraints early — this is where most "can't install" issues come from. The NOASSERTION license identifier means the license wasn't machine-detectable; confirm licensing before shipping commercially.
Container deletion requires empty
deleteContainer fails on a non-empty container. Delete objects first (with pagination — listing returns pages), then the container. Same ordering matters for teardown scripts and test cleanup.
Metadata and content type defaults
Swift stores custom metadata as X-Object-Meta-* headers. The wrapper handles the prefixing, but keys are case-sensitive and hyphens are constrained. Content-Type defaults to application/octet-stream if not set — always set it explicitly or browsers will download PNGs instead of rendering them.
Errors are Guzzle exceptions
Wrap calls and distinguish 404 (missing object) from 401/403 (auth) from 5xx (transient, retry). Don't let raw Guzzle exceptions bubble into controllers; map them to your domain exceptions in the service layer.
Large objects
Single-request uploads have size limits and can time out on slow links. Swift supports segmented uploads (manifest + parts), but this client's support is thin — for very large files, either accept the limit or drop to direct Swift API calls for the segment flow. Set sane Guzzle timeouts either way.
Config quirks worth checking
Extension points
The clean seams are: your own storage service interface (swap providers), a custom Flysystem adapter (swap filesystem semantics), and a token provider you control (swap auth). Nothing in the package is designed for extension, so build these wrappers rather than subclassing Client.
How can I help you explore Laravel packages today?