rooberthh / php-sdk
A framework for building SDKs in PHP.
Requires
- php: ^8.3
- ext-fileinfo: *
- ext-json: *
- cuyz/valinor: ^2.6
- nyholm/psr7: ^1.8.1
- php-http/client-common: ^2.7.1
- php-http/discovery: ^1.19.2
- psr/http-client: ^1.0.3
- psr/http-client-implementation: *
- psr/http-factory-implementation: *
- psr/http-message: ^2.0
- rooberthh/php-http-tools: ^0.1.1
- symfony/http-client: ^7.2
Requires (Dev)
- laravel/pint: ^1.18
- pestphp/pest: ^3.5
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.0
Suggests
None
Provides
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-08-28 15:04:43 UTC
README
A flexible PHP SDK framework that helps you quickly build API client libraries with built-in authentication, resource management, and automatic data object mapping.
Requirements
- PHP 8.3 or higher
- Composer
- A PSR-18 HTTP client and PSR-17 factories (any implementation; discovered automatically)
Dependencies
php-http/client-common- the plugin chain requests pass throughphp-http/discovery- finds your installed PSR-18 client and PSR-17 factoriescuyz/valinor- maps API responses onto data objectsrooberthh/http-tools- HTTP utilities
Quick Start
Installation
composer require rooberthh/php-sdk
Generating Your SDK
Using the CLI Generator
The fastest way to create your SDK is using the included CLI generator that you can run from your project root.
./vendor/bin/php-sdk-generate
Example Generated Client
<?php namespace MyApi; use Http\Message\Authentication; use Http\Message\Authentication\Bearer; use Rooberthh\Sdk\Client; use Rooberthh\Sdk\Contracts\AuthenticationContract; use Rooberthh\Sdk\Http\Builder; use MyApi\Resources\UserResource; class MyApiClient extends Client implements AuthenticationContract { public function __construct( string $url, private readonly string $token, ?Builder $builder = null, ) { parent::__construct($url, $builder); } public function users(): UserResource { return new UserResource($this); } public function authentication(): Authentication { return new Bearer($this->token); } }
Credentials are promoted constructor properties on purpose. authentication()
is called from parent::__construct(), and promoted properties are assigned
before the parent constructor body runs — assigning them afterwards fails with
"must not be accessed before initialization".
Using Your Generated SDK
$client = new MyApi\MyApiClient('https://api.example.com'); // Use the resource methods $user = $client->users()->show(1); echo $user->name;
The client assembles its own transport from whatever PSR-18 client and PSR-17
factories you have installed, so nothing else needs wiring up. To supply your
own, pass a Builder:
use Rooberthh\Sdk\Http\Builder; $client = new MyApi\MyApiClient( url: 'https://api.example.com', builder: new Builder($myPsr18Client), );
Authentication
How Authentication Works
Implement AuthenticationContract and the credentials it returns are applied to
every request — the client wires it up in its constructor, so there is no
separate setup step.
Bearer Token
use Http\Message\Authentication; use Http\Message\Authentication\Bearer; class MyApiClient extends Client implements AuthenticationContract { public function authentication(): Authentication { return new Bearer($this->token); } }
Other forms
php-http/message ships the common ones — swap Bearer for any of these:
| Class | Sends |
|---|---|
Bearer |
Authorization: Bearer <token> |
BasicAuth |
Authorization: Basic <base64> |
Header |
any header you name |
QueryParam |
credentials as query parameters |
Wsse |
a WSSE token |
Chain |
several of the above, in order |
Signing requests, and refreshing tokens
Anything else is your own Authentication. It is handed the request and
returns it authenticated, so HMAC signing and fetching a fresh token both
belong here:
use Http\Message\Authentication; use Psr\Http\Message\RequestInterface; final class HmacSignature implements Authentication { public function __construct(private readonly string $secret) {} public function authenticate(RequestInterface $request): RequestInterface { $signature = hash_hmac('sha256', (string) $request->getBody(), $this->secret); return $request->withHeader('X-Signature', $signature); } }
Resource Classes
Resources represent API endpoints and handle HTTP requests. Each resource typically manages one API resource type (users, products, orders, etc.).
<?php namespace MyApi\Resources; use Rooberthh\Sdk\Concerns\Resources\CanAccessClient; use Rooberthh\Sdk\Concerns\Resources\CanCreateRequests; use Rooberthh\Sdk\FilterBuilder; use Rooberthh\HttpTools\Enums\Method; use MyApi\DataObjects\UserDataObject; class UserResource { use CanAccessClient; use CanCreateRequests; public function list(): array { // Use FilterBuilder for fluent query parameter construction $request = FilterBuilder::for($this->request(Method::GET, '/users')) ->where('sort', 'name') ->where('order', 'asc') ->where('limit', 10) ->apply(); // Results in: /users?sort=name&order=asc&limit=10 $response = $this->client->send($request); $data = json_decode($response->getBody()->getContents(), true); return array_map( fn($user) => UserDataObject::make($user), $data ); } public function show(int $id): UserDataObject { $request = $this->request(Method::GET, "/users/{$id}"); $response = $this->client->send($request); $data = json_decode($response->getBody()->getContents(), true); return UserDataObject::make($data); } public function create(array $userData): UserDataObject { $request = $this->request(Method::POST, '/users') ->withBody($this->payload(json_encode($userData))); $response = $this->client->send($request); $data = json_decode($response->getBody()->getContents(), true); return UserDataObject::make($data); } public function update(int $id, array $userData): UserDataObject { $request = $this->request(Method::PUT, "/users/{$id}") ->withBody($this->payload(json_encode($userData))); $response = $this->client->send($request); $data = json_decode($response->getBody()->getContents(), true); return UserDataObject::make($data); } public function delete(int $id): void { $request = $this->request(Method::DELETE, "/users/{$id}"); $this->client->send($request); } }
FilterBuilder
The FilterBuilder provides a fluent interface for adding query parameters to requests:
use Rooberthh\Sdk\FilterBuilder; // Basic usage $request = FilterBuilder::for($this->request(Method::GET, '/users')) ->where('status', 'active') ->where('page', 1) ->apply(); // Optional filters - null is dropped, so no conditionals are needed $request = FilterBuilder::for($this->request(Method::GET, '/users')) ->where('status', 'active') ->where('q', $search ?: null) ->apply(); // The key format is up to you - use whatever your API expects $request = FilterBuilder::for($this->request(Method::GET, '/products')) ->where('filter[price][gte]', 100) // Nested bracket notation ->where('price_lte', 500) // Underscore notation ->apply(); // Bulk filters - nulls are dropped, so optional filters can be passed straight through $request = FilterBuilder::for($this->request(Method::GET, '/users')) ->whereAll(['status' => $status, 'role' => $role]) ->apply();
Value handling
Values are normalised when the query string is built:
| Given | Sent as |
|---|---|
null |
omitted entirely |
true / false |
true / false |
| Backed enum | its value |
| Pure enum | its name |
DateTimeInterface |
ATOM (2026-01-02T03:04:05+00:00) |
array |
bracketed pairs (tags[0]=vip&tags[1]=new) |
Stringable |
its string form |
Anything else throws an InvalidArgumentException naming the offending filter.
Query parameters already present on the request are preserved; filters merge over
them, and bracketed keys merge key by key rather than colliding. Values are encoded
per RFC 3986, so spaces become %20 rather than +.
Advanced Usage
The transport
Every request passes through a plugin chain held by a Builder. Plugins can be
added and removed at any point, and the client is rebuilt on the next request:
use Http\Client\Common\Plugin\RetryPlugin; $client->builder()->addPlugin(new RetryPlugin(['retries' => 3])); $client->builder()->removePlugin(RetryPlugin::class);
Anything implementing Http\Client\Common\Plugin fits here — logging,
retries, redirects, recording. php-http/cache-plugin adds HTTP response
caching if you install it.
To apply plugins to every client of a given SDK instead, override
defaultPlugins():
class MyApiClient extends Client implements AuthenticationContract { public function defaultPlugins(): array { return [new RetryPlugin(['retries' => 3])]; } }
It returns nothing by default. ErrorPlugin is deliberately not included:
failures are reported by $response->failed() and thrown only when you call
$response->throw(). RetryPlugin is left out too, since retrying a request
that is not idempotent is the SDK author's call to make.
Reading the last response
Resource methods usually return data objects, which discards the response they
came from. getLastResponse() gets it back — for pagination links, rate limit
headers, or request ids:
$users = $client->users()->list(); // list<UserDataObject> $client->getLastResponse()?->header('X-RateLimit-Remaining'); $client->getLastResponse()?->status();
Custom headers
Override defaultHeaders() in your client:
class MyApiClient extends Client implements AuthenticationContract { public function defaultHeaders(): array { return [ 'Accept' => 'application/json', 'User-Agent' => 'MySDK/1.0', 'X-Custom-Header' => 'value', ]; } }
Special Thanks
This project would not have been possible without the incredible work of the open-source community.
A huge thank you to Steve McDougall whose work heavily inspired the architecture and patterns used in this SDK. The foundation and approach taken here are directly influenced by his contributions to the PHP ecosystem.
License
MIT License