php-opencloud/openstack
PHP OpenStack SDK lets PHP developers connect to OpenStack APIs in a simple, idiomatic way. It supports multiple services and versions, installs via Composer, and requires PHP 7.2.5+ with ext-curl. Official docs and integration tests included.
Install and pin.
composer require php-opencloud/openstack:^3.16
Requires PHP >= 7.2.5 and ext-curl. Note v3.17.0 tightened Guzzle/PSR-7 constraints — resolve dependencies before writing code, not after.
Where to look first. In order:
samples/ — every sample is runnable and doubles as an integration-test fixture. They are the fastest way to learn the real option-array shapes (createServer, createVolume, createLargeObject), which the Sphinx docs often only sketch.doc/ (readthedocs) for service coverage and authentication variants.COVERAGE.md to confirm the exact services/microversions your cloud exposes are supported.src/OpenStack.php — the single entry point; everything else hangs off it.Minimal wiring. The client takes a plain options array, so put it in config/openstack.php and never inline credentials:
// config/openstack.php
return [
'authUrl' => env('OS_AUTH_URL'),
'region' => env('OS_REGION', 'RegionOne'),
'user' => [
'name' => env('OS_USERNAME'),
'password' => env('OS_PASSWORD'),
'domain' => ['id' => 'default'],
],
'scope' => ['project' => ['id' => env('OS_PROJECT_ID')]],
];
// AppServiceProvider::register()
$this->app->singleton(\OpenStack\OpenStack::class, fn () =>
new \OpenStack\OpenStack(config('openstack')));
First use case (read-only, one afternoon). Authenticate and enumerate compute servers into a dashboard or cache table:
$compute = app(\OpenStack\OpenStack::class)->computeV2();
foreach ($compute->listServers() as $server) {
// $server->id, $server->name, $server->status, $server->flavor
}
This validates the whole chain — Keystone auth, endpoint catalog, region, project scope — with zero risk of provisioning anything. Move to writes (create server, upload object, attach volume) only after that loop is green against the real cloud.
Wrap, bind, don't leak. Treat the SDK as an infrastructure adapter. Define App\Cloud\ComputeContract, ObjectStoreContract, etc., implement them with SDK calls, and bind via the container. Controllers, jobs, and Blade views should see plain arrays or your own DTOs — never OpenStack\Compute\v2\Models\Server.
One adapter per service, one client per scope. Multi-tenant apps need a client per (project, region) pair. Build a small OpenStackClientFactory that resolves by tenant key and memoizes:
$client = $this->factory->forProject($projectId, $region); // returns OpenStack
$server = $client->computeV2()->getServer(['id' => $id]);
Application credentials (v3.5.0+) are preferable to user/password in deployed services — no long-lived user secret on disk, and revocation is scoped.
Lists are generators — actually iterate them. listServers(), listObjects(), etc. return Generators that lazily page. This is a feature: use foreach to stream straight into chunked DB inserts or a cursor. It bites when someone does count($compute->listServers()) on a 5,000-server cloud.
Long operations belong in queued jobs. Server build, volume attach/detach, snapshots, image creation, and large-object uploads are minutes-long. Pattern:
// Job per resource, keyed by the resource ID for idempotency
$server = $client->computeV2()->createServer([
'name' => $name,
'flavor' => ['id' => $flavorId],
'image' => ['id' => $imageId],
'networks' => [['uuid' => $networkId]],
]);
$server->waitUntil('ACTIVE', 600); // waitUntil ships via HasWaiterTrait
If the job dies mid-wait, the server may still be building. Your job's retry/reconcile path must re-read by ID and resume polling rather than re-create. Idempotency is entirely your responsibility; the SDK provides none.
Pass PSR-7 streams for object storage. Never file_get_contents a 2 GB file:
$container = $objectStore->getContainer($name);
$container->createObject([
'name' => $path,
'stream' => \GuzzleHttp\Psr7\Utils::streamFor(fopen($tmp, 'r')),
]);
For files beyond the segment threshold use createLargeObject() and set segmentIndexFormat (v3.3.0+) — the default format silently breaks past 10 segments.
Inject your own Guzzle stack. The client accepts a configured httpClient/handler stack. Use it for request-ID logging (X-OpenStack-Request-ID), retry middleware, and proxy/timeout settings:
$stack->push(Middleware::retry($decider, $delay)); // 429/503 on GET only
new \OpenStack\OpenStack($options + ['httpClient' => new Client(['handler' => $stack])]);
This is also your test seam: a MockHandler with recorded OpenStack JSON responses gives you fast, cloud-free contract tests.
Cache tokens deliberately. Keystone tokens default to ~1 hour. Cache the token per project and reuse it via the identity service; Token::validate() (v3.6.0+) lets you check validity before making a call. For long-running workers, prefer short-lived clients or re-auth in middleware over holding a client for hours.
Map errors once. Every failure surfaces as OpenStack\Common\Error\BadResponseError (with an improving error builder as of v3.9.0). Catch it at the adapter boundary and translate into domain exceptions (QuotaExceeded, NotFound, Conflict), inspecting the status code and response body once, centrally.
Object no longer exists. Class OpenStack\ObjectStore\v1\Models\Object was renamed StorageObject in 3.0 because object became reserved in PHP 7.2. Every pre-2020 tutorial and StackOverflow answer imports the old name.
Don't serialize SDK models onto queues. Models hold a reference to the HTTP client and hydrated state; passing one as a job payload will fail or deserialize into a useless husk. Queue scalar IDs (serverId, containerName) and re-retrieve inside the job.
Guzzle version drift is the top install risk. Laravel apps pin Guzzle, and this SDK has historically fought it — uri-template compatibility (fixed 3.2.2), promise API changes (3.2.3), and patched Guzzle/PSR-7 requirements (3.17.0). Run composer why guzzlehttp/guzzle and a full test suite before merging the dependency. PHP 8.4 support only landed in 3.11.0.
Endpoint catalog failures happen at client construction, not at call time. If Neutron isn't in the catalog for that region, ->networkingV2() blows up before your try/catch around the actual operation. Wrap service construction too, and surface "service unavailable in region" as a distinct error.
Waiter timeouts are short by default. waitUntil('ACTIVE') without an explicit timeout can give up long before a real build finishes. Always pass seconds, and treat waiter timeout as "unknown, keep polling" rather than "failed."
Don't retry non-idempotent calls. There is no built-in retry policy, and adding a blanket Guzzle retry will happily create three servers. Retry GET/list, and 429/503 only, with jitter.
Known-fixed bugs worth checking your version for: Swift container names containing tokens (fixed 3.7.1), sort_key/sort_dir for BlockStorage v2 snapshots and Images v2 (fixed 3.3.1), Swift metadata headers being case-sensitive (fixed 3.10.0), and header merging in OperatorTrait::sendRequest (3.14.0). If you're pinned below these, work around them explicitly.
Debugging workflow. Reproduce in a standalone samples/-style script with a verbose Guzzle logger before blaming Laravel. Capture the X-OpenStack-Request-ID from the failing response — it's the only thing your cloud operator's support will accept. Use errorVerbosity (3.8.0+) to keep raw OpenStack error bodies out of user-facing responses while retaining them in logs.
Extension points. If an operation is missing, follow the repo's own layering: add the REST call to the service Api.php, params to Params.php, expose it from Service.php, and add model behaviour in Models/*; $aliases/Alias handle field renames. Subclass OpenStack or a versioned Service and register it, rather than patching vendor code.
Assume you own the lifecycle. Orphan cleanup, compensation on partial failure, quota pre-checks, and reconciliation are not in the SDK. Budget for a ReconcileResources scheduled command from day one. And keep the SDK behind your interfaces: 234 stars with a narrow maintainer base means an exit path is a design requirement, not paranoia.
How can I help you explore Laravel packages today?