Skip to content

[4.x] Type dto() from the request's createDtoFromResponse() in PHPStan - #565

Open
pindab0ter wants to merge 2 commits into
saloonphp:v4from
pindab0ter:phpstan-dto-types
Open

pindab0ter wants to merge 2 commits into
saloonphp:v4from
pindab0ter:phpstan-dto-types

Conversation

@pindab0ter

Copy link
Copy Markdown

PHPStan sees Response::dto() and dtoOrFail() as mixed, even when the request's createDtoFromResponse() declares a precise return type. Every call site needs a @var to get the type back:

/** @var User $user */
$user = $connector->send(new GetUserRequest($id))->dtoOrFail();

This PR lets PHPStan infer it:

$user = $connector->send(new GetUserRequest($id))->dtoOrFail(); // User

Generics

Response and PendingRequest carry the request that produced them as @template TRequest of Request = Request. The default keeps every existing Response $response hint valid without generics.

  • Connector::send() and sendAndRetry() return Response<TRequest> for the request passed in; SoloRequest::send() returns Response<$this>
  • Response::getRequest() and PendingRequest::getRequest() return the concrete request class
  • Response::fromPsrResponse() and withBufferedBody() keep the request type

The type survives variables and helper methods, since it lives in the type rather than in the call chain:

$response = $connector->send(new GetUserRequest($id));

$response->getRequest(); // GetUserRequest
$response->dto();        // User

PHPStan extension

PHPDoc cannot express "the return type of createDtoFromResponse() on TRequest", so the package ships a small extension, registered the same way as laravel-actions:

  • DtoReturnTypeExtension types dto() and dtoOrFail() as the request's createDtoFromResponse() return type. A request without its own typed implementation keeps mixed
  • DtoThrowTypeExtension adds the exceptions declared on createDtoFromResponse() to dto() and dtoOrFail(), and respects exceptions.implicitThrows

With phpstan/extension-installer the extension loads automatically through extra.phpstan.includes. Without it:

includes:
    - vendor/saloonphp/saloon/extension.neon

Missing @throws

A separate commit documents exceptions that were thrown but undeclared: FatalRequestException and RequestException on send(), sendAndRetry() and SoloRequest::send(), JsonException on json(), array(), object() and collect(), and LogicException on dtoOrFail().

Projects that enable PHPStan's checked exceptions (exceptions.check.missingCheckedExceptionInThrows) will start getting reports at call sites that don't declare these. That's the intended effect, but it can surface new errors on upgrade, so it may warrant a release note. If you'd rather not ship these in a minor, the commit can be dropped without affecting the rest.

Notes

  • When the request's createDtoFromResponse() returns null, dto() falls back to the connector's. The response doesn't carry the connector type, so the extension can't account for that fallback
  • One existing baseline entry changes wording only, because PendingRequest now prints with its generic

馃 Generated with Claude Code

https://claude.ai/code/session_011np691Rnida5v4u3wsaKby

pindab0ter and others added 2 commits October 4, 2026 12:20
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011np691Rnida5v4u3wsaKby
@pindab0ter
pindab0ter marked this pull request as ready for review October 4, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant