Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement third-party HTTP API SDKs in the phpdevkits style with SaloonPHP. Covers the connector + resources + DTOs + requests layout, fixture-driven Pest testing with PII redaction, and the live-recording workflow. Use when building a new Saloon-based SDK (e.g. forge-sdk, a future stripe-sdk), adding a new resource to one, debugging a fixture-related test failure, or recovering from a spec-vs-runtime API surprise.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 6% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 25% | 0% |
| case-06 | ✗→✓ | ▲ Improved | -25% | 0% |
| case-07 | ✗→✓ | ▲ Improved | -18% | 0% |
| case-08 | ✗→✓ | ▲ Improved | -16% | 0% |
Use this skill when working inside a SaloonPHP-based SDK in this repo's shape — single Connector extending Saloon's Connector, organized by Resources/ + Requests/ + Data/ + Enums/ + Exceptions/, with recorded Saloon fixtures driving Pest tests at 100% coverage.
For general Pest / factory conventions that aren't Saloon-specific (no it(), factories under tests/Factories/, exception-test styles), see the pest-package-tests skill — this skill assumes those and only covers the Saloon side.
A minimal slice (one resource, one request, one DTO, one fixture-backed test):
php// src/Data/Widget.php final readonly class Widget implements JsonSerializable { public function __construct(public string $id, public string $name) {} public static function from(array $data): self { /* hydration + validation */ } public function jsonSerialize(): array { /* snake_case */ } } // src/Requests/GetWidget.php final class GetWidget extends Request { protected Method $method = Method::GET; public function __construct(private readonly int|string $id) {} #[Override] public function resolveEndpoint(): string { return sprintf('/widgets/%s', $this->id); } } // src/Resources/WidgetResource.php final class WidgetResource extends BaseResource { public function get(): Widget { $data = $this->connector->send(new GetWidget($this->id))->json('data'); return Widget::from($data); } } // tests/Unit/Resources/WidgetResourceTest.php test('get() returns a hydrated Widget', function (): void { $mockClient = new MockClient([ GetWidget::class => new WidgetFixture('widgets/get'), ]); $sdk = new MySdk('token')->withMockClient($mockClient); expect($sdk->widget(1)->get())->toBeInstanceOf(Widget::class); });
The WidgetFixture is a project-local subclass of a base Tests\Utils\<SdkName>Fixture that adds resource-specific redaction rules — see TESTING.md.
namespace layout, connector responsibilities, exception hierarchy, org-context / nested-resource chains.
vs. item resources, ParsesPage for cursor pagination, iterate() helpers, action methods, write-DTO patterns.
(from() / jsonSerialize()), input DTOs (toArray() stripping nulls), when to add enums.
Requestclass shape, HasJsonBody body trait, query params via defaultQuery(), common Saloon API gotchas.
ForgeFixture-style PII redaction, no MockResponse::make(), no hand-crafted fixtures, lifecycle tests with try/finally cleanup.
.env setup, Saloon auto-record default, poisoned-fixture recovery, cursor-walk recording.
spec-vs-runtime mismatches, Saloon traps (#[Override] on defaultBody(), MockClient class-vs-sequence mode, clone helpers dropping the mock), coverage attribution quirks.
layout in rules/architecture.md. If not, this skill doesn't apply — fall back to upstream Saloon docs.
on an existing resource?). Read the matching rule file before writing code.
class, then the resource, then the test. Run the test once with no fixture to record from the live API.
docs/FINDINGS.md with the date — see rules/pitfalls.md for the entry template.
Other measured skills in the registry, with their headline benchmark lift.