Skip to content

Commit a55c654

Browse files
committed
feat(resource): support constructor dependencies
1 parent 7320872 commit a55c654

4 files changed

Lines changed: 129 additions & 1 deletion

File tree

‎docs/03-api.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ Protected helper for creating resource instances from an API class.
111111

112112
See [Resource Authoring](04-resource-authoring.md) for the recommended API-to-resource pattern.
113113

114+
### `resourceWith()`
115+
116+
> **Available since version 3.3.0.**
117+
118+
```php
119+
resourceWith(string $class, mixed ...$arguments): Resource
120+
```
121+
122+
Protected helper for creating a resource with typed SDK-author constructor
123+
dependencies. The resource `Runtime` is provided automatically as the first
124+
constructor argument; additional positional or named arguments are forwarded
125+
after it.
126+
127+
See [Resource Constructor Dependencies](04-resource-authoring.md#resource-constructor-dependencies)
128+
for the complete authoring pattern.
129+
114130
## Request Defaults
115131

116132
### `baseUrl()`

‎docs/04-resource-authoring.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,61 @@ final class ExampleApi extends Api
3939

4040
`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.
4141

42+
## Resource Constructor Dependencies
43+
44+
> **Available since version 3.3.0.**
45+
46+
Use `resourceWith()` when a resource needs typed, SDK-author-owned data that
47+
should not be placed in the shared SDK config. The runtime is injected
48+
automatically as the first constructor argument.
49+
50+
```php
51+
use ProgrammatorDev\Api\Api;
52+
use ProgrammatorDev\Api\Resource;
53+
use ProgrammatorDev\Api\Runtime;
54+
55+
final class ExampleApi extends Api
56+
{
57+
public function __construct(
58+
private readonly string $apiKey,
59+
) {
60+
parent::__construct();
61+
}
62+
63+
public function assets(): AssetResource
64+
{
65+
return $this->resourceWith(
66+
AssetResource::class,
67+
apiKey: $this->apiKey,
68+
);
69+
}
70+
}
71+
72+
final class AssetResource extends Resource
73+
{
74+
public function __construct(
75+
Runtime $runtime,
76+
private readonly string $apiKey,
77+
) {
78+
parent::__construct($runtime);
79+
}
80+
81+
public function url(string $file): string
82+
{
83+
return sprintf(
84+
'https://cdn.example.com/assets/%s?key=%s',
85+
rawurlencode($file),
86+
rawurlencode($this->apiKey),
87+
);
88+
}
89+
}
90+
```
91+
92+
This keeps the dependency private to the concrete API and resource. Use
93+
`Config` for SDK options that should also be available to contexts, entities,
94+
envelopes, hooks, and error handlers. Credentials used for HTTP authentication
95+
should still be configured through `auth()`.
96+
4297
## Endpoint Requests
4398

4499
Use `endpoint()` inside resource methods to create the request builder:

‎src/Api.php‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,19 @@ public function setup(): Setup
104104
*/
105105
protected function resource(string $class): Resource
106106
{
107-
return new $class($this->runtime());
107+
// Keep the original protected signature compatible with SDKs that override it.
108+
return $this->resourceWith($class);
109+
}
110+
111+
/**
112+
* @template T of Resource
113+
* @param class-string<T> $class
114+
* @return T
115+
* @todo Merge constructor argument forwarding into resource() in the next major release.
116+
*/
117+
protected function resourceWith(string $class, mixed ...$arguments): Resource
118+
{
119+
return new $class($this->runtime(), ...$arguments);
108120
}
109121

110122
protected function baseUrl(?string $baseUrl): static

‎tests/Integration/ApiTest.php‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use ProgrammatorDev\Api\Context\RequestContext;
88
use ProgrammatorDev\Api\Context\ResponseContext;
99
use ProgrammatorDev\Api\Http\Method;
10+
use ProgrammatorDev\Api\Resource;
11+
use ProgrammatorDev\Api\Runtime;
1012
use ProgrammatorDev\Api\Test\Fixture\FakeApi;
1113
use ProgrammatorDev\Api\Test\Fixture\HeaderPlugin;
1214
use ProgrammatorDev\Api\Test\Support\AbstractTestCase;
@@ -32,6 +34,30 @@ public function testConfigCanBeSetAndReadBySdkApi(): void
3234
], $api->config()->all());
3335
}
3436

37+
public function testSdkAuthorCanPassTypedConstructorDependenciesToResource(): void
38+
{
39+
$api = new class('secret') extends Api {
40+
public function __construct(
41+
private readonly string $apiKey
42+
) {
43+
parent::__construct();
44+
}
45+
46+
public function links(): ConstructorDependencyResource
47+
{
48+
return $this->resourceWith(
49+
ConstructorDependencyResource::class,
50+
apiKey: $this->apiKey
51+
);
52+
}
53+
};
54+
55+
$this->assertSame(
56+
'https://api.example.com/files/report%201.pdf?api_key=secret',
57+
$api->links()->url('report 1.pdf')
58+
);
59+
}
60+
3561
public function testApiCanSendPublicRequest(): void
3662
{
3763
$client = $this->mockClient(new Response(body: '{"id":1,"name":"John"}'));
@@ -235,3 +261,22 @@ enum ApiRequestValue: string
235261
{
236262
case ACTIVE = 'active';
237263
}
264+
265+
final class ConstructorDependencyResource extends Resource
266+
{
267+
public function __construct(
268+
Runtime $runtime,
269+
private readonly string $apiKey
270+
) {
271+
parent::__construct($runtime);
272+
}
273+
274+
public function url(string $file): string
275+
{
276+
return sprintf(
277+
'https://api.example.com/files/%s?api_key=%s',
278+
rawurlencode($file),
279+
rawurlencode($this->apiKey)
280+
);
281+
}
282+
}

0 commit comments

Comments
 (0)