Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Responses
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
6 / 6
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 json
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 empty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 problem
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 encode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 body
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Api;
6
7use Celema\Core\Factory\Factory;
8use Psr\Http\Message\ResponseInterface as Response;
9
10/** JSON bodies and RFC 9457 problem details, the only two things the API answers with. */
11final class Responses
12{
13    public const string JSON = 'application/json';
14    public const string PROBLEM = 'application/problem+json';
15
16    private const array TITLES = [
17        400 => 'Bad Request',
18        401 => 'Unauthorized',
19        403 => 'Forbidden',
20        404 => 'Not Found',
21        405 => 'Method Not Allowed',
22        406 => 'Not Acceptable',
23        409 => 'Conflict',
24        410 => 'Gone',
25        412 => 'Precondition Failed',
26        413 => 'Content Too Large',
27        415 => 'Unsupported Media Type',
28        422 => 'Unprocessable Content',
29        428 => 'Precondition Required',
30        500 => 'Internal Server Error',
31        503 => 'Service Unavailable',
32    ];
33
34    public function __construct(
35        private readonly Factory $factory,
36    ) {}
37
38    /** @param array<string, string> $headers */
39    public function json(mixed $data, int $status = 200, array $headers = []): Response
40    {
41        return $this->body(self::encode($data), $status, ['Content-Type' => self::JSON, ...$headers]);
42    }
43
44    /** @param array<string, string> $headers */
45    public function empty(int $status = 204, array $headers = []): Response
46    {
47        return $this->body('', $status, $headers);
48    }
49
50    /**
51     * @param array<string, mixed> $extensions members next to the standard ones, like `errors`
52     * @param array<string, string> $headers
53     */
54    public function problem(int $status, ?string $detail = null, array $extensions = [], array $headers = []): Response
55    {
56        $body = ['type' => 'about:blank', 'title' => self::TITLES[$status] ?? 'Error', 'status' => $status];
57
58        if ($detail !== null && $detail !== '') {
59            $body['detail'] = $detail;
60        }
61
62        return $this->body(
63            self::encode([...$body, ...$extensions]),
64            $status,
65            ['Content-Type' => self::PROBLEM, ...$headers],
66        );
67    }
68
69    public static function encode(mixed $data): string
70    {
71        return json_encode(
72            $data,
73            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR,
74        );
75    }
76
77    /** @param array<string, string> $headers */
78    private function body(string $body, int $status, array $headers): Response
79    {
80        $response = $this->factory->response($status);
81
82        foreach ($headers as $name => $value) {
83            $response = $response->withHeader($name, $value);
84        }
85
86        if ($body !== '') {
87            $response->getBody()->write($body);
88        }
89
90        return $response;
91    }
92}