tarlepp/symfony-flex-backend

View on GitHub
src/Rest/ResponseHandler.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php
declare(strict_types = 1);
/**
 * /src/Rest/ResponseHandler.php
 *
 * @author TLe, Tarmo Leppänen <tarmo.leppanen@pinja.com>
 */

namespace App\Rest;

use App\Rest\Interfaces\ResponseHandlerInterface;
use App\Rest\Interfaces\RestResourceInterface;
use Override;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Serializer\SerializerInterface;
use Throwable;
use function array_key_exists;
use function array_map;
use function array_merge;
use function array_pop;
use function array_unique;
use function array_values;
use function end;
use function explode;
use function implode;
use function sprintf;
use function str_starts_with;

/**
 * @package App\Rest
 * @author TLe, Tarmo Leppänen <tarmo.leppanen@pinja.com>
 */
class ResponseHandler implements ResponseHandlerInterface
{
    /**
     * Content types for supported response output formats.
     *
     * @var array<string, string>
     */
    private array $contentTypes = [
        self::FORMAT_JSON => 'application/json',
        self::FORMAT_XML => 'application/xml',
    ];

    public function __construct(
        private readonly SerializerInterface $serializer,
    ) {
    }

    #[Override]
    public function getSerializer(): SerializerInterface
    {
        return $this->serializer;
    }

    /**
     * @return array<int|string, mixed>
     *
     * @throws Throwable
     */
    #[Override]
    public function getSerializeContext(Request $request, ?RestResourceInterface $restResource = null): array
    {
        /**
         * Specify used populate settings
         *
         * @var array<int, string> $populate
         */
        $populate = (array)($request->query->get('populate') ?? $request->request->get('populate'));

        $groups = ['default', ...$populate];

        if ($restResource !== null) {
            // Get current entity name
            $bits = explode('\\', $restResource->getEntityName());
            $entityName = end($bits);

            $populate = $this->checkPopulateAll(
                array_key_exists('populateAll', $request->query->all()),
                $populate,
                $entityName,
                $restResource
            );

            $groups = [$entityName, ...$populate];
            $filter = static fn (string $groupName): bool => str_starts_with($groupName, 'Set.');

            if (array_key_exists('populateOnly', $request->query->all())
                || array_values(array_filter($groups, $filter)) !== []
            ) {
                $groups = $populate === [] ? [$entityName] : $populate;
            }
        }

        return array_merge(
            [
                'groups' => array_unique($groups),
            ],
            $restResource !== null ? $restResource->getSerializerContext() : [],
        );
    }

    /**
     * @throws Throwable
     */
    #[Override]
    public function createResponse(
        Request $request,
        mixed $data,
        ?RestResourceInterface $restResource = null,
        ?int $httpStatus = null,
        ?string $format = null,
        ?array $context = null,
    ): Response {
        $httpStatus ??= 200;
        $context ??= $this->getSerializeContext($request, $restResource);
        $format = $this->getFormat($request, $format);
        $response = $this->getResponse($data, $httpStatus, $format, $context);

        // Set content type
        $response->headers->set('Content-Type', $this->contentTypes[$format]);

        return $response;
    }

    #[Override]
    public function handleFormError(FormInterface $form): void
    {
        $errors = [];

        /** @var FormError $error */
        foreach ($form->getErrors(true) as $error) { // @phpstan-ignore-line
            $name = $error->getOrigin()?->getName() ?? '';

            $errors[] = sprintf(
                'Field \'%s\': %s',
                $name,
                $error->getMessage()
            );

            if ($name === '') {
                array_pop($errors);

                $errors[] = $error->getMessage();
            }
        }

        throw new HttpException(Response::HTTP_BAD_REQUEST, implode("\n", $errors));
    }

    /**
     * @param array<int, string> $populate
     *
     * @return array<int, string>
     *
     * @throws Throwable
     */
    private function checkPopulateAll(
        bool $populateAll,
        array $populate,
        string $entityName,
        RestResourceInterface $restResource,
    ): array {
        // Set all associations to be populated
        if ($populateAll && $populate === []) {
            $associations = $restResource->getAssociations();
            $populate = array_map(
                static fn (string $assocName): string => $entityName . '.' . $assocName,
                $associations,
            );
        }

        return $populate;
    }

    /**
     * Getter method response format with fallback to default formats;
     *  - XML
     *  - JSON
     */
    private function getFormat(Request $request, ?string $format = null): string
    {
        return $format
            ?? ($request->getContentTypeFormat() === self::FORMAT_XML ? self::FORMAT_XML : self::FORMAT_JSON);
    }

    /**
     * @param array<mixed> $context
     */
    private function getResponse(mixed $data, int $httpStatus, string $format, array $context): Response
    {
        try {
            // Create new response
            $response = new Response();
            $response->setContent($this->serializer->serialize($data, $format, $context));
            $response->setStatusCode($httpStatus);
        } catch (Throwable $exception) {
            $status = Response::HTTP_BAD_REQUEST;

            throw new HttpException($status, $exception->getMessage(), $exception, [], $status);
        }

        return $response;
    }
}