From a55c654fc04f5feaf10cf60d842444301c12cad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Sat, 8 Aug 2026 14:43:38 +0100 Subject: [PATCH 1/2] feat(resource): support constructor dependencies --- docs/03-api.md | 16 ++++++++++ docs/04-resource-authoring.md | 55 +++++++++++++++++++++++++++++++++++ src/Api.php | 14 ++++++++- tests/Integration/ApiTest.php | 45 ++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) diff --git a/docs/03-api.md b/docs/03-api.md index 9708550..61da10b 100644 --- a/docs/03-api.md +++ b/docs/03-api.md @@ -111,6 +111,22 @@ Protected helper for creating resource instances from an API class. See [Resource Authoring](04-resource-authoring.md) for the recommended API-to-resource pattern. +### `resourceWith()` + +> **Available since version 3.3.0.** + +```php +resourceWith(string $class, mixed ...$arguments): Resource +``` + +Protected helper for creating a resource with typed SDK-author constructor +dependencies. The resource `Runtime` is provided automatically as the first +constructor argument; additional positional or named arguments are forwarded +after it. + +See [Resource Constructor Dependencies](04-resource-authoring.md#resource-constructor-dependencies) +for the complete authoring pattern. + ## Request Defaults ### `baseUrl()` diff --git a/docs/04-resource-authoring.md b/docs/04-resource-authoring.md index 4978eaa..5528497 100644 --- a/docs/04-resource-authoring.md +++ b/docs/04-resource-authoring.md @@ -39,6 +39,61 @@ final class ExampleApi extends Api `Api::resource()` creates a fresh resource instance. Resource-chain infrastructure overrides, such as `withCache()`, are immutable, so fluent customizations do not leak into later calls. +## Resource Constructor Dependencies + +> **Available since version 3.3.0.** + +Use `resourceWith()` when a resource needs typed, SDK-author-owned data that +should not be placed in the shared SDK config. The runtime is injected +automatically as the first constructor argument. + +```php +use ProgrammatorDev\Api\Api; +use ProgrammatorDev\Api\Resource; +use ProgrammatorDev\Api\Runtime; + +final class ExampleApi extends Api +{ + public function __construct( + private readonly string $apiKey, + ) { + parent::__construct(); + } + + public function assets(): AssetResource + { + return $this->resourceWith( + AssetResource::class, + apiKey: $this->apiKey, + ); + } +} + +final class AssetResource extends Resource +{ + public function __construct( + Runtime $runtime, + private readonly string $apiKey, + ) { + parent::__construct($runtime); + } + + public function url(string $file): string + { + return sprintf( + 'https://cdn.example.com/assets/%s?key=%s', + rawurlencode($file), + rawurlencode($this->apiKey), + ); + } +} +``` + +This keeps the dependency private to the concrete API and resource. Use +`Config` for SDK options that should also be available to contexts, entities, +envelopes, hooks, and error handlers. Credentials used for HTTP authentication +should still be configured through `auth()`. + ## Endpoint Requests Use `endpoint()` inside resource methods to create the request builder: diff --git a/src/Api.php b/src/Api.php index 5e43ca9..710e7fe 100644 --- a/src/Api.php +++ b/src/Api.php @@ -104,7 +104,19 @@ public function setup(): Setup */ protected function resource(string $class): Resource { - return new $class($this->runtime()); + // Keep the original protected signature compatible with SDKs that override it. + return $this->resourceWith($class); + } + + /** + * @template T of Resource + * @param class-string $class + * @return T + * @todo Merge constructor argument forwarding into resource() in the next major release. + */ + protected function resourceWith(string $class, mixed ...$arguments): Resource + { + return new $class($this->runtime(), ...$arguments); } protected function baseUrl(?string $baseUrl): static diff --git a/tests/Integration/ApiTest.php b/tests/Integration/ApiTest.php index 74c6d88..5379bad 100644 --- a/tests/Integration/ApiTest.php +++ b/tests/Integration/ApiTest.php @@ -7,6 +7,8 @@ use ProgrammatorDev\Api\Context\RequestContext; use ProgrammatorDev\Api\Context\ResponseContext; use ProgrammatorDev\Api\Http\Method; +use ProgrammatorDev\Api\Resource; +use ProgrammatorDev\Api\Runtime; use ProgrammatorDev\Api\Test\Fixture\FakeApi; use ProgrammatorDev\Api\Test\Fixture\HeaderPlugin; use ProgrammatorDev\Api\Test\Support\AbstractTestCase; @@ -32,6 +34,30 @@ public function testConfigCanBeSetAndReadBySdkApi(): void ], $api->config()->all()); } + public function testSdkAuthorCanPassTypedConstructorDependenciesToResource(): void + { + $api = new class('secret') extends Api { + public function __construct( + private readonly string $apiKey + ) { + parent::__construct(); + } + + public function links(): ConstructorDependencyResource + { + return $this->resourceWith( + ConstructorDependencyResource::class, + apiKey: $this->apiKey + ); + } + }; + + $this->assertSame( + 'https://api.example.com/files/report%201.pdf?api_key=secret', + $api->links()->url('report 1.pdf') + ); + } + public function testApiCanSendPublicRequest(): void { $client = $this->mockClient(new Response(body: '{"id":1,"name":"John"}')); @@ -235,3 +261,22 @@ enum ApiRequestValue: string { case ACTIVE = 'active'; } + +final class ConstructorDependencyResource extends Resource +{ + public function __construct( + Runtime $runtime, + private readonly string $apiKey + ) { + parent::__construct($runtime); + } + + public function url(string $file): string + { + return sprintf( + 'https://api.example.com/files/%s?api_key=%s', + rawurlencode($file), + rawurlencode($this->apiKey) + ); + } +} From d3bb55d6f4e82054e80193cd8c7d3e1de9a47547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Sat, 8 Aug 2026 14:49:44 +0100 Subject: [PATCH 2/2] fix(resource): preserve existing construction path --- src/Api.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Api.php b/src/Api.php index 710e7fe..753f351 100644 --- a/src/Api.php +++ b/src/Api.php @@ -104,8 +104,7 @@ public function setup(): Setup */ protected function resource(string $class): Resource { - // Keep the original protected signature compatible with SDKs that override it. - return $this->resourceWith($class); + return new $class($this->runtime()); } /**