Framework/src/Http/Responses/JsonApiResponseFactory.php
Funky Waddle fd1c9d23df refactor(core): enforce SOLID across HTTP pipeline; add small contracts and defaults; align tests
- 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.
2025-12-15 09:15:49 -06:00

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;
}
}