mittwald/api-client
PHP client for the mittwald API, generated from its OpenAPI specification. It offers typed request and response models plus PSR-18 HTTP integration, making it easy to automate mittwald hosting resources such as projects, apps, domains, and databases from PHP or Laravel.
This is a generated PHP SDK for the mittwald mStudio v2 API — think "manage hosting projects, apps, domains, databases, and cronjobs programmatically." It is a thin transport layer over REST, so the fastest path to productivity is to read the generated resource classes rather than the README.
composer require mittwald/api-client guzzlehttp/guzzle
projectId:
use Mittwald\ApiClient\Client;
$client = new Client($token); // or new Client($token, $psr18Client)
$projects = $client->project()->listProjects();
Resource accessors are grouped by domain (project(), app(), domain(), database(), container(), user(), ...). If a method name is not obvious, open src/ and grep the endpoint path — the method names mirror the API docs closely.new Client() in a controller again.First real use case: enumerate projects, then list the apps/installations inside one. That tells you whether the token's scopes and the ID shapes match your expectations before you build anything on top.
Bind it as a scoped singleton with a configured PSR-18 client. Never let the SDK construct its own transport — you lose timeouts, retries, and observability.
// config/services.php
'mittwald' => [
'token' => env('MITTWALD_API_TOKEN'),
'base_uri' => env('MITTWALD_API_BASE_URI'), // for staging / mocking
'timeout' => env('MITTWALD_TIMEOUT', 15),
],
// AppServiceProvider::register()
$this->app->singleton(Client::class, function ($app) {
$http = new \GuzzleHttp\Client([
'timeout' => config('services.mittwald.timeout'),
'connect_timeout' => 5,
'headers' => ['User-Agent' => 'my-app/1.0'],
]);
return new Client(config('services.mittwald.token'), $http);
});
Wrap it in your own gateway, not in controllers. The generated API surface is wide and will churn; a single MittwaldGateway class isolates your domain vocabulary (provisionSite, attachDomain) from generated method names. This is the cheapest insurance against a minor version bump.
Treat every list endpoint as paginated. Generated clients typically expose limit/offset (or a cursor) as parameters on the request object. Write one helper and reuse it:
function all(callable $fetch, int $pageSize = 100): \Generator {
$offset = 0;
do {
$batch = $fetch(['limit' => $pageSize, 'offset' => $offset]);
yield from $batch;
$offset += $pageSize;
} while (count($batch) === $pageSize);
}
Model long-running operations as jobs. Provisioning, app installs, and domain changes are frequently asynchronous — you get an operation/installation ID back, not a finished resource. Poll with exponential backoff in a queued job, persist the operation ID, and make the job idempotent so retries don't double-provision:
class AwaitInstallation implements ShouldQueue {
public $tries = 10;
public $backoff = [5, 10, 30, 60];
public function handle(MittwaldGateway $mw) {
$op = $mw->getOperation($this->operationId);
if ($op->isFinished()) { /* update local model */ }
else { $this->release(15); }
}
}
Route bulk work through the queue. The API is rate-limited per token; a foreach over 200 projects will get you 429s. Chunk with Bus::batch() and keep concurrency low (2–3) so your polling doesn't compete with user-triggered requests.
Test with a fake PSR-18 client, not by mocking the SDK. Because the transport is injected, you can drop in Http\Mock\Client and queue canned JSON responses. This exercises the real (de)serialization — which is where generated clients actually break.
$mock = new \Http\Mock\Client();
$mock->addResponse(new \GuzzleHttp\Psr7\Response(200, [], json_encode(['items' => []])));
$client = new Client('token', $mock);
Webhooks (if you use them) replace polling. For "app installed / domain verified" events, prefer the webhook to the polling loop — but verify the signature and return 2xx fast, then dispatch a job. Keep polling as a fallback reconciler, because webhook delivery is at-least-once, not exactly-once.
"mittwald/api-client": "1.2.3", not ^1.0) and read the CHANGELOG before upgrading. Method signatures, constructor order, and DTO property names are the likely breaking points.new Client($token) or new Client($token, $httpClient) or requires a config object — check src/Client.php first. Everything downstream depends on this.composer require mittwald/api-client may not pull a transport; the failure mode is a confusing "no HTTP client found" error at runtime, not at install time.connect_timeout and timeout, and set a lower timeout for polling loops.403 usually means the token lacks a scope, while a 404 often means "exists but you can't see it" — don't treat 404 as "safe to create." Check for existence with a list call scoped to a project you know you can read.status, request ID (usually in a response header like X-Request-Id), and body, then rethrow a domain exception. That request ID is what mittwald support will ask for.int, don't string-compare for ordering, don't build URLs by hand — always go through the client so base URI and version prefixes stay correct."9.99" not 9.99, ISO-8601 timestamps not DateTime. Convert at your boundary, once.readonly properties.Retry-After if present, otherwise exponential backoff with jitter. A naive retry storm on a shared token will get the whole integration throttled.HandlerStack middleware on the injected Guzzle client — not a decorator around the SDK. That keeps the SDK unmodified and upgradeable.Authorization in your logging middleware.MITTWALD_API_BASE_URI at a local stub (WireMock, or a Laravel Http::fake()-backed listener) for integration tests — far more reliable than mocking dozens of generated methods.How can I help you explore Laravel packages today?