tarlepp/symfony-flex-backend

View on GitHub
src/Rest/Traits/RestMethodHelper.php

Summary

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

namespace App\Rest\Traits;

use App\DTO\RestDtoInterface;
use App\Rest\Interfaces\ControllerInterface;
use App\Rest\Traits\Methods\RestMethodProcessCriteria;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\UnitOfWork;
use LogicException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
use UnexpectedValueException;
use function array_key_exists;
use function class_implements;
use function in_array;
use function is_array;
use function is_int;
use function sprintf;

/**
 * @package App\Rest\Traits\Methods
 * @author TLe, Tarmo Leppänen <tarmo.leppanen@pinja.com>
 */
trait RestMethodHelper
{
    use RestMethodProcessCriteria;

    /**
     * Method + DTO class names (key + value)
     *
     * @var array<string, string>
     */
    protected static array $dtoClasses = [];

    public function getDtoClass(?string $method = null): string
    {
        $dtoClass = $method !== null && array_key_exists($method, static::$dtoClasses)
            ? static::$dtoClasses[$method]
            : $this->getResource()->getDtoClass();

        $interfaces = class_implements($dtoClass);

        if (is_array($interfaces) && !in_array(RestDtoInterface::class, $interfaces, true)) {
            $message = sprintf(
                'Given DTO class \'%s\' is not implementing \'%s\' interface.',
                $dtoClass,
                RestDtoInterface::class,
            );

            throw new UnexpectedValueException($message);
        }

        return $dtoClass;
    }

    /**
     * @param array<int, string> $allowedHttpMethods
     */
    public function validateRestMethod(Request $request, array $allowedHttpMethods): void
    {
        // Make sure that we have everything we need to make this work
        if (!($this instanceof ControllerInterface)) {
            $message = sprintf(
                'You cannot use \'%s\' controller class with REST traits if that does not implement \'%s\'',
                static::class,
                ControllerInterface::class,
            );

            throw new LogicException($message);
        }

        if (!in_array($request->getMethod(), $allowedHttpMethods, true)) {
            throw new MethodNotAllowedHttpException($allowedHttpMethods);
        }
    }

    /**
     * @throws Throwable
     */
    public function handleRestMethodException(Throwable $exception, ?string $id = null): Throwable
    {
        if ($id !== null) {
            $this->detachEntityFromManager($id);
        }

        return $this->determineOutputAndStatusCodeForRestMethodException($exception);
    }

    /**
     * Getter method for exception code with fallback to `400` bad response.
     */
    private function getExceptionCode(Throwable $exception): int
    {
        $code = $exception->getCode();

        return is_int($code) && $code !== 0 ? $code : Response::HTTP_BAD_REQUEST;
    }

    /**
     * Method to detach entity from entity manager so possible changes to it
     * won't be saved.
     *
     * @throws Throwable
     */
    private function detachEntityFromManager(string $id): void
    {
        $currentResource = $this->getResource();
        $entityManager = $currentResource->getRepository()->getEntityManager();

        // Fetch entity
        $entity = $currentResource->getRepository()->find($id);

        // Detach entity from manager if it's been managed by it
        if ($entity !== null
            /* @scrutinizer ignore-call */
            && $entityManager->getUnitOfWork()->getEntityState($entity) === UnitOfWork::STATE_MANAGED
        ) {
            $entityManager->clear();
        }
    }

    private function determineOutputAndStatusCodeForRestMethodException(Throwable $exception): Throwable
    {
        $code = $this->getExceptionCode($exception);

        $output = new HttpException($code, $exception->getMessage(), $exception, [], $code);

        if ($exception instanceof NoResultException || $exception instanceof NotFoundHttpException) {
            $code = Response::HTTP_NOT_FOUND;

            $output = new HttpException($code, 'Not found', $exception, [], $code);
        } elseif ($exception instanceof NonUniqueResultException) {
            $code = Response::HTTP_INTERNAL_SERVER_ERROR;

            $output = new HttpException($code, $exception->getMessage(), $exception, [], $code);
        } elseif ($exception instanceof HttpException) {
            if ($exception->getCode() === 0) {
                $output = new HttpException(
                    $exception->getStatusCode(),
                    $exception->getMessage(),
                    $exception->getPrevious(),
                    $exception->getHeaders(),
                    $code,
                );
            } else {
                $output = $exception;
            }
        }

        return $output;
    }
}