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..753f351 100644 --- a/src/Api.php +++ b/src/Api.php @@ -107,6 +107,17 @@ protected function resource(string $class): Resource return new $class($this->runtime()); } + /** + * @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 { $this->baseUrl = $baseUrl; 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) + ); + } +}