{"id":49771538,"url":"https://github.com/andy87/php-client-sdk","last_synced_at":"2026-05-16T18:00:45.624Z","repository":{"id":356831443,"uuid":"1234222165","full_name":"andy87/php-client-sdk","owner":"andy87","description":"Base abstractions for building typed PHP API clients.","archived":false,"fork":false,"pushed_at":"2026-05-12T08:15:02.000Z","size":246,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-12T14:39:22.559Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andy87.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-09T22:46:37.000Z","updated_at":"2026-05-12T08:15:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/andy87/php-client-sdk","commit_stats":null,"previous_names":["andy87/php-client-sdk"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/andy87/php-client-sdk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andy87%2Fphp-client-sdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andy87%2Fphp-client-sdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andy87%2Fphp-client-sdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andy87%2Fphp-client-sdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andy87","download_url":"https://codeload.github.com/andy87/php-client-sdk/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andy87%2Fphp-client-sdk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32987764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-13T13:14:54.681Z","status":"ssl_error","status_checked_at":"2026-05-13T13:14:51.610Z","response_time":115,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-11T13:14:23.879Z","updated_at":"2026-05-14T16:02:00.056Z","avatar_url":"https://github.com/andy87.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PHP Client SDK\n\nBase abstractions for building typed PHP API clients.\n\n[Russian documentation](docs/ru/README.md)\n\n## Overview\n\n`andy87/php-client-sdk` provides a small set of reusable building blocks for API client SDKs:\n\n- prompt DTOs for request method, endpoint, path parameters, query parameters, body and validation;\n- response DTOs for normalized response data, status code, headers and API errors;\n- provider base class for executing typed API methods;\n- pluggable authorization strategies;\n- pluggable HTTP transport with a native PHP stream implementation.\n\nThe package does not generate API clients and does not depend on a specific HTTP client library.\n\n## Requirements\n\n- PHP 8.1 or higher.\n- Composer.\n\n## Installation\n\n```bash\ncomposer require andy87/php-client-sdk\n```\n\n## Core Concepts\n\nThe package separates an API call into three parts:\n\n- `PromptInterface` describes an outgoing request.\n- `ResponseInterface` describes a typed API response.\n- `AbstractProvider` connects prompts, responses, authorization and HTTP transport.\n\n`NativeHttpTransport` can be used without extra dependencies. If a project needs another transport, implement `HttpTransportInterface`.\n\n## Prompt DTO\n\nExtend `AbstractPrompt` to describe a request. The base class hydrates declared properties from input data, validates required fields, builds path/query/body arrays and normalizes nested DTO values through `toArray()` or `toValue()` when those methods exist.\n\nUse `PublicPrompt` for public endpoints and `PrivatePrompt` for private endpoints with an authorization profile. `AbstractPrompt` remains the generic base class for custom prompt schemes.\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Request\\Prompt\\AbstractPrompt;\n\n/**\n * Describes a request for loading one user by identifier.\n */\nfinal class GetUserPrompt extends AbstractPrompt\n{\n    protected const METHOD = 'GET';\n    protected const ENDPOINT = '/users/{id}';\n    protected const FIELD_MAP = [\n        'id' =\u003e 'id',\n        'includePosts' =\u003e 'include_posts',\n    ];\n    protected const REQUIRED_FIELDS = ['id'];\n    protected const PATH_FIELDS = ['id'];\n    protected const QUERY_FIELDS = ['includePosts'];\n    protected const BODY_FIELDS = [];\n    protected const CONTENT_TYPE = null;\n\n    public int $id;\n    public ?bool $includePosts = null;\n}\n```\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Request\\Prompt\\PrivatePrompt;\nuse and_y87\\PhpClientSdk\\Request\\Prompt\\PublicPrompt;\n\n/**\n * Describes a public health-check request.\n */\nfinal class HealthPrompt extends PublicPrompt\n{\n    protected const METHOD = 'GET';\n    protected const ENDPOINT = '/health';\n}\n\n/**\n * Describes a private order creation request.\n */\nfinal class CreateOrderPrompt extends PrivatePrompt\n{\n    protected const METHOD = 'POST';\n    protected const ENDPOINT = '/orders';\n    protected const AUTHORIZATION_PROFILE = 'orders-api';\n}\n```\n\n## Response DTO\n\nExtend `AbstractResponse` to describe data returned by the API. On successful responses the base class hydrates properties listed in `FIELD_MAP` and validates `REQUIRED_FIELDS`. On HTTP errors it stores `ApiError` and skips required-field validation.\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Response\\Model\\AbstractResponse;\n\n/**\n * Contains user data returned by the API.\n */\nfinal class GetUserResponse extends AbstractResponse\n{\n    protected const FIELD_MAP = [\n        'id' =\u003e 'id',\n        'name' =\u003e 'name',\n    ];\n    protected const REQUIRED_FIELDS = ['id', 'name'];\n\n    public int $id;\n    public string $name;\n}\n```\n\n## Provider Usage\n\nExtend `AbstractProvider` and expose public methods for concrete API operations. The protected `request()` method validates the prompt, adds authorization headers when required, sends the HTTP request and returns the requested response DTO.\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Client\\Provider\\AbstractProvider;\n\n/**\n * Provides typed access to user API methods.\n */\nfinal class UsersProvider extends AbstractProvider\n{\n    /**\n     * Loads one user by identifier.\n     *\n     * @param int $id User identifier.\n     *\n     * @return GetUserResponse Typed API response.\n     *\n     * @throws InvalidArgumentException When prompt validation fails.\n     * @throws RuntimeException When authorization or transport fails.\n     * @throws UnexpectedValueException When a successful response misses required fields.\n     */\n    public function getUser(int $id): GetUserResponse\n    {\n        return $this-\u003erequest(\n            new GetUserPrompt(['id' =\u003e $id]),\n            GetUserResponse::class,\n        );\n    }\n}\n```\n\nCreate the provider with a base URL, authorization strategy and transport:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Strategy\\NullAuthorizationStrategy;\nuse and_y87\\PhpClientSdk\\Client\\Config\\ClientOptions;\nuse and_y87\\PhpClientSdk\\Transport\\Native\\NativeHttpTransport;\nuse and_y87\\PhpClientSdk\\Transport\\Retry\\DefaultRetryPolicy;\n\n$provider = new UsersProvider(\n    baseUrl: 'https://api.example.com',\n    authorizationStrategy: new NullAuthorizationStrategy(),\n    transport: new NativeHttpTransport(),\n    options: new ClientOptions(\n        timeout: 30,\n        retryPolicy: new DefaultRetryPolicy(maxAttempts: 2),\n    ),\n);\n\n$response = $provider-\u003egetUser(123);\n\nif ($response-\u003ehasError()) {\n    $error = $response-\u003egetError();\n    echo $error?-\u003emessage ?? 'API request failed.';\n}\n\necho $response-\u003egetStatusCode();\n```\n\n## Client Options\n\n`ClientOptions` is the main extension point. If it is not passed, the SDK uses safe defaults: JSON requests and responses, strict successful response validation, native no-retry policy and default request factory.\n\nConfigurable parts:\n\n- `timeout`, `headers`, `events`;\n- `strictValidation`;\n- `validatePrompt`;\n- `retryPolicy`;\n- `queryEncoder`;\n- `bodyEncoder`;\n- `responseDecoder`;\n- `errorFactory`;\n- `requestFactory`.\n- `authorizationResolver`;\n- `refreshAuthorizationStatusCodes`.\n\nRetry is disabled by default. Use `DefaultRetryPolicy` only when repeated requests are safe for the target API operation.\n\n`validatePrompt` controls local prompt validation before a request is built. It is enabled by default. Set it to `false` only in mock or test environments where a client must return success fixtures for incomplete input:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Client\\Config\\ClientOptions;\n\n$options = new ClientOptions(\n    strictValidation: true,\n    validatePrompt: false,\n);\n```\n\n`refreshAuthorizationStatusCodes` defaults to `[401]`. If the selected authorization strategy implements `RefreshableAuthorizationStrategyInterface`, the provider refreshes authorization and retries the request once after these statuses. Pass an empty list to disable this behavior.\n\nUse `BaseUrl` when a client wants to configure protocol, host, port and path prefix separately:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Client\\Config\\BaseUrl;\n\n$baseUrl = new BaseUrl(\n    host: 'api.example.com',\n    protocol: 'https',\n    prefix: 'api/v1',\n);\n```\n\n## Runtime Events and Headers\n\n`ClientRuntime` stores default request headers and event listeners shared by a client and its providers. Pass the same runtime object to providers that must share headers and listeners.\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Client\\Event\\BeforeRequestEvent;\nuse and_y87\\PhpClientSdk\\Client\\Event\\ClientEvents;\nuse and_y87\\PhpClientSdk\\Client\\Runtime\\ClientRuntime;\n\n$runtime = new ClientRuntime(\n    headers: [\n        'X-Client' =\u003e 'crm',\n    ],\n    events: [\n        ClientEvents::BEFORE_REQUEST =\u003e static function (BeforeRequestEvent $event): void {\n            $event-\u003erequest-\u003eheaders['X-Trace-Id'] = bin2hex(random_bytes(8));\n        },\n    ],\n);\n\n$runtime-\u003eaddHeaders([\n    'X-Account' =\u003e 'main',\n]);\n```\n\nSupported events:\n\n- `ClientEvents::AFTER_INIT` after a concrete client finishes initialization.\n- `ClientEvents::BEFORE_REQUEST` before transport sends a mutable `HttpRequest`.\n- `ClientEvents::AFTER_REQUEST` after raw HTTP response is converted to a typed response DTO.\n- `ClientEvents::REQUEST_EXCEPTION` after transport, JSON decoding or response DTO construction fails.\n\nHeader names are merged case-insensitively. Authorization headers override default runtime headers, and `BEFORE_REQUEST` listeners can still mutate the final request.\n\n## Authorization\n\nUse `NullAuthorizationStrategy` for public APIs:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Strategy\\NullAuthorizationStrategy;\n\n$authorization = new NullAuthorizationStrategy();\n```\n\nUse `ClientCredentialsAuthorizationStrategy` for OAuth `client_credentials`. The strategy requests an access token through the configured transport and caches it until it expires. By default, the token is stored in process memory.\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Strategy\\ClientCredentialsAuthorizationStrategy;\n\n$authorization = new ClientCredentialsAuthorizationStrategy(\n    tokenUrl: 'https://auth.example.com/oauth/token',\n    clientId: 'client-id',\n    clientSecret: 'client-secret',\n    scope: 'users.read',\n    timeout: 30,\n);\n```\n\nPass `CacheInterface` when the token must outlive the current PHP process. The SDK ships `ArrayCache` for memory scenarios and `SimpleCacheAdapter` for plugging in PSR-16/simple-cache compatible stores without adding a direct dependency on a concrete framework.\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Strategy\\ClientCredentialsAuthorizationStrategy;\nuse and_y87\\PhpClientSdk\\Transport\\Cache\\SimpleCacheAdapter;\n\n$authorization = new ClientCredentialsAuthorizationStrategy(\n    tokenUrl: 'https://auth.example.com/oauth/token',\n    clientId: 'client-id',\n    clientSecret: 'client-secret',\n    tokenCache: new SimpleCacheAdapter($psr16Cache),\n    tokenCacheKey: 'oauth:example:client-id',\n    clockSkew: 60,\n);\n```\n\nExternal cache payloads store `access_token` and `expires_at`. `clockSkew` controls early refresh: with `60`, the strategy stops using the token 60 seconds before `expires_at`.\n\n`ClientCredentialsAuthorizationStrategy` refreshes its cached token when a provider receives a configured refresh status, `401` by default, and then the provider retries the original request once.\n\nOther built-in strategies:\n\n- `BearerTokenAuthorizationStrategy` for a static Bearer token;\n- `BasicAuthorizationStrategy` for HTTP Basic auth;\n- `ApiKeyAuthorizationStrategy` for header or query API keys;\n- `CallbackAuthorizationStrategy` for project-specific authorization headers.\n\nPrompts require authorization by default. Override the prompt constant when a request is public:\n\n```php\nprotected const AUTHORIZATION_REQUIRED = false;\n```\n\nUse an authorization resolver when different operations require different authorization strategies:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Strategy\\ApiKeyAuthorizationStrategy;\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Resolver\\AuthorizationProfileStrategyResolver;\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Resolver\\PromptClassAuthorizationStrategyResolver;\nuse and_y87\\PhpClientSdk\\Client\\Config\\ClientOptions;\n\n$options = new ClientOptions(\n    authorizationResolver: new PromptClassAuthorizationStrategyResolver([\n        GetUserPrompt::class =\u003e new ApiKeyAuthorizationStrategy('X-Api-Key', 'secret'),\n    ]),\n);\n```\n\nFor `PrivatePrompt` subclasses, prefer profile names such as `default`, `avito-client-credentials`, `api-key` or `sandbox-token`:\n\n```php\n$options = new ClientOptions(\n    authorizationResolver: new AuthorizationProfileStrategyResolver([\n        'orders-api' =\u003e new ApiKeyAuthorizationStrategy('X-Api-Key', 'secret'),\n    ]),\n);\n```\n\n## HTTP Transport\n\n`NativeHttpTransport` sends requests through PHP stream wrappers. It supports:\n\n- query parameters;\n- JSON request bodies;\n- `application/x-www-form-urlencoded` request bodies;\n- `multipart/form-data` request bodies through `MultipartFile`;\n- already encoded raw request bodies;\n- response status code and headers;\n- JSON response decoding through `HttpResponse::json()`.\n\nCustom transports must implement `HttpTransportInterface`:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Contracts\\Http\\HttpTransportInterface;\nuse and_y87\\PhpClientSdk\\Transport\\Http\\HttpRequest;\nuse and_y87\\PhpClientSdk\\Transport\\Http\\HttpResponse;\n\n/**\n * Sends HTTP requests through an application-specific client.\n */\nfinal class CustomTransport implements HttpTransportInterface\n{\n    /**\n     * Sends an HTTP request.\n     *\n     * @param HttpRequest $request Request data.\n     *\n     * @return HttpResponse Response data.\n     *\n     * @throws RuntimeException When the request cannot be sent.\n     */\n    public function send(HttpRequest $request): HttpResponse\n    {\n        throw new RuntimeException('Implement transport integration here.');\n    }\n}\n```\n\n## Mock Transport\n\n`MockTransport` returns configured `HttpResponse` fixtures and never falls back to real network requests. Use it for test stands where a client must return successful API-shaped data without calling the external service.\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Strategy\\NullAuthorizationStrategy;\nuse and_y87\\PhpClientSdk\\Client\\Config\\ClientOptions;\nuse and_y87\\PhpClientSdk\\Testing\\Mock\\MockTransport;\nuse and_y87\\PhpClientSdk\\Testing\\Mock\\PromptClassMockResponseResolver;\nuse and_y87\\PhpClientSdk\\Testing\\Mock\\RouteMockResponseResolver;\n\n$resolver = (new RouteMockResponseResolver())\n    -\u003eaddJson('GET', '/users/{id}', [\n        'id' =\u003e 123,\n        'name' =\u003e 'Mock User',\n    ]);\n\n$provider = new UsersProvider(\n    baseUrl: 'https://api.example.com',\n    authorizationStrategy: new NullAuthorizationStrategy(),\n    transport: new MockTransport($resolver),\n    options: new ClientOptions(validatePrompt: false),\n);\n```\n\nRoutes match by HTTP method and absolute URL, path or endpoint template stored in request metadata. OAuth token requests can be mocked by absolute token URL:\n\n```php\n$resolver-\u003eaddJson('POST', 'https://auth.example.com/oauth/token', [\n    'access_token' =\u003e 'mock-token',\n    'expires_in' =\u003e 3600,\n]);\n```\n\n`validatePrompt=false` disables only `Prompt::validate()`. Request building can still fail when a prompt cannot provide a method, endpoint or required path placeholder.\n\nIf route paths are unstable or generated, use `PromptClassMockResponseResolver` to bind fixtures to Prompt DTO classes:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Testing\\Mock\\MockTransport;\nuse and_y87\\PhpClientSdk\\Testing\\Mock\\PromptClassMockResponseResolver;\n\n$resolver = (new PromptClassMockResponseResolver())\n    -\u003eaddJson(GetUserPrompt::class, [\n        'id' =\u003e 123,\n        'name' =\u003e 'Mock User',\n    ]);\n\n$provider = new UsersProvider(\n    baseUrl: 'https://api.example.com',\n    authorizationStrategy: new NullAuthorizationStrategy(),\n    transport: new MockTransport($resolver),\n);\n```\n\n## Traceable Transport\n\n`TraceableTransport` wraps any `HttpTransportInterface` and records requests, responses, exceptions and duration without changing transport behavior.\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nuse and_y87\\PhpClientSdk\\Transport\\Native\\NativeHttpTransport;\nuse and_y87\\PhpClientSdk\\Transport\\Trace\\TraceableTransport;\nuse and_y87\\PhpClientSdk\\Security\\Authorization\\Strategy\\NullAuthorizationStrategy;\n\n$transport = new TraceableTransport(new NativeHttpTransport());\n$provider = new UsersProvider(\n    baseUrl: 'https://api.example.com',\n    authorizationStrategy: new NullAuthorizationStrategy(),\n    transport: $transport,\n);\n\n$response = $provider-\u003egetUser(123);\n$lastRecord = $transport-\u003egetLastRecord();\n```\n\nResponse DTOs can also store local diagnostic notes:\n\n```php\n$response-\u003eaddDiagnostic(['source' =\u003e 'fixture', 'case' =\u003e 'empty-list']);\n$diagnostics = $response-\u003egetDiagnostics();\n```\n\n## Error Handling\n\n- Prompt validation throws `InvalidArgumentException` when a required field is missing or empty.\n- Request factory validation can throw `ValidationException` when an endpoint contains an unfilled path placeholder.\n- Authorization failures throw `AuthorizationException`.\n- Transport failures throw `TransportException`.\n- Successful non-JSON responses throw `ResponseDecodeException`.\n- Response DTO construction failures throw `ResponseHydrationException`.\n- HTTP responses with status code `400` or higher are converted to `ApiError` and available through `ResponseInterface::getError()`, including non-JSON error bodies.\n- Successful responses with missing required fields throw `UnexpectedValueException` when `strictValidation` is enabled.\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandy87%2Fphp-client-sdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandy87%2Fphp-client-sdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandy87%2Fphp-client-sdk/lists"}