Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@
/.idea
/index.php
/src/TestApi.php
/AGENTS.md
198 changes: 198 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# AGENTS.md

## Project Purpose

`programmatordev/php-api-sdk` is a lightweight foundation for building fluent,
maintainable PHP API SDKs. It should make common SDK work compact and enjoyable
without hiding the request lifecycle or becoming a heavy framework.

The package serves two developer audiences:

- SDK authors extend the package to build concrete API SDKs.
- SDK users consume those concrete SDKs.

Favor the SDK-author experience when choosing internal extension points and
authoring APIs. Keep the SDK-user surface focused on real resources and endpoint
methods, with deliberate escape hatches for advanced use cases.

## Core Concepts

Keep the architecture centered on a small set of clear responsibilities:

- `Api`: the SDK facade, resource entry point, and author-owned configuration
surface.
- `Setup`: the explicit SDK-user setup and hackability surface exposed through
`Api::setup()`.
- `Runtime`: the internal configured runtime used by resources for configuration
access and request execution.
- `Resource`: an immutable endpoint group and the primary SDK-author workflow.
- `Endpoint`: an immutable builder for request-local query, header, and body
options.
- `RequestOptions`: the request-local query, header, and body state carried by an
endpoint.
- `Response`: the decoded/raw response wrapper and mapping surface.
- `Entity`: the optional contract for typed response data objects.

Do not blur these responsibilities without a concrete simplification. In
particular, keep setup concerns out of resources and API-specific behavior out of
the generic runtime.

## Authoring Experience

Keep the common resource path compact:

```php
return $this
->endpoint()
->get('/path/{id}', ['id' => $id])
->entity(User::class);
```

Prefer fluent SDK authoring over low-level request construction inside
resources. Advanced PSR capabilities should remain available without dominating
the basic workflow.

Expose advanced SDK-user setup through one obvious surface:

```php
$api->setup()->plugins()->add($plugin);
$api->setup()->client($client);
$api->setup()->auth()->bearer($token);
```

Keep SDK-author setup helpers protected on `Api` by default. This preserves a
focused SDK-user autocomplete surface while allowing concrete SDKs to provide
purpose-built public configuration methods.

`send()` may remain public as an advanced escape hatch for endpoints not modeled
by a concrete SDK. It must still use the configured authentication, plugins,
cache, hooks, decoding, and error handling.

## Design Principles

- Keep the package small, explicit, and composable. Add an abstraction only when
it removes real complexity, improves SDK authoring, or supports an established
capability.
- Hackability is a feature. SDK users may intentionally customize the runtime
through `setup()`; that power should be explicit rather than hidden.
- Builders are mutable configuration objects. Fluent builder methods configure
state, while methods returning stored or built data use `get*()` names.
- Resources and endpoints are immutable request scopes. Fluent modifiers must
return clones and must not leak state into the API, sibling resources, or
later requests.
- Scoped overrides should retain access to the current API runtime and merge
lazily. Do not snapshot shared configuration or runtime services when only one
value needs to be overridden.
- One effective configuration must flow through request creation, hooks,
transport, errors, responses, and hydration. Different stages must not observe
different values for the same request.
- Independent modifiers should compose in any order unless ordering is an
intentional part of their contract. Applying one modifier must not discard
another modifier's state.
- Normalize ergonomic author-facing values at the narrowest shared boundary.
For example, normalize supported request values after defaults and endpoint
options merge, before serialization.
- Authentication strategies must be explicit. Multiple strategies compose
through `auth()->chain(...)` rather than relying on implicit precedence.
- Keep entities as response data/value objects by default. Do not introduce
hidden network calls, lazy loading, or transparent proxy behavior.
- Keep API-specific vocabulary in concrete SDK packages. Concepts such as
includes, selects, filters, and pagination should build on generic resource
primitives rather than enter the base package without broad applicability.
- Avoid architecture that requires constant dependency injection or repetitive
boilerplate in downstream SDKs.

## Compatibility And Capabilities

Preserve backward compatibility by default. Do not remove, rename, or change the
meaning of public APIs or protected SDK-author extension points without explicit
approval for a breaking release.

When extending behavior:

