Skip to content

Commit e669e85

Browse files
authored
Merge pull request #387 from saloonphp/feature/debug-better
Feature | Debug Helper Method
2 parents 1830bf8 + 34d50f3 commit e669e85

File tree

6 files changed

+292
-15
lines changed

6 files changed

+292
-15
lines changed

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,17 @@
3535
"phpstan/phpstan": "^1.9",
3636
"saloonphp/xml-wrangler": "^1.1",
3737
"spatie/ray": "^1.33",
38-
"symfony/dom-crawler": "^6.0"
38+
"symfony/dom-crawler": "^6.0 || ^7.0",
39+
"symfony/var-dumper": "^6.3 || ^7.0"
3940
},
4041
"conflict": {
4142
"sammyjo20/saloon": "*"
4243
},
4344
"suggest": {
4445
"illuminate/collections": "Required for the response collect() method.",
4546
"symfony/dom-crawler": "Required for the response dom() method.",
46-
"saloonphp/xml-wrangler": "Required for the response xmlReader() method."
47+
"saloonphp/xml-wrangler": "Required for the response xmlReader() method.",
48+
"symfony/var-dumper": "Required for default debugging drivers."
4749
},
4850
"minimum-stability": "stable",
4951
"autoload": {

src/Helpers/Debugger.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Saloon\Helpers;
6+
7+
use Closure;
8+
use Saloon\Http\Response;
9+
use Saloon\Http\PendingRequest;
10+
use Psr\Http\Message\RequestInterface;
11+
use Psr\Http\Message\ResponseInterface;
12+
use Symfony\Component\VarDumper\VarDumper;
13+
14+
class Debugger
15+
{
16+
/**
17+
* Application "Die" handler.
18+
*
19+
* Only used for Saloon tests
20+
*/
21+
public static ?Closure $dieHandler = null;
22+
23+
/**
24+
* Debug a request with Symfony Var Dumper
25+
*/
26+
public static function symfonyRequestDebugger(PendingRequest $pendingRequest, RequestInterface $psrRequest): void
27+
{
28+
$headers = [];
29+
30+
foreach ($psrRequest->getHeaders() as $headerName => $value) {
31+
$headers[$headerName] = implode(';', $value);
32+
}
33+
34+
$className = explode('\\', $pendingRequest->getRequest()::class);
35+
$label = end($className);
36+
37+
VarDumper::dump([
38+
'connector' => $pendingRequest->getConnector()::class,
39+
'request' => $pendingRequest->getRequest()::class,
40+
'method' => $psrRequest->getMethod(),
41+
'uri' => (string)$psrRequest->getUri(),
42+
'headers' => $headers,
43+
'body' => (string)$psrRequest->getBody(),
44+
], 'Saloon Request (' . $label . ') ->');
45+
}
46+
47+
/**
48+
* Debug a response with Symfony Var Dumper
49+
*/
50+
public static function symfonyResponseDebugger(Response $response, ResponseInterface $psrResponse): void
51+
{
52+
$headers = [];
53+
54+
foreach ($psrResponse->getHeaders() as $headerName => $value) {
55+
$headers[$headerName] = implode(';', $value);
56+
}
57+
58+
$className = explode('\\', $response->getRequest()::class);
59+
$label = end($className);
60+
61+
VarDumper::dump([
62+
'status' => $response->status(),
63+
'headers' => $headers,
64+
'body' => $response->body(),
65+
], 'Saloon Response (' . $label . ') ->');
66+
}
67+
68+
/**
69+
* Kill the application
70+
*
71+
* This is a method as it can be easily mocked during tests
72+
*/
73+
public static function die(): void
74+
{
75+
$handler = self::$dieHandler ?? static fn () => exit(1);
76+
77+
$handler();
78+
}
79+
}

src/Http/Response.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ public function getSenderException(): ?Throwable
195195
*
196196
* @param array-key|null $key
197197
* @return ($key is null ? array<array-key, mixed> : mixed)
198-
* @throws \JsonException
199198
*/
200199
public function json(string|int|null $key = null, mixed $default = null): mixed
201200
{
@@ -217,7 +216,6 @@ public function json(string|int|null $key = null, mixed $default = null): mixed
217216
*
218217
* @param array-key|null $key
219218
* @return ($key is null ? array<array-key, mixed> : mixed)
220-
* @throws \JsonException
221219
*/
222220
public function array(int|string|null $key = null, mixed $default = null): mixed
223221
{
@@ -226,8 +224,6 @@ public function array(int|string|null $key = null, mixed $default = null): mixed
226224

227225
/**
228226
* Get the JSON decoded body of the response as an object.
229-
*
230-
* @throws \JsonException
231227
*/
232228
public function object(): object
233229
{
@@ -273,7 +269,6 @@ public function xmlReader(): XmlReader
273269
*
274270
* @param array-key|null $key
275271
* @return \Illuminate\Support\Collection<array-key, mixed>
276-
* @throws \JsonException
277272
*/
278273
public function collect(string|int|null $key = null): Collection
279274
{
@@ -309,8 +304,6 @@ public function dto(): mixed
309304

310305
/**
311306
* Convert the response into a DTO or throw a LogicException if the response failed
312-
*
313-
* @throws LogicException
314307
*/
315308
public function dtoOrFail(): mixed
316309
{

src/Traits/HasDebugging.php

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,37 @@
66

77
use Saloon\Http\Response;
88
use Saloon\Enums\PipeOrder;
9+
use Saloon\Helpers\Debugger;
910
use Saloon\Http\PendingRequest;
1011

1112
trait HasDebugging
1213
{
1314
/**
1415
* Register a request debugger
1516
*
16-
* @param callable(\Saloon\Http\PendingRequest, \Psr\Http\Message\RequestInterface): void $onRequest
17+
* Leave blank for a default debugger (requires symfony/var-dump)
18+
*
19+
* @param callable(\Saloon\Http\PendingRequest, \Psr\Http\Message\RequestInterface): void|null $onRequest
1720
* @return $this
1821
*/
19-
public function debugRequest(callable $onRequest): static
22+
public function debugRequest(?callable $onRequest = null, bool $die = false): static
2023
{
24+
// When the user has not specified a callable to debug with, we will use this default
25+
// debugging driver. This will use symfony/var-dumper to display a nice output to
26+
// the user's screen of the request.
27+
28+
$onRequest ??= Debugger::symfonyRequestDebugger(...);
29+
30+
// Register the middleware - we will use PipeOrder::FIRST to ensure that the response
31+
// is shown before it is modified by the user's middleware.
32+
2133
$this->middleware()->onRequest(
22-
callable: static function (PendingRequest $pendingRequest) use ($onRequest): void {
34+
callable: static function (PendingRequest $pendingRequest) use ($onRequest, $die): void {
2335
$onRequest($pendingRequest, $pendingRequest->createPsrRequest());
36+
37+
if ($die) {
38+
Debugger::die();
39+
}
2440
},
2541
order: PipeOrder::LAST
2642
);
@@ -31,18 +47,50 @@ public function debugRequest(callable $onRequest): static
3147
/**
3248
* Register a response debugger
3349
*
34-
* @param callable(\Saloon\Http\Response, \Psr\Http\Message\ResponseInterface): void $onResponse
50+
* Leave blank for a default debugger (requires symfony/var-dump)
51+
*
52+
* @param callable(\Saloon\Http\Response, \Psr\Http\Message\ResponseInterface): void|null $onResponse
3553
* @return $this
3654
*/
37-
public function debugResponse(callable $onResponse): static
55+
public function debugResponse(?callable $onResponse = null, bool $die = false): static
3856
{
57+
// When the user has not specified a callable to debug with, we will use this default
58+
// debugging driver. This will use symfony/var-dumper to display a nice output to
59+
// the user's screen of the response.
60+
61+
$onResponse ??= Debugger::symfonyResponseDebugger(...);
62+
63+
// Register the middleware - we will use PipeOrder::FIRST to ensure that the response
64+
// is shown before it is modified by the user's middleware.
65+
3966
$this->middleware()->onResponse(
40-
callable: static function (Response $response) use ($onResponse): void {
67+
callable: static function (Response $response) use ($onResponse, $die): void {
4168
$onResponse($response, $response->getPsrResponse());
69+
70+
if ($die) {
71+
Debugger::die();
72+
}
4273
},
4374
order: PipeOrder::FIRST
4475
);
4576

4677
return $this;
4778
}
79+
80+
/**
81+
* Dump a pretty output of the request and response.
82+
*
83+
* This is useful if you would like to see the request right before it is sent
84+
* to inspect the body and URI to ensure it is correct. You can also inspect
85+
* the raw response as it comes back.
86+
*
87+
* Note that any changes made to the PSR request by the sender will not be
88+
* reflected by this output.
89+
*
90+
* Requires symfony/var-dumper
91+
*/
92+
public function debug(bool $die = false): static
93+
{
94+
return $this->debugRequest()->debugResponse(die: $die);
95+
}
4896
}

tests/Feature/DebugTest.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
declare(strict_types=1);
44

55
use Saloon\Http\Response;
6+
use Saloon\Helpers\Debugger;
67
use Saloon\Http\PendingRequest;
78
use Saloon\Http\Faking\MockClient;
89
use Saloon\Http\Faking\MockResponse;
910
use Psr\Http\Message\RequestInterface;
1011
use Psr\Http\Message\ResponseInterface;
12+
use Symfony\Component\VarDumper\VarDumper;
1113
use Saloon\Tests\Fixtures\Requests\UserRequest;
1214
use Saloon\Tests\Fixtures\Connectors\TestConnector;
1315
use Saloon\Tests\Fixtures\Requests\AlwaysThrowRequest;
@@ -133,3 +135,135 @@
133135
expect($middlewareCount)->toBe(2);
134136
}
135137
});
138+
139+
test('the default debugRequest driver will dump an output using symfony var-dumper', function () {
140+
$output = fopen('php://memory', 'rwb+');
141+
142+
VarDumper::setHandler(getCustomVarDump($output));
143+
144+
$connector = new TestConnector;
145+
146+
$connector->withMockClient(new MockClient([
147+
new MockResponse(['name' => 'Sam'], 500),
148+
]));
149+
150+
$connector->debugRequest()->send(new UserRequest);
151+
152+
VarDumper::setHandler(null);
153+
154+
rewind($output);
155+
156+
$output = stream_get_contents($output);
157+
158+
$expected = <<<END
159+
Saloon Request (UserRequest) -> array:6 [
160+
"connector" => "Saloon\Tests\Fixtures\Connectors\TestConnector"
161+
"request" => "Saloon\Tests\Fixtures\Requests\UserRequest"
162+
"method" => "GET"
163+
"uri" => "https://tests.saloon.dev/api/user"
164+
"headers" => array:2 [
165+
"Host" => "tests.saloon.dev"
166+
"Accept" => "application/json"
167+
]
168+
"body" => ""
169+
]\n
170+
END;
171+
172+
expect($output)->toEqual(str_replace("\r\n", "\n", $expected));
173+
});
174+
175+
test('the default debugResponse driver will dump an output using symfony var-dumper', function () {
176+
$output = fopen('php://memory', 'rwb+');
177+
178+
VarDumper::setHandler(getCustomVarDump($output));
179+
180+
$connector = new TestConnector;
181+
182+
$connector->withMockClient(new MockClient([
183+
new MockResponse(['name' => 'Sam'], 500),
184+
]));
185+
186+
$connector->debugResponse()->send(new UserRequest);
187+
188+
VarDumper::setHandler(null);
189+
190+
rewind($output);
191+
192+
$output = stream_get_contents($output);
193+
194+
$expected = <<<END
195+
Saloon Response (UserRequest) -> array:3 [
196+
"status" => 500
197+
"headers" => []
198+
"body" => "{"name":"Sam"}"
199+
]\n
200+
END;
201+
202+
expect($output)->toEqual(str_replace("\r\n", "\n", $expected));
203+
});
204+
205+
test('the debug method will output both request and response at the same time', function () {
206+
$output = fopen('php://memory', 'rwb+');
207+
208+
VarDumper::setHandler(getCustomVarDump($output));
209+
210+
$connector = new TestConnector;
211+
212+
$connector->withMockClient(new MockClient([
213+
new MockResponse(['name' => 'Sam'], 500),
214+
]));
215+
216+
$connector->debug()->send(new UserRequest);
217+
218+
VarDumper::setHandler(null);
219+
220+
rewind($output);
221+
222+
$output = stream_get_contents($output);
223+
224+
$expected = <<<END
225+
Saloon Request (UserRequest) -> array:6 [
226+
"connector" => "Saloon\Tests\Fixtures\Connectors\TestConnector"
227+
"request" => "Saloon\Tests\Fixtures\Requests\UserRequest"
228+
"method" => "GET"
229+
"uri" => "https://tests.saloon.dev/api/user"
230+
"headers" => array:2 [
231+
"Host" => "tests.saloon.dev"
232+
"Accept" => "application/json"
233+
]
234+
"body" => ""
235+
]
236+
Saloon Response (UserRequest) -> array:3 [
237+
"status" => 500
238+
"headers" => []
239+
"body" => "{"name":"Sam"}"
240+
]\n
241+
END;
242+
243+
expect($output)->toEqual(str_replace("\r\n", "\n", $expected));
244+
});
245+
246+
test('the debug method can kill the application', function () {
247+
$killed = false;
248+
249+
$output = fopen('php://memory', 'rwb+');
250+
251+
VarDumper::setHandler(getCustomVarDump($output));
252+
253+
Debugger::$dieHandler = static function () use (&$killed) {
254+
$killed = true;
255+
};
256+
257+
$connector = new TestConnector;
258+
259+
$connector->withMockClient(new MockClient([
260+
new MockResponse(['name' => 'Sam'], 500),
261+
]));
262+
263+
$connector->debug(die: true)->send(new UserRequest);
264+
265+
VarDumper::setHandler(null);
266+
Debugger::$dieHandler = null;
267+
268+
expect($killed)->toBeTrue();
269+
});

0 commit comments

Comments
 (0)