Skip to content
Open
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
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@
},
"sort-packages": true
},
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"scripts": {
"fix-code": [
"./vendor/bin/php-cs-fixer fix --allow-risky=yes"
Expand Down
15 changes: 15 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
-
class: Saloon\PHPStan\CreateDtoMethodResolver

-
class: Saloon\PHPStan\DtoReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension

-
class: Saloon\PHPStan\DtoThrowTypeExtension
arguments:
implicitThrows: %exceptions.implicitThrows%
tags:
- phpstan.dynamicMethodThrowTypeExtension
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ parameters:
path: src/Http/Connector.php

-
message: '#^Parameter \#1 \$response of method Saloon\\Http\\PendingRequest\:\:executeResponsePipeline\(\) expects Saloon\\Http\\Response, GuzzleHttp\\Promise\\PromiseInterface\|Saloon\\Http\\Response given\.$#'
message: '#^Parameter \#1 \$response of method Saloon\\Http\\PendingRequest\<Saloon\\Http\\Request\>\:\:executeResponsePipeline\(\) expects Saloon\\Http\\Response, GuzzleHttp\\Promise\\PromiseInterface\|Saloon\\Http\\Response given\.$#'
identifier: argument.type
count: 1
path: src/Http/Connector.php
Expand Down
1 change: 1 addition & 0 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#############################################################################################

includes:
- extension.neon
- phpstan-baseline.neon

parameters:
Expand Down
9 changes: 9 additions & 0 deletions src/Http/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
use Saloon\Http\PendingRequest\AuthenticatePendingRequest;
use Saloon\Http\OAuth2\GetClientCredentialsTokenBasicAuthRequest;

/**
* @template TRequest of \Saloon\Http\Request = \Saloon\Http\Request
*/
class PendingRequest
{
use AuthenticatesRequests;
Expand All @@ -52,6 +55,8 @@ class PendingRequest

/**
* The request used by the instance.
*
* @var TRequest
*/
protected Request $request;

Expand Down Expand Up @@ -82,6 +87,8 @@ class PendingRequest

/**
* Build up the request payload.
*
* @param TRequest $request
*/
public function __construct(Connector $connector, Request $request, ?MockClient $mockClient = null)
{
Expand Down Expand Up @@ -173,6 +180,8 @@ public function executeFatalPipeline(FatalRequestException $throwable): void

/**
* Get the request.
*
* @return TRequest
*/
public function getRequest(): Request
{
Expand Down
23 changes: 23 additions & 0 deletions src/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
use Saloon\Contracts\DataObjects\WithResponse;
use Saloon\Contracts\ArrayStore as ArrayStoreContract;

/**
* @template TRequest of \Saloon\Http\Request = \Saloon\Http\Request
*/
class Response
{
use Macroable;
Expand All @@ -43,6 +46,8 @@ class Response

/**
* The pending request that has all the request properties
*
* @var \Saloon\Http\PendingRequest<TRequest>
*/
protected readonly PendingRequest $pendingRequest;

Expand Down Expand Up @@ -85,6 +90,8 @@ class Response

/**
* Create a new response instance.
*
* @param \Saloon\Http\PendingRequest<TRequest> $pendingRequest
*/
public function __construct(ResponseInterface $psrResponse, PendingRequest $pendingRequest, RequestInterface $psrRequest, ?Throwable $senderException = null)
{
Expand All @@ -96,6 +103,11 @@ public function __construct(ResponseInterface $psrResponse, PendingRequest $pend

/**
* Create a new response instance
*
* @template TSentRequest of \Saloon\Http\Request
*
* @param \Saloon\Http\PendingRequest<TSentRequest> $pendingRequest
* @return static<TSentRequest>
*/
public static function fromPsrResponse(ResponseInterface $psrResponse, PendingRequest $pendingRequest, RequestInterface $psrRequest, ?Throwable $senderException = null): static
{
Expand All @@ -104,6 +116,8 @@ public static function fromPsrResponse(ResponseInterface $psrResponse, PendingRe

/**
* Get the pending request that created the response.
*
* @return \Saloon\Http\PendingRequest<TRequest>
*/
public function getPendingRequest(): PendingRequest
{
Expand All @@ -120,6 +134,8 @@ public function getConnector(): Connector

/**
* Get the original request that created the response.
*
* @return TRequest
*/
public function getRequest(): Request
{
Expand Down Expand Up @@ -220,6 +236,7 @@ public function getSenderException(): ?Throwable
*
* @param array-key|null $key
* @return ($key is null ? array<array-key, mixed> : mixed)
* @throws \JsonException
*/
public function json(string|int|null $key = null, mixed $default = null): mixed
{
Expand All @@ -241,6 +258,7 @@ public function json(string|int|null $key = null, mixed $default = null): mixed
*
* @param array-key|null $key
* @return ($key is null ? array<array-key, mixed> : mixed)
* @throws \JsonException
*/
public function array(int|string|null $key = null, mixed $default = null): mixed
{
Expand All @@ -249,6 +267,8 @@ public function array(int|string|null $key = null, mixed $default = null): mixed

/**
* Get the JSON decoded body of the response as an object or scalar value.
*
* @throws \JsonException
*/
public function object(string|int|null $key = null, mixed $default = null): mixed
{
Expand Down Expand Up @@ -302,6 +322,7 @@ public function xmlReader(): XmlReader
*
* @param array-key|null $key
* @return \Illuminate\Support\Collection<array-key, mixed>
* @throws \JsonException
*/
public function collect(string|int|null $key = null): Collection
{
Expand Down Expand Up @@ -337,6 +358,8 @@ public function dto(): mixed

/**
* Convert the response into a DTO or throw a LogicException if the response failed
*
* @throws \LogicException
*/
public function dtoOrFail(): mixed
{
Expand Down
33 changes: 33 additions & 0 deletions src/PHPStan/CreateDtoMethodResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Saloon\PHPStan;

use Saloon\Http\Response;
use PHPStan\Analyser\Scope;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ExtendedMethodReflection;

final class CreateDtoMethodResolver
{
public function isDtoMethod(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), ['dto', 'dtoOrFail'], true);
}

/**
* Resolve createDtoFromResponse() on the request that produced the response.
*/
public function resolve(MethodCall $methodCall, Scope $scope): ?ExtendedMethodReflection
{
$requestType = $scope->getType($methodCall->var)->getTemplateType(Response::class, 'TRequest');

if (! $requestType->hasMethod('createDtoFromResponse')->yes()) {
return null;
}

return $requestType->getMethod('createDtoFromResponse', $scope);
}
}
41 changes: 41 additions & 0 deletions src/PHPStan/DtoReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Saloon\PHPStan;

use PHPStan\Type\Type;
use Saloon\Http\Response;
use PHPStan\Analyser\Scope;
use PHPStan\Type\MixedType;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;

/**
* Types dto() and dtoOrFail() as the return type of the request's createDtoFromResponse().
*/
final class DtoReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(
private CreateDtoMethodResolver $resolver,
) {
}

public function getClass(): string
{
return Response::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $this->resolver->isDtoMethod($methodReflection);
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
$dtoType = $this->resolver->resolve($methodCall, $scope)?->getOnlyVariant()->getReturnType();

return $dtoType instanceof MixedType ? null : $dtoType;
}
}
53 changes: 53 additions & 0 deletions src/PHPStan/DtoThrowTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Saloon\PHPStan;

use Throwable;
use PHPStan\Type\Type;
use Saloon\Http\Response;
use PHPStan\Analyser\Scope;
use PHPStan\Type\ObjectType;
use PHPStan\Type\TypeCombinator;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodThrowTypeExtension;

/**
* Adds the exceptions of the request's createDtoFromResponse() to dto() and dtoOrFail().
*/
final class DtoThrowTypeExtension implements DynamicMethodThrowTypeExtension
{
public function __construct(
private CreateDtoMethodResolver $resolver,
private bool $implicitThrows,
) {
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getDeclaringClass()->is(Response::class)
&& $this->resolver->isDtoMethod($methodReflection);
}

public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
$createDtoThrowType = $this->resolver->resolve($methodCall, $scope)?->getThrowType();

if ($createDtoThrowType === null) {
return $methodReflection->getThrowType() ?? $this->implicitThrowType();
}

return TypeCombinator::union($createDtoThrowType, ...array_filter([$methodReflection->getThrowType()]));
}

/**
* PHPStan reads a null throw type as "throws nothing", so an undeclared
* createDtoFromResponse() has to fall back to what PHPStan infers on its own.
*/
private function implicitThrowType(): ?Type
{
return $this->implicitThrows ? new ObjectType(Throwable::class) : null;
}
}
12 changes: 12 additions & 0 deletions src/Traits/Connector/SendsRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ trait SendsRequests
/**
* Send a request synchronously
*
* @template TRequest of \Saloon\Http\Request
*
* @param TRequest $request
* @param callable(\Throwable, \Saloon\Http\Request): (bool)|null $handleRetry
* @return \Saloon\Http\Response<TRequest>
* @throws \Saloon\Exceptions\Request\FatalRequestException
* @throws \Saloon\Exceptions\Request\RequestException
*/
public function send(Request $request, ?MockClient $mockClient = null, ?callable $handleRetry = null): Response
{
Expand Down Expand Up @@ -162,7 +168,13 @@ public function sendAsync(Request $request, ?MockClient $mockClient = null): Pro
*
* @deprecated This method will be removed in Saloon v4. Please refer to the documentation to see connector or request-based retry functionality.
*
* @template TRequest of \Saloon\Http\Request
*
* @param TRequest $request
* @param callable(\Throwable, \Saloon\Http\Request): (bool)|null $handleRetry
* @return \Saloon\Http\Response<TRequest>
* @throws \Saloon\Exceptions\Request\FatalRequestException
* @throws \Saloon\Exceptions\Request\RequestException
*/
public function sendAndRetry(Request $request, int $tries, int $interval = 0, ?callable $handleRetry = null, bool $throw = true, ?MockClient $mockClient = null, bool $useExponentialBackoff = false): Response
{
Expand Down
4 changes: 4 additions & 0 deletions src/Traits/Request/HasConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public function createPendingRequest(?MockClient $mockClient = null): PendingReq

/**
* Send a request synchronously
*
* @return \Saloon\Http\Response<$this>
* @throws \Saloon\Exceptions\Request\FatalRequestException
* @throws \Saloon\Exceptions\Request\RequestException
*/
public function send(?MockClient $mockClient = null): Response
{
Expand Down
28 changes: 28 additions & 0 deletions tests/Fixtures/Requests/TypedDtoRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Saloon\Tests\Fixtures\Requests;

use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Tests\Fixtures\Data\User;

class TypedDtoRequest extends Request
{
protected Method $method = Method::GET;

public function resolveEndpoint(): string
{
return '/user';
}

/**
* @throws \JsonException
*/
public function createDtoFromResponse(Response $response): User
{
return User::fromSaloon($response);
}
}
Loading
Loading