Skip to content

Commit

Permalink
Merge pull request #387 from saloonphp/feature/debug-better
Browse files Browse the repository at this point in the history
Feature | Debug Helper Method
  • Loading branch information
Sammyjo20 authored Mar 20, 2024
2 parents 1830bf8 + 34d50f3 commit e669e85
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 15 deletions.
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

0 comments on commit e669e85

Please sign in to comment.