Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit c6a6ce5

Browse files
committed
Merging develop to master in preparation for 1.1.0 release.
2 parents 33547b3 + 24d240e commit c6a6ce5

10 files changed

+155
-9
lines changed

CHANGELOG.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ All notable changes to this project will be documented in this file, in reverse
44

55
Versions 0.3.0 and prior were released as "weierophinney/problem-details".
66

7-
## 1.0.3 - TBD
7+
## 1.1.0 - 2019-12-30
88

99
### Added
1010

11-
- Nothing.
11+
- [#51](https://github.com/zendframework/zend-problem-details/pull/51) adds a new `problem-details.default_types_map` config option, which can be used to define custom `type` values based on status codes.
1212

1313
### Changed
1414

composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
"config-provider": "Zend\\ProblemDetails\\ConfigProvider"
5050
},
5151
"branch-alias": {
52-
"dev-master": "1.0.x-dev",
53-
"dev-develop": "1.1.x-dev"
52+
"dev-master": "1.1.x-dev",
53+
"dev-develop": "1.2.x-dev"
5454
}
5555
},
5656
"scripts": {

composer.lock

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/book/default-types.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Default Types
2+
3+
- **Since 1.1.0.**
4+
5+
When you raise your own exceptions implementing `Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface`
6+
you will always be in control of all the properties returned as part of the
7+
response payload, including the `status`, `type`, `title`, `detail`, etc.
8+
items.
9+
10+
However, there are some use cases in which this library will have to infer some
11+
of those values.
12+
13+
The main situations in which this can happen are:
14+
15+
- When an exception not implementing `ProblemDetailsExceptionInterface` is
16+
captured by the `ProblemDetailsMiddleware`.
17+
- When the `ProblemDetailsNotFoundHandler` is executed.
18+
19+
In these two cases, the `title` and `type` properties will be inferred from the
20+
status code, which will usually be `500` in the first case and `404` in the
21+
second one.
22+
23+
> To be more precise, the `ProblemDetailsMiddleware` will use the exception's
24+
> error code when `debug` is `true`, and `500` otherwise.
25+
26+
Because of this, in any of those cases, you will end up with values like
27+
`https://httpstatus.es/404` or `https://httpstatus.es/500` for the `type`
28+
property.
29+
30+
## Configuring custom default types
31+
32+
Since the `type` property will usually be used by API consumers to uniquely
33+
identify an error, you might want to be able to provide your own custom values
34+
for the `type` property.
35+
36+
In order to do that, this library lets you configure the default `type` value to
37+
be used for every status code when some of the cases listed above happens.
38+
39+
```php
40+
return [
41+
'problem-details' => [
42+
'default_types_map' => [
43+
404 => 'https://example.com/problem-details/error/not-found',
44+
500 => 'https://example.com/problem-details/error/internal-server-error',
45+
],
46+
],
47+
];
48+
```
49+
50+
If this configuration is found, it will be consumed by the
51+
[ProblemDetailsResponseFactoryFactory](response.md#problemdetailsresponsefactoryfactory)
52+
and your custom values will be used when the `type` was not explicitly provided.

docs/book/response.md

+5
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ This package also provides a factory for generating the
146146
- If the service contains a `problem-details` key with an array value
147147
containing a `json_flags` key, and that value is an integer, that value is
148148
provided as the `$jsonFlags` parameter.
149+
- If the service contains a `problem-details` key with an array value
150+
containing a `default_types_map` key, and that value is an array, that
151+
value is provided as the `$defaultTypesMap` parameter; see the
152+
[default types documentation](default-types.md) for details on defining
153+
this map. (Since 1.1.0.)
149154

150155
If any of the above config values are not present, a `null` value will be
151156
passed, allowing the default value to be used.

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pages:
99
- "Exceptions": exception.md
1010
- "Error Handling Middleware": middleware.md
1111
- "Not Found Handler": not-found-handler.md
12+
- "Default Types": default-types.md
1213
site_name: zend-problem-details
1314
site_description: 'PSR-7 Problem Details for HTTP API responses and middleware'
1415
repo_url: 'https://github.com/zendframework/zend-problem-details'

src/ProblemDetailsResponseFactory.php

+13-2
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,22 @@ class ProblemDetailsResponseFactory
204204
*/
205205
private $defaultDetailMessage;
206206

207+
/**
208+
* A map used to infer the "type" property based on the status code.
209+
*
210+
* Defaults to an empty map.
211+
*
212+
* @var array
213+
*/
214+
private $defaultTypesMap;
215+
207216
public function __construct(
208217
callable $responseFactory,
209218
bool $isDebug = self::EXCLUDE_THROWABLE_DETAILS,
210219
int $jsonFlags = null,
211220
bool $exceptionDetailsInResponse = false,
212-
string $defaultDetailMessage = self::DEFAULT_DETAIL_MESSAGE
221+
string $defaultDetailMessage = self::DEFAULT_DETAIL_MESSAGE,
222+
array $defaultTypesMap = []
213223
) {
214224
// Ensures type safety of the composed factory
215225
$this->responseFactory = function () use ($responseFactory) : ResponseInterface {
@@ -228,6 +238,7 @@ public function __construct(
228238
$this->jsonFlags = $jsonFlags;
229239
$this->exceptionDetailsInResponse = $exceptionDetailsInResponse;
230240
$this->defaultDetailMessage = $defaultDetailMessage;
241+
$this->defaultTypesMap = $defaultTypesMap;
231242
}
232243

233244
public function createResponse(
@@ -392,7 +403,7 @@ private function createTitleFromStatus(int $status) : string
392403

393404
private function createTypeFromStatus(int $status) : string
394405
{
395-
return sprintf('https://httpstatus.es/%s', $status);
406+
return $this->defaultTypesMap[$status] ?? sprintf('https://httpstatus.es/%s', $status);
396407
}
397408

398409
private function createThrowableDetail(Throwable $e) : array

src/ProblemDetailsResponseFactoryFactory.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ public function __invoke(ContainerInterface $container) : ProblemDetailsResponse
2121

2222
$problemDetailsConfig = $config['problem-details'] ?? [];
2323
$jsonFlags = $problemDetailsConfig['json_flags'] ?? null;
24+
$defaultTypesMap = $problemDetailsConfig['default_types_map'] ?? [];
2425

2526
return new ProblemDetailsResponseFactory(
2627
$container->get(ResponseInterface::class),
2728
$includeThrowableDetail,
2829
$jsonFlags,
29-
$includeThrowableDetail
30+
$includeThrowableDetail,
31+
ProblemDetailsResponseFactory::DEFAULT_DETAIL_MESSAGE,
32+
$defaultTypesMap
3033
);
3134
}
3235
}

test/ProblemDetailsResponseFactoryFactoryTest.php

+21
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,25 @@ public function testUsesJsonFlagsSettingFromConfigWhenPresent() : void
139139
$this->assertInstanceOf(ProblemDetailsResponseFactory::class, $factory);
140140
$this->assertAttributeSame(JSON_PRETTY_PRINT, 'jsonFlags', $factory);
141141
}
142+
143+
public function testUsesDefaultTypesSettingFromConfigWhenPresent() : void
144+
{
145+
$expectedDefaultTypes = [
146+
404 => 'https://example.com/problem-details/error/not-found',
147+
];
148+
149+
$this->container->has('config')->willReturn(true);
150+
$this->container->get('config')->willReturn(
151+
['problem-details' => ['default_types_map' => $expectedDefaultTypes]]
152+
);
153+
154+
$this->container->get(ResponseInterface::class)->willReturn(function () {
155+
});
156+
157+
$factoryFactory = new ProblemDetailsResponseFactoryFactory();
158+
$factory = $factoryFactory($this->container->reveal());
159+
160+
$this->assertInstanceOf(ProblemDetailsResponseFactory::class, $factory);
161+
$this->assertAttributeSame($expectedDefaultTypes, 'defaultTypesMap', $factory);
162+
}
142163
}

test/ProblemDetailsResponseFactoryTest.php

+52
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use function array_keys;
2525
use function fclose;
2626
use function fopen;
27+
use function json_decode;
2728
use function stripos;
2829

2930
class ProblemDetailsResponseFactoryTest extends TestCase
@@ -475,4 +476,55 @@ function (array $payload) {
475476

476477
$this->assertSame($this->response->reveal(), $response);
477478
}
479+
480+
public function provideMappedStatuses() : array
481+
{
482+
$defaultTypesMap = [
483+
404 => 'https://example.com/problem-details/error/not-found',
484+
500 => 'https://example.com/problem-details/error/internal-server-error',
485+
];
486+
487+
return [
488+
[$defaultTypesMap, 404, 'https://example.com/problem-details/error/not-found'],
489+
[$defaultTypesMap, 500, 'https://example.com/problem-details/error/internal-server-error'],
490+
[$defaultTypesMap, 400, 'https://httpstatus.es/400'],
491+
[[], 500, 'https://httpstatus.es/500'],
492+
];
493+
}
494+
495+
/**
496+
* @dataProvider provideMappedStatuses
497+
*/
498+
public function testTypeIsInferredFromDefaultTypesMap(array $map, int $status, string $expectedType) : void
499+
{
500+
$this->request->getHeaderLine('Accept')->willReturn('application/json');
501+
502+
$stream = $this->prophesize(StreamInterface::class);
503+
$writeStream = $stream->write(Argument::that(function (string $body) use ($expectedType) {
504+
$payload = json_decode($body, true);
505+
Assert::assertEquals($expectedType, $payload['type']);
506+
507+
return $body;
508+
}));
509+
510+
$this->response->getBody()->will([$stream, 'reveal']);
511+
$withStatus = $this->response->withStatus($status)->will([$this->response, 'reveal']);
512+
$this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']);
513+
514+
$factory = new ProblemDetailsResponseFactory(
515+
function () {
516+
return $this->response->reveal();
517+
},
518+
false,
519+
null,
520+
false,
521+
'',
522+
$map
523+
);
524+
525+
$factory->createResponse($this->request->reveal(), $status, 'detail');
526+
527+
$writeStream->shouldHaveBeenCalled();
528+
$withStatus->shouldHaveBeenCalled();
529+
}
478530
}

0 commit comments

Comments
 (0)