Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature | Debug Helper Method #387

Merged
merged 23 commits into from
Mar 20, 2024
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
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@
"phpstan/phpstan": "^1.9",
"saloonphp/xml-wrangler": "^1.1",
"spatie/ray": "^1.33",
"symfony/dom-crawler": "^6.0"
"symfony/dom-crawler": "^6.0 || ^7.0",
"symfony/var-dumper": "^6.3 || ^7.0"
},
"conflict": {
"sammyjo20/saloon": "*"
},
"suggest": {
"illuminate/collections": "Required for the response collect() method.",
"symfony/dom-crawler": "Required for the response dom() method.",
"saloonphp/xml-wrangler": "Required for the response xmlReader() method."
"saloonphp/xml-wrangler": "Required for the response xmlReader() method.",
"symfony/var-dumper": "Required for default debugging drivers."
},
"minimum-stability": "stable",
"autoload": {
Expand Down
79 changes: 79 additions & 0 deletions src/Helpers/Debugger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

use Closure;
use Saloon\Http\Response;
use Saloon\Http\PendingRequest;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\VarDumper\VarDumper;

class Debugger
{
/**
* Application "Die" handler.
*
* Only used for Saloon tests
*/
public static ?Closure $dieHandler = null;

/**
* Debug a request with Symfony Var Dumper
*/
public static function symfonyRequestDebugger(PendingRequest $pendingRequest, RequestInterface $psrRequest): void
{
$headers = [];

foreach ($psrRequest->getHeaders() as $headerName => $value) {
$headers[$headerName] = implode(';', $value);
}

$className = explode('\\', $pendingRequest->getRequest()::class);
$label = end($className);

VarDumper::dump([
'connector' => $pendingRequest->getConnector()::class,
'request' => $pendingRequest->getRequest()::class,
'method' => $psrRequest->getMethod(),
'uri' => (string)$psrRequest->getUri(),
'headers' => $headers,
'body' => (string)$psrRequest->getBody(),
], 'Saloon Request (' . $label . ') ->');
}

/**
* Debug a response with Symfony Var Dumper
*/
public static function symfonyResponseDebugger(Response $response, ResponseInterface $psrResponse): void
{
$headers = [];

foreach ($psrResponse->getHeaders() as $headerName => $value) {
$headers[$headerName] = implode(';', $value);
}

$className = explode('\\', $response->getRequest()::class);
$label = end($className);

VarDumper::dump([
'status' => $response->status(),
'headers' => $headers,
'body' => $response->body(),
], 'Saloon Response (' . $label . ') ->');
}

/**
* Kill the application
*
* This is a method as it can be easily mocked during tests
*/
public static function die(): void
{
$handler = self::$dieHandler ?? static fn () => exit(1);

$handler();
}
}
7 changes: 0 additions & 7 deletions src/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ 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 @@ -217,7 +216,6 @@ 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 @@ -226,8 +224,6 @@ public function array(int|string|null $key = null, mixed $default = null): mixed

/**
* Get the JSON decoded body of the response as an object.
*
* @throws \JsonException
*/
public function object(): object
{
Expand Down Expand Up @@ -273,7 +269,6 @@ 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 @@ -309,8 +304,6 @@ 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
60 changes: 54 additions & 6 deletions src/Traits/HasDebugging.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,37 @@

use Saloon\Http\Response;
use Saloon\Enums\PipeOrder;
use Saloon\Helpers\Debugger;
use Saloon\Http\PendingRequest;

trait HasDebugging
{
/**
* Register a request debugger
*
* @param callable(\Saloon\Http\PendingRequest, \Psr\Http\Message\RequestInterface): void $onRequest
* Leave blank for a default debugger (requires symfony/var-dump)
*
* @param callable(\Saloon\Http\PendingRequest, \Psr\Http\Message\RequestInterface): void|null $onRequest
* @return $this
*/
public function debugRequest(callable $onRequest): static
public function debugRequest(?callable $onRequest = null, bool $die = false): static
{
// When the user has not specified a callable to debug with, we will use this default
// debugging driver. This will use symfony/var-dumper to display a nice output to
// the user's screen of the request.

$onRequest ??= Debugger::symfonyRequestDebugger(...);

// Register the middleware - we will use PipeOrder::FIRST to ensure that the response
// is shown before it is modified by the user's middleware.

$this->middleware()->onRequest(
callable: static function (PendingRequest $pendingRequest) use ($onRequest): void {
callable: static function (PendingRequest $pendingRequest) use ($onRequest, $die): void {
$onRequest($pendingRequest, $pendingRequest->createPsrRequest());

if ($die) {
Debugger::die();
}
},
order: PipeOrder::LAST
);
Expand All @@ -31,18 +47,50 @@ public function debugRequest(callable $onRequest): static
/**
* Register a response debugger
*
* @param callable(\Saloon\Http\Response, \Psr\Http\Message\ResponseInterface): void $onResponse
* Leave blank for a default debugger (requires symfony/var-dump)
*
* @param callable(\Saloon\Http\Response, \Psr\Http\Message\ResponseInterface): void|null $onResponse
* @return $this
*/
public function debugResponse(callable $onResponse): static
public function debugResponse(?callable $onResponse = null, bool $die = false): static
{
// When the user has not specified a callable to debug with, we will use this default
// debugging driver. This will use symfony/var-dumper to display a nice output to
// the user's screen of the response.

$onResponse ??= Debugger::symfonyResponseDebugger(...);

// Register the middleware - we will use PipeOrder::FIRST to ensure that the response
// is shown before it is modified by the user's middleware.

$this->middleware()->onResponse(
callable: static function (Response $response) use ($onResponse): void {
callable: static function (Response $response) use ($onResponse, $die): void {
$onResponse($response, $response->getPsrResponse());

if ($die) {
Debugger::die();
}
},
order: PipeOrder::FIRST
);

return $this;
}

/**
* Dump a pretty output of the request and response.
*
* This is useful if you would like to see the request right before it is sent
* to inspect the body and URI to ensure it is correct. You can also inspect
* the raw response as it comes back.
*
* Note that any changes made to the PSR request by the sender will not be
* reflected by this output.
*
* Requires symfony/var-dumper
*/
public function debug(bool $die = false): static
{
return $this->debugRequest()->debugResponse(die: $die);
}
}
134 changes: 134 additions & 0 deletions tests/Feature/DebugTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
declare(strict_types=1);

use Saloon\Http\Response;
use Saloon\Helpers\Debugger;
use Saloon\Http\PendingRequest;
use Saloon\Http\Faking\MockClient;
use Saloon\Http\Faking\MockResponse;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\VarDumper\VarDumper;
use Saloon\Tests\Fixtures\Requests\UserRequest;
use Saloon\Tests\Fixtures\Connectors\TestConnector;
use Saloon\Tests\Fixtures\Requests\AlwaysThrowRequest;
Expand Down Expand Up @@ -133,3 +135,135 @@
expect($middlewareCount)->toBe(2);
}
});

test('the default debugRequest driver will dump an output using symfony var-dumper', function () {
$output = fopen('php://memory', 'rwb+');

VarDumper::setHandler(getCustomVarDump($output));

$connector = new TestConnector;

$connector->withMockClient(new MockClient([
new MockResponse(['name' => 'Sam'], 500),
]));

$connector->debugRequest()->send(new UserRequest);

VarDumper::setHandler(null);

rewind($output);

$output = stream_get_contents($output);

$expected = <<<END
Saloon Request (UserRequest) -> array:6 [
"connector" => "Saloon\Tests\Fixtures\Connectors\TestConnector"
"request" => "Saloon\Tests\Fixtures\Requests\UserRequest"
"method" => "GET"
"uri" => "https://tests.saloon.dev/api/user"
"headers" => array:2 [
"Host" => "tests.saloon.dev"
"Accept" => "application/json"
]
"body" => ""
]\n
END;

expect($output)->toEqual(str_replace("\r\n", "\n", $expected));
});

test('the default debugResponse driver will dump an output using symfony var-dumper', function () {
$output = fopen('php://memory', 'rwb+');

VarDumper::setHandler(getCustomVarDump($output));

$connector = new TestConnector;

$connector->withMockClient(new MockClient([
new MockResponse(['name' => 'Sam'], 500),
]));

$connector->debugResponse()->send(new UserRequest);

VarDumper::setHandler(null);

rewind($output);

$output = stream_get_contents($output);

$expected = <<<END
Saloon Response (UserRequest) -> array:3 [
"status" => 500
"headers" => []
"body" => "{"name":"Sam"}"
]\n
END;

expect($output)->toEqual(str_replace("\r\n", "\n", $expected));
});

test('the debug method will output both request and response at the same time', function () {
$output = fopen('php://memory', 'rwb+');

VarDumper::setHandler(getCustomVarDump($output));

$connector = new TestConnector;

$connector->withMockClient(new MockClient([
new MockResponse(['name' => 'Sam'], 500),
]));

$connector->debug()->send(new UserRequest);

VarDumper::setHandler(null);

rewind($output);

$output = stream_get_contents($output);

$expected = <<<END
Saloon Request (UserRequest) -> array:6 [
"connector" => "Saloon\Tests\Fixtures\Connectors\TestConnector"
"request" => "Saloon\Tests\Fixtures\Requests\UserRequest"
"method" => "GET"
"uri" => "https://tests.saloon.dev/api/user"
"headers" => array:2 [
"Host" => "tests.saloon.dev"
"Accept" => "application/json"
]
"body" => ""
]
Saloon Response (UserRequest) -> array:3 [
"status" => 500
"headers" => []
"body" => "{"name":"Sam"}"
]\n
END;

expect($output)->toEqual(str_replace("\r\n", "\n", $expected));
});

test('the debug method can kill the application', function () {
$killed = false;

$output = fopen('php://memory', 'rwb+');

VarDumper::setHandler(getCustomVarDump($output));

Debugger::$dieHandler = static function () use (&$killed) {
$killed = true;
};

$connector = new TestConnector;

$connector->withMockClient(new MockClient([
new MockResponse(['name' => 'Sam'], 500),
]));

$connector->debug(die: true)->send(new UserRequest);

VarDumper::setHandler(null);
Debugger::$dieHandler = null;

expect($killed)->toBeTrue();
});
Loading
Loading