diff --git a/src/Http/Response.php b/src/Http/Response.php index 2d84ae3a..97e4bd4b 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -76,7 +76,7 @@ class Response /** * Create a new response instance. */ - public function __construct(ResponseInterface $psrResponse, PendingRequest $pendingRequest, RequestInterface $psrRequest, Throwable $senderException = null) + public function __construct(ResponseInterface $psrResponse, PendingRequest $pendingRequest, RequestInterface $psrRequest, ?Throwable $senderException = null) { $this->psrRequest = $psrRequest; $this->psrResponse = $psrResponse; @@ -193,7 +193,7 @@ public function getSenderException(): ?Throwable /** * Get the JSON decoded body of the response as an array or scalar value. * - * @param array-key|null $key + * @param array-key|null $key * @return ($key is null ? array : mixed) */ public function json(string|int|null $key = null, mixed $default = null): mixed @@ -214,7 +214,7 @@ public function json(string|int|null $key = null, mixed $default = null): mixed * * Alias of json() * - * @param array-key|null $key + * @param array-key|null $key * @return ($key is null ? array : mixed) */ public function array(int|string|null $key = null, mixed $default = null): mixed @@ -265,9 +265,10 @@ public function xmlReader(): XmlReader * Get the JSON decoded body of the response as a collection. * * Requires Laravel Collections (composer require illuminate/collections) + * * @see https://github.com/illuminate/collections * - * @param array-key|null $key + * @param array-key|null $key * @return \Illuminate\Support\Collection */ public function collect(string|int|null $key = null): Collection @@ -287,14 +288,24 @@ public function collect(string|int|null $key = null): Collection /** * Cast the response to a DTO. + * + * @template T of object + * + * @return ($type is class-string ? T : object) */ - public function dto(): mixed + public function dto(?string $type = null): mixed { $request = $this->pendingRequest->getRequest(); $connector = $this->pendingRequest->getConnector(); $dataObject = $request->createDtoFromResponse($this) ?? $connector->createDtoFromResponse($this); + if (! is_null($type) && ! is_null($dataObject) && $dataObject::class !== $type) { + throw new InvalidArgumentException( + message: sprintf('The class-string provided [%s] must match the class-string returned by the connector/request [%s].', $type, $dataObject::class), + ); + } + if ($dataObject instanceof WithResponse) { $dataObject->setResponse($this); } @@ -304,14 +315,19 @@ public function dto(): mixed /** * Convert the response into a DTO or throw a LogicException if the response failed + * + * @template T of object + * + * @param class-string|null $type + * @return ($type is class-string ? T : object) */ - public function dtoOrFail(): mixed + public function dtoOrFail(?string $type = null): mixed { if ($this->failed()) { throw new LogicException('Unable to create data transfer object as the response has failed.', 0, $this->toException()); } - return $this->dto(); + return $this->dto($type); } /** @@ -394,7 +410,7 @@ public function serverError(): bool /** * Execute the given callback if there was a server or client error. * - * @param callable($this): (void) $callback + * @param callable($this): (void) $callback * @return $this */ public function onError(callable $callback): static @@ -454,6 +470,7 @@ protected function createException(): Throwable * Throw an exception if a server or client error occurred. * * @return $this + * * @throws \Throwable */ public function throw(): static @@ -498,7 +515,7 @@ public function getRawStream(): mixed /** * Save the body to a file * - * @param string|resource $resourceOrPath + * @param string|resource $resourceOrPath */ public function saveBodyToFile(mixed $resourceOrPath, bool $closeResource = true): void { diff --git a/tests/Feature/DataObjectWrapperTest.php b/tests/Feature/DataObjectWrapperTest.php index a33fe83d..554519d6 100644 --- a/tests/Feature/DataObjectWrapperTest.php +++ b/tests/Feature/DataObjectWrapperTest.php @@ -7,6 +7,7 @@ use Saloon\Tests\Fixtures\Data\User; use Saloon\Contracts\DataObjects\WithResponse; use Saloon\Tests\Fixtures\Requests\DTORequest; +use Saloon\Tests\Fixtures\Requests\UserRequest; use Saloon\Tests\Fixtures\Data\UserWithResponse; use Saloon\Tests\Fixtures\Requests\DTOWithResponseRequest; @@ -16,7 +17,8 @@ ]); $response = connector()->send(new DTORequest, $mockClient); - $dto = $response->dto(); + + $dto = $response->dto(User::class); expect($dto)->toBeInstanceOf(User::class); expect($dto)->not->toBeInstanceOf(WithResponse::class); @@ -37,3 +39,28 @@ expect($dto)->toBeInstanceOf(WithResponse::class); expect($dto->getResponse())->toBe($response); }); + +test('if a dto type is provided and the class does not support a dto null is still returned', function () { + $mockClient = new MockClient([ + new MockResponse(['name' => 'Sammyjo20', 'actual_name' => 'Sam', 'twitter' => '@carre_sam']), + ]); + + $response = connector()->send(new UserRequest, $mockClient); + + $dto = $response->dto(User::class); + + expect($dto)->toBeNull(); +}); + +test('if a dto type is provided and the class returned doesnt match an exception is thrown', function () { + $mockClient = new MockClient([ + new MockResponse(['name' => 'Sammyjo20', 'actual_name' => 'Sam', 'twitter' => '@carre_sam']), + ]); + + $response = connector()->send(new DTORequest, $mockClient); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The class-string provided [Saloon\Tests\Fixtures\Data\UserWithResponse] must match the class-string returned by the connector/request [Saloon\Tests\Fixtures\Data\User].'); + + $response->dto(UserWithResponse::class); +});