Generate a PHP SDK from your OpenAPI spec

Octri reads your OpenAPI document and writes a PHP 8.2 client with typed models, releases it to Packagist so Composer can install it, and rebuilds it every time the spec changes.

Install from Packagist
composer require acme-api/sdk

The client they actually get

What your users write once the PHP package is installed.

Two call-site shapes come out of the same spec. class-namespaced is the default: a client object whose operations are grouped by resource, which is the surface every widely used SDK ships and the one your own reference docs already assume. functions is the override: no client object, one configure call, and each operation called directly. It is a single setting, per language, and nothing else about the client changes with it.

Parameters are PHP 8 named arguments, so callers skip the optional ones by name. list is a reserved word in PHP, so a plain list operation is generated as list_() beside its listPaginated companion.

Packagist indexes your repository and Composer installs from its tags, so a PHP release is a git tag that Packagist picks up through its webhook.

Client object (default)
<?php

use acme_api\Acme;
use acme_api\ClientAuthConfig;
use acme_api\ClientConfig;

$config = new ClientConfig();
$config->baseUrl = 'https://api.acme.com/v1';
$auth = new ClientAuthConfig();
$auth->bearer = getenv('ACME_TOKEN') ?: null;
$config->auth = $auth;

$acme = new Acme($config);

// listPaginated returns a lazy Generator: foreach pulls one page at a time.
foreach ($acme->users->listPaginated(limit: 100) as $user) {
    echo $user->id, PHP_EOL;
}

$invoice = $acme->invoices->create(customerId: 'cus_123', amountCents: 4900);
Free functions
<?php

use acme_api\SdkConfig;
use acme_api\Users;

SdkConfig::configure($config);

foreach (Users::listUsersPaginated(limit: 100) as $user) {
    echo $user->id, PHP_EOL;
}

The package at a glance

What a developer checks before adding the PHP package to a project.

Runs on
PHP 8.2+ with the curl extension
Transport
cURL
Runtime dependencies
None from Composer
Models
Classes with readonly typed properties
Pagination
Lazy Generator, consumed with foreach
Ships to
Packagist, installed with Composer

When a call fails

Each common status is its own exception class, so a catch block names the case it handles. A declared error model is decoded onto $e->data for an instanceof check.

Typed errors
<?php

use acme_api\SdkHttpError;
use acme_api\SdkRateLimitError;

try {
    $acme->invoices->create(customerId: 'cus_123', amountCents: 4900);
} catch (SdkRateLimitError $e) {
    // 429 after the built-in retries. Release the job back to the queue.
} catch (SdkHttpError $e) {
    error_log("acme failed: {$e->statusCode} request {$e->requestId}");
}

What you decide for PHP

PHP has no engine setting. The client talks cURL directly, so it adds no HTTP library to your users’ composer.lock.

  • Client style

    class-namespaced (default), class, namespaced, functions

    A client object with a property per resource, the new Acme($config) shape PHP developers expect. Functions become static methods on a class per resource.

  • Argument style

    positional (default), object

    Typed parameters that callers can pass by name, with optional ones defaulting to an unset marker so omitted and null stay distinct on the wire.

  • Doc comments

    full (default), minimal

    Full writes PHPDoc with the array shapes PHPStan and PhpStorm read.

Method naming and namespacing work the same in every language: operations are grouped by your spec’s tags or by path, and the namespace word is dropped from the method name unless you ask to keep the operationId whole.

Packagist reads from your repository

Packagist stores metadata only and fetches the code from your git repository and its tags. That makes the PHP repository a required setting: without it there is nothing for Packagist to read. Set it per language before the first release, and register the vendor name on Packagist early, since vendor names are first come, first served.

Packagist package
acme-api/sdk
  • Not possible: Release without a repository
  • Not possible: Upload code to Packagist directly
  • Still possible: Release by pushing a git tag

The same in every generated client

The parts nobody wants to hand-write, and the parts a hand-written client usually skips.

  • Retries with exponential backoff and jitter, on the statuses that mean "try again", with a per-attempt timeout.
  • Idempotency keys on the methods that need them, so a retried write does not become two.
  • Auth wired in from your spec’s security scheme, read from wherever your users keep secrets.
  • Typed errors split by cause: a failed constraint, an HTTP status, a network failure and a timeout are four different things.
  • Pagination that follows whichever contract your spec declares, cursor, offset, page number or next URL, without prefetching the collection.
  • Server-sent events as a native stream, for the operations that stream.
  • Error reporting to Octri’s monitoring, pre-wired and switched off until a consumer opts in.
  1. Point it at your spec

    Upload an OpenAPI or Swagger document, paste a URL, or connect the repository it lives in so a merge to main updates it.

  2. Read the audit

    The spec is scored out of 10 on what a generator can do with it, with every missing schema and colliding operationId named. 9 of the fourteen rules carry a button that writes the fix.

  3. Shape the client

    Client style, argument style, namespacing, auth, retries and pagination, set once for the project and overridden per language or per endpoint.

  4. Generate and read it

    The package is generated, compiled and handed to you as source you can read before anyone installs it.

  5. Publish, then keep publishing

    Release to the registry under your own account. Every later spec change regenerates the client, and the version number comes from your own document.

Questions

No. The client uses the curl extension directly and requires nothing from Composer at runtime, so it installs beside any Guzzle or Symfony HttpClient version your users already have.