- Introduce small interfaces and default adapters (DIP): - Support\Contracts\ConfigInterface + Support\DefaultConfig - Http\Contracts\ErrorFormatNegotiatorInterface + Http\Support\DefaultErrorFormatNegotiator - Http\Contracts\RequestIdProviderInterface + Http\Support\DefaultRequestIdProvider - Http\Contracts\ExceptionToStatusMapperInterface + Http\Support\DefaultExceptionToStatusMapper - Kernel: bind new contracts in the container; keep DelegatingApiResponseFactory wiring - ContentNegotiationMiddleware: depend on ConfigInterface + negotiator; honor Accept for JSON:API - ProblemDetailsMiddleware: inject negotiator + config; split into small helpers; deterministic content negotiation; stable Whoops HTML; include X-Request-Id - DispatchMiddleware: SRP refactor into small methods; remove hidden coupling; normalize non-Response returns - Add/adjust tests: - tests/ErrorHandlingTest.php for problem details, JSON:API errors, and Whoops HTML - tests/ContentNegotiationTest.php for format selection - tests/MakeCommandTest.php aligned with create:command scaffolder - Docs/Meta: update README and MILESTONES; .gitignore to ignore .junie.json No runtime behavior changes intended beyond clearer DI boundaries and content-negotiation determinism. All tests green.
64 lines
1.9 KiB
PHP
64 lines
1.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Phred\Http\Responses;
|
|
|
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
|
use Phred\Http\Contracts\ApiResponseFactoryInterface;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
|
final class JsonApiResponseFactory implements ApiResponseFactoryInterface
|
|
{
|
|
public function __construct(private Psr17Factory $psr17 = new Psr17Factory()) {}
|
|
|
|
public function ok(array $data = []): ResponseInterface
|
|
{
|
|
return $this->document(['data' => $data], 200);
|
|
}
|
|
|
|
public function created(array $data = [], ?string $location = null): ResponseInterface
|
|
{
|
|
$res = $this->document(['data' => $data], 201);
|
|
if ($location) {
|
|
$res = $res->withHeader('Location', $location);
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
public function noContent(): ResponseInterface
|
|
{
|
|
// JSON:API allows 204 without body
|
|
return $this->psr17->createResponse(204);
|
|
}
|
|
|
|
public function error(int $status, string $title, ?string $detail = null, array $extra = []): ResponseInterface
|
|
{
|
|
$error = array_filter([
|
|
'status' => (string) $status,
|
|
'title' => $title,
|
|
'detail' => $detail,
|
|
], static fn($v) => $v !== null && $v !== '');
|
|
if (!empty($extra)) {
|
|
$error = array_merge($error, $extra);
|
|
}
|
|
return $this->document(['errors' => [$error]], $status);
|
|
}
|
|
|
|
public function fromArray(array $payload, int $status = 200): ResponseInterface
|
|
{
|
|
// Caller must ensure payload is a valid JSON:API document shape
|
|
return $this->document($payload, $status);
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $doc
|
|
*/
|
|
private function document(array $doc, int $status): ResponseInterface
|
|
{
|
|
$res = $this->psr17->createResponse($status)
|
|
->withHeader('Content-Type', 'application/vnd.api+json');
|
|
$res->getBody()->write(json_encode($doc, JSON_UNESCAPED_SLASHES));
|
|
return $res;
|
|
}
|
|
}
|