Skip to content

Commit 2a7074b

Browse files
committed
rfc7807 server implementation
1 parent f1e85a5 commit 2a7074b

File tree

5 files changed

+147
-16
lines changed

5 files changed

+147
-16
lines changed

.github/README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,28 +278,57 @@ you can easily get this transformer from DI, without any new configuration (stan
278278

279279
/**
280280
* @var \SimpleAsFuck\ApiToolkit\Service\Config\Repository $configRepository
281+
* @var \Psr\Log\LoggerInterface $logger
281282
*/
282283

283284
try {
284285
// some breakable logic
285286
}
286287
catch(\SimpleAsFuck\ApiToolkit\Model\Server\ApiException $exception) {
287-
//catch(\Symfony\Component\HttpKernel\Exception\HttpException $exception) {
288+
// exception message for logging or debugging, you SHOULD log this, so you know WTF is going wrong
289+
$message = $exception->getMessage();
290+
if ($exception->getInternalMessage() !== null) {
291+
$message .= ', '.$exception->getInternalMessage();
292+
}
293+
$logger->error($message, [
294+
'type' => $exception->getType(),
295+
'status' => $exception->getCode(),
296+
'instance' => $exception->getInstance(),
297+
...$exception->getExtensions(),
298+
]);
299+
288300
$response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson(
289301
//$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson(
290302
$exception,
291-
// transformer will convert exception in to json object with message property with original exception message
303+
// transformer will convert exception in to https://datatracker.ietf.org/doc/html/rfc7807 json object with message and all another extensions
292304
new \SimpleAsFuck\ApiToolkit\Service\Server\ApiExceptionTransformer(),
293305
$exception->getStatusCode()
294306
);
295307
}
308+
// if you use Symfony Http Exceptions you can use HttpExceptionTransformer
309+
catch (\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception) {
310+
$logger->error($exception->getMessage(), ['status' => $exception->getStatusCode()]);
311+
312+
$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson(
313+
$exception,
314+
// transformer will convert exception into json object
315+
// with message property contains message from http exception
316+
// and https://datatracker.ietf.org/doc/html/rfc7807#section-3.1 status property
317+
new \SimpleAsFuck\ApiToolkit\Service\Symfony\HttpExceptionTransformer(),
318+
$exception->getStatusCode()
319+
);
320+
}
296321
catch (\Throwable $exception) {
322+
$logger->error($exception->getMessage());
323+
297324
$response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson(
298325
//$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson(
299326
$exception,
300-
// transformer will convert exception in to json object with message property
327+
// transformer will convert exception in to json object
301328
// if application has turned off debug, message property contain only "Internal server error"
302329
// but with enabled debug message contains exception type, message, file and line where was exception thrown
330+
// with enabled debug json object also contains trace property with exception stacktrace
331+
// and json object contains https://datatracker.ietf.org/doc/html/rfc7807#section-3.1 status property always with 500 http code
303332
new \SimpleAsFuck\ApiToolkit\Service\Server\ExceptionTransformer($configRepository),
304333
\Kayex\HttpCodes::HTTP_INTERNAL_SERVER_ERROR
305334
);

src/Model/Server/ApiException.php

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,85 @@
88

99
class ApiException extends \RuntimeException
1010
{
11-
/** @var int<100,505> */
12-
private int $httpCode;
11+
/**
12+
* @param non-empty-string $message https://datatracker.ietf.org/doc/html/rfc7807 extension available to server and client for logging or debugging purposes MUST contain only English message
13+
* @param int<100,505> $code HTTP status
14+
* @param non-empty-string|null $type https://datatracker.ietf.org/doc/html/rfc7807#section-3.1 type
15+
* @param non-empty-string|null $title https://datatracker.ietf.org/doc/html/rfc7807#section-3.1 title message for end user
16+
* @param non-empty-string|null $detail https://datatracker.ietf.org/doc/html/rfc7807#section-3.1 detail for end user
17+
* @param non-empty-string|null $instance https://datatracker.ietf.org/doc/html/rfc7807#section-3.1 instance
18+
* @param array<literal-string, mixed> $extensions https://datatracker.ietf.org/doc/html/rfc7807 extensions all values MUST be json serializable
19+
* @param non-empty-string|null $internalMessage available only to server for logging or debugging purposes MUST NOT leave server environment
20+
*/
21+
public function __construct(
22+
string $message,
23+
int $code = HttpCodes::HTTP_INTERNAL_SERVER_ERROR,
24+
private readonly ?string $type = null,
25+
private readonly ?string $title = null,
26+
private readonly ?string $detail = null,
27+
private readonly ?string $instance = null,
28+
private readonly array $extensions = [],
29+
private readonly ?string $internalMessage = null,
30+
\Throwable $previous = null
31+
) {
32+
parent::__construct($message, $code, $previous);
33+
}
34+
35+
/**
36+
* @return non-empty-string|null
37+
*/
38+
public function getType(): ?string
39+
{
40+
return $this->type;
41+
}
42+
43+
/**
44+
* @return non-empty-string|null
45+
*/
46+
public function getTitle(): ?string
47+
{
48+
return $this->title;
49+
}
50+
51+
/**
52+
* @return non-empty-string|null
53+
*/
54+
public function getDetail(): ?string
55+
{
56+
return $this->detail;
57+
}
58+
59+
/**
60+
* @return non-empty-string|null
61+
*/
62+
public function getInstance(): ?string
63+
{
64+
return $this->instance;
65+
}
66+
67+
/**
68+
* @return array<literal-string, mixed>
69+
*/
70+
public function getExtensions(): array
71+
{
72+
return $this->extensions;
73+
}
1374

1475
/**
15-
* @param int<100,505> $httpCode
76+
* @return non-empty-string|null
1677
*/
17-
public function __construct(string $message, int $httpCode = HttpCodes::HTTP_INTERNAL_SERVER_ERROR, \Throwable $previous = null)
78+
public function getInternalMessage(): ?string
1879
{
19-
parent::__construct($message, $httpCode, $previous);
20-
$this->httpCode = $httpCode;
80+
return $this->internalMessage;
2181
}
2282

2383
/**
84+
* @deprecated use $this->getCode()
2485
* @return int<100,505>
2586
*/
2687
public function getStatusCode(): int
2788
{
28-
return $this->httpCode;
89+
/** @var int<100,505> */
90+
return $this->getCode();
2991
}
3092
}

src/Service/Server/ApiExceptionTransformer.php

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

77
use SimpleAsFuck\ApiToolkit\Model\Server\ApiException;
88
use SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer;
9-
use Symfony\Component\HttpKernel\Exception\HttpException;
109

1110
/**
12-
* @implements Transformer<ApiException|HttpException>
11+
* @implements Transformer<ApiException>
1312
*/
1413
class ApiExceptionTransformer implements Transformer
1514
{
1615
/**
17-
* @param ApiException|HttpException $transformed
16+
* @param ApiException $transformed
1817
*/
1918
public function toApi($transformed): \stdClass
2019
{
21-
$responseData = new \stdClass();
22-
$responseData->message = $transformed->getMessage();
20+
$responseData = ['message' => $transformed->getMessage()];
21+
if ($transformed->getType() !== null) {
22+
$responseData['type'] = $transformed->getType();
23+
}
24+
if ($transformed->getTitle() !== null) {
25+
$responseData['title'] = $transformed->getTitle();
26+
}
27+
$responseData['status'] = $transformed->getCode();
28+
if ($transformed->getDetail() !== null) {
29+
$responseData['detail'] = $transformed->getDetail();
30+
}
31+
if ($transformed->getInstance() !== null) {
32+
$responseData['instance'] = $transformed->getInstance();
33+
}
2334

24-
return $responseData;
35+
$responseData = [...$responseData, ...$transformed->getExtensions()];
36+
37+
return (object) $responseData;
2538
}
2639
}

src/Service/Server/ExceptionTransformer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace SimpleAsFuck\ApiToolkit\Service\Server;
66

7+
use Kayex\HttpCodes;
78
use SimpleAsFuck\ApiToolkit\Service\Config\Repository;
89
use SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer;
910

@@ -26,6 +27,7 @@ public function toApi($transformed): \stdClass
2627
{
2728
$responseData = new \stdClass();
2829
$responseData->message = 'Internal server error';
30+
$responseData->status = HttpCodes::HTTP_INTERNAL_SERVER_ERROR;
2931
if ($this->configRepository->getServerConfig()->debug()) {
3032
$responseData->message = 'Exception ('.\get_class($transformed).') message: \''.$transformed->getMessage().'\' from: '.$transformed->getFile().':'.$transformed->getLine();
3133
$responseData->trace = array_map(fn (array $item): string => ($item['file'] ?? '-').':'.($item['line'] ?? '-'), $transformed->getTrace());
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleAsFuck\ApiToolkit\Service\Symfony;
6+
7+
use SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer;
8+
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
9+
10+
/**
11+
* @implements Transformer<HttpExceptionInterface>
12+
*/
13+
class HttpExceptionTransformer implements Transformer
14+
{
15+
/**
16+
* @param HttpExceptionInterface $transformed
17+
*/
18+
public function toApi($transformed): \stdClass
19+
{
20+
return (object) [
21+
'message' => $transformed->getMessage(),
22+
'status' => $transformed->getStatusCode(),
23+
];
24+
}
25+
}

0 commit comments

Comments
 (0)