Skip to content

Commit ce1d124

Browse files
committed
Add strategy to extract the status code from errors
One of the main differentiators of this package is the usage of marker interfaces to perform the translation from error/exception to HTTP responses. This provides the extension point for that operation and a default strategy using a translation map (class/interface -> HTTP status code).
1 parent 5aec88a commit ce1d124

File tree

3 files changed

+185
-0
lines changed

3 files changed

+185
-0
lines changed

src/StatusCodeExtractionStrategy.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Lcobucci\ErrorHandling;
5+
6+
use Throwable;
7+
8+
interface StatusCodeExtractionStrategy
9+
{
10+
public function extractStatusCode(Throwable $error): int;
11+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Lcobucci\ErrorHandling\StatusCodeExtractionStrategy;
5+
6+
use Fig\Http\Message\StatusCodeInterface;
7+
use Lcobucci\ErrorHandling\Problem;
8+
use Lcobucci\ErrorHandling\StatusCodeExtractionStrategy;
9+
use Throwable;
10+
11+
final class ClassMap implements StatusCodeExtractionStrategy
12+
{
13+
private const DEFAULT_MAP = [
14+
Problem\InvalidRequest::class => StatusCodeInterface::STATUS_BAD_REQUEST,
15+
Problem\AuthorizationRequired::class => StatusCodeInterface::STATUS_UNAUTHORIZED,
16+
Problem\Forbidden::class => StatusCodeInterface::STATUS_FORBIDDEN,
17+
Problem\ResourceNotFound::class => StatusCodeInterface::STATUS_NOT_FOUND,
18+
Problem\Conflict::class => StatusCodeInterface::STATUS_CONFLICT,
19+
Problem\ResourceNoLongerAvailable::class => StatusCodeInterface::STATUS_GONE,
20+
Problem\UnprocessableRequest::class => StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY,
21+
Problem\ServiceUnavailable::class => StatusCodeInterface::STATUS_SERVICE_UNAVAILABLE,
22+
];
23+
24+
/**
25+
* @var array<string, int>
26+
*/
27+
private $conversionMap;
28+
29+
/**
30+
* @param array<string, int> $conversionMap
31+
*/
32+
public function __construct(array $conversionMap = self::DEFAULT_MAP)
33+
{
34+
$this->conversionMap = $conversionMap;
35+
}
36+
37+
public function extractStatusCode(Throwable $error): int
38+
{
39+
foreach ($this->conversionMap as $class => $code) {
40+
if ($error instanceof $class) {
41+
return $code;
42+
}
43+
}
44+
45+
return $error->getCode() ?: StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR;
46+
}
47+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Lcobucci\ErrorHandling\Tests\StatusCodeExtractionStrategy;
5+
6+
use Fig\Http\Message\StatusCodeInterface;
7+
use Lcobucci\ErrorHandling\Problem;
8+
use Lcobucci\ErrorHandling\StatusCodeExtractionStrategy\ClassMap;
9+
use PHPUnit\Framework\TestCase;
10+
use RuntimeException;
11+
use Throwable;
12+
13+
/**
14+
* @coversDefaultClass \Lcobucci\ErrorHandling\StatusCodeExtractionStrategy\ClassMap
15+
*/
16+
final class ClassMapTest extends TestCase
17+
{
18+
/**
19+
* @test
20+
*
21+
* @covers ::__construct
22+
* @covers ::extractStatusCode
23+
*/
24+
public function extractStatusCodeShouldUseGivenMapToRetrieveTheCode(): void
25+
{
26+
$extractor = new ClassMap([RuntimeException::class => StatusCodeInterface::STATUS_SERVICE_UNAVAILABLE]);
27+
28+
self::assertSame(
29+
StatusCodeInterface::STATUS_SERVICE_UNAVAILABLE,
30+
$extractor->extractStatusCode(new RuntimeException())
31+
);
32+
}
33+
34+
/**
35+
* @test
36+
*
37+
* @covers ::__construct
38+
* @covers ::extractStatusCode
39+
*/
40+
public function extractStatusCodeShouldUseExceptionCodeWhenItIsNotSetInTheMap(): void
41+
{
42+
$extractor = new ClassMap([]);
43+
44+
self::assertSame(
45+
StatusCodeInterface::STATUS_SERVICE_UNAVAILABLE,
46+
$extractor->extractStatusCode(new RuntimeException('', StatusCodeInterface::STATUS_SERVICE_UNAVAILABLE))
47+
);
48+
}
49+
50+
/**
51+
* @test
52+
*
53+
* @covers ::__construct
54+
* @covers ::extractStatusCode
55+
*/
56+
public function extractStatusCodeShouldFallbackToInternalServerError(): void
57+
{
58+
$extractor = new ClassMap([]);
59+
60+
self::assertSame(
61+
StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR,
62+
$extractor->extractStatusCode(new RuntimeException())
63+
);
64+
}
65+
66+
/**
67+
* @test
68+
* @dataProvider defaultConversions
69+
*
70+
* @covers ::__construct
71+
* @covers ::extractStatusCode
72+
*/
73+
public function extractStatusCodeShouldUseDefaultClassMapWhenNothingIsProvided(
74+
Throwable $error,
75+
int $expected
76+
): void {
77+
$extractor = new ClassMap();
78+
79+
self::assertSame($expected, $extractor->extractStatusCode($error));
80+
}
81+
82+
/**
83+
* @return array<string, array<Throwable|int>>
84+
*/
85+
public function defaultConversions(): iterable
86+
{
87+
yield Problem\InvalidRequest::class => [
88+
$this->createMock(Problem\InvalidRequest::class),
89+
StatusCodeInterface::STATUS_BAD_REQUEST,
90+
];
91+
92+
yield Problem\AuthorizationRequired::class => [
93+
$this->createMock(Problem\AuthorizationRequired::class),
94+
StatusCodeInterface::STATUS_UNAUTHORIZED,
95+
];
96+
97+
yield Problem\Forbidden::class => [
98+
$this->createMock(Problem\Forbidden::class),
99+
StatusCodeInterface::STATUS_FORBIDDEN,
100+
];
101+
102+
yield Problem\ResourceNotFound::class => [
103+
$this->createMock(Problem\ResourceNotFound::class),
104+
StatusCodeInterface::STATUS_NOT_FOUND,
105+
];
106+
107+
yield Problem\Conflict::class => [
108+
$this->createMock(Problem\Conflict::class),
109+
StatusCodeInterface::STATUS_CONFLICT,
110+
];
111+
112+
yield Problem\ResourceNoLongerAvailable::class => [
113+
$this->createMock(Problem\ResourceNoLongerAvailable::class),
114+
StatusCodeInterface::STATUS_GONE,
115+
];
116+
117+
yield Problem\UnprocessableRequest::class => [
118+
$this->createMock(Problem\UnprocessableRequest::class),
119+
StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY,
120+
];
121+
122+
yield Problem\ServiceUnavailable::class => [
123+
$this->createMock(Problem\ServiceUnavailable::class),
124+
StatusCodeInterface::STATUS_SERVICE_UNAVAILABLE,
125+
];
126+
}
127+
}

0 commit comments

Comments
 (0)