- Prefer additive APIs and compatible normalization at existing boundaries.
- Preserve established defaults and merge precedence.
- Keep original objects unchanged when introducing scoped fluent behavior.
- Call out any unavoidable break explicitly before implementation.
- Document a migration path for every approved breaking change.

Maintain support for the package's core capabilities:

- PSR-18 HTTP clients.
- PSR-17 request and stream factories.
- PSR-6 caches.
- PSR-3 loggers.
- Authentication.
- Plugins and middleware.
- Request and response hooks.
- Query and header defaults.
- Base URL and path construction.
- Response decoding and transformation.
- Error handling.
- Test utilities for SDK authors where they provide clear value.

## Implementation Approach

- Read adjacent code and tests before editing. Follow existing naming, fluent
patterns, typing, and file organization.
- Prefer the smallest coherent change that solves the current problem.
- Reuse existing helpers and extension points before creating new layers.
- Keep internal and public APIs consistent in terminology and return behavior.
- Add comments only for non-obvious constraints, ordering, isolation, or design
decisions. Do not narrate self-explanatory code.
- Treat request construction, execution, and response mapping as one pipeline.
Changes at one stage must be checked for effects on the others.

## Documentation

Update documentation alongside every user-visible or SDK-author-visible behavior
change.

Documentation should explain:

- The core concepts and their responsibilities.
- How to create and configure a simple SDK.
- How to author resources and request options.
- How to map responses to entities, collections, and envelopes.
- How to configure authentication, clients, factories, cache, logging, plugins,
hooks, and errors.
- How to create API-specific fluent helpers on top of generic primitives.
- Availability versions for newly introduced features when relevant.

Use focused documents as topics grow. Prefer clear navigation and concise,
complete examples over a single large guide. Keep examples API-neutral unless a
real downstream SDK is being used as an integration proof.

For breaking releases, provide an upgrade guide that identifies each changed
contract and its replacement path. Do not create version-specific upgrade guides
for additive minor releases.

## Testing

Add or update tests with every meaningful behavior change. Test public behavior
and supported extension points rather than private implementation details.

Use fixtures, fake APIs, test resources, mock clients, and local response objects
to represent realistic SDK authoring and usage. Cover both:

- Base package behavior.
- SDK-author behavior through small concrete SDK fixtures.

For scoped or pipeline behavior, verify isolation and propagation explicitly:

- Original and sibling instances remain unchanged.
- Defaults and local overrides merge with documented precedence.
- Later runtime configuration remains visible where it is not overridden.
- Hooks, errors, responses, and hydration observe the same effective context.
- Cache behavior does not leak request-local state.
- Independent fluent modifiers compose correctly.

## Downstream Validation

Validate important design decisions against representative downstream SDKs,
including both simple integrations and complex integrations with resources,
envelopes, metadata, pagination, filtering, and many entities.

The base package should make both styles straightforward without becoming
coupled to either API. A downstream friction point is evidence to evaluate, not
automatic justification for adding API-specific behavior to the core.
17 changes: 17 additions & 0 deletions docs/03-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ $response = $api->send(
);
```

> **Backed-enum normalization is available since version 3.1.0.**

The `query` and `headers` arrays accept string- and integer-backed enums. Query
parameters use their backed values, while header values are converted to strings
as required by PSR-7.

Path parameters are encoded and replaced in `{name}` placeholders.

`send()` still runs through the configured SDK pipeline:
Expand Down Expand Up @@ -75,6 +81,10 @@ $api->config(['timezone' => 'UTC']);
$api->config()->get('timezone');
```

API configuration applies globally. SDK users can override selected values for
one immutable resource chain with `Resource::withConfig()` without changing the
API-wide config. See [Resource-Local Configuration](04-resource-authoring.md#resource-local-configuration).

### `setup()`

```php
Expand Down Expand Up @@ -173,6 +183,13 @@ $this->defaultHeaders(['Accept' => 'application/json']);

Header names are not normalized by the package.

> **Backed-enum normalization is available since version 3.1.0.**

String- and integer-backed enums can be used as default query or header values.
Their scalar values are used when the request is built. Normalization also
applies recursively to nested query values and header value lists. Header values
are converted to strings as required by PSR-7.

## Pipeline Builders

### `auth()`
Expand Down
131 changes: 130 additions & 1 deletion docs/04-resource-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,45 @@ return $this
->collection(User::class, key: 'data');
```

### Backed Enum Values

> **Available since version 3.1.0.**

String- and integer-backed enums can be passed directly as query parameters or
header values:

```php
enum Status: string
{
case ACTIVE = 'active';
case PENDING = 'pending';
}

enum Visibility: int
{
case PUBLIC = 1;
}

return $this
->endpoint()
->queries([
'status' => Status::ACTIVE,
'filter' => ['visibility' => Visibility::PUBLIC],
])
->headers([
'X-Status' => Status::ACTIVE,
'X-Allowed-Statuses' => [Status::ACTIVE, Status::PENDING],
])
->get('/users')
->collection(User::class, key: 'data');
```

The backed values are normalized recursively after API defaults and endpoint
options are merged. This applies to endpoint values, API-level defaults,
nested query arrays, and header value lists. Header values are converted to
strings as required by PSR-7. Unit enums are not supported as request values;
pass an explicit scalar value instead.

SDK-user customization should be explicit in the resource method API. If a method argument is enough, prefer that over hidden resource state:

```php
Expand Down Expand Up @@ -328,6 +367,92 @@ final class UserEnvelope implements EnvelopeInterface

Keep context usage focused on hydration decisions. Entities should still be data/value objects by default and should not perform hidden network calls.

## Resource-Local Configuration

> **Available since version 3.1.0.**

Resource-local configuration lets SDK authors build typed, immutable helpers
for options that affect both a request and its response context:

```php
public function withLocale(string $locale): static
{
return $this->withConfig([
'locale' => $locale,
]);
}
```

SDK users then get an API-specific fluent method:

```php
$user = $api->users()->withLocale('pt')->find(1);
```

`withConfig()` remains available as the generic escape hatch:

```php
$user = $api
->users()
->withConfig(['locale' => 'pt'])
->find(1);
```

The original resource and API-wide configuration remain unchanged. The
override belongs to the cloned resource, so reusing that resource applies it to
every request made through the clone:

```php
$portugueseUsers = $api->users()->withLocale('pt');

$first = $portugueseUsers->find(1);
$second = $portugueseUsers->find(2);
```

Repeated calls merge their values, and later values win for the same key:

```php
$users = $api
->users()
->withConfig(['locale' => 'en', 'timezone' => 'UTC'])
->withConfig(['locale' => 'pt']);
```

The effective configuration contains `locale=pt` and `timezone=UTC`.
Non-overridden values come from the latest API-wide configuration, including
changes made after the resource was created:

```text
Latest API-wide configuration
-> resource withConfig() overrides
```

Inside a resource, the scoped runtime exposes the effective configuration:

```php
public function find(int $id): User
{
return $this
->endpoint()
->query(
'locale',
$this->runtime->config()->get('locale'),
)
->get('/users/{id}', ['id' => $id])
->entity(User::class);
}
```

Configuration is not automatically converted into query parameters or headers.
The resource author decides how each option maps to an endpoint.

Inside resource methods, read scoped values with
`$this->runtime->config()->get()`. Do not call `set()` or `merge()` on that
scoped `Config`; apply changes by returning a clone through `withConfig()`.
The effective configuration is propagated through request and response hooks,
error handling, response mapping, entities, collections, and envelopes. This
keeps request construction and response interpretation consistent.

## API-Specific Resource Chains

Keep API-specific vocabulary out of the base package. Add it in SDK resources with small fluent methods that use the generic endpoint helpers underneath.
Expand Down Expand Up @@ -376,7 +501,11 @@ $users = $api
->all();
```

Use the same pattern for API-specific concepts such as includes, filters, selects, pagination options, or locale settings. Clone the resource in `with*` methods so a configured chain does not leak into later calls.
Use the same pattern for API-specific concepts such as includes, filters,
selects, pagination options, or locale settings. Use `withConfig()` when the
value also affects request context or response interpretation. Clone the
resource directly for other API-specific request state so a configured chain
does not leak into later calls.

## Navigation

Expand Down
Loading