fapi-cz/php-client

View on GitHub
src/Fapi/FapiClient/Rest/FapiRestClient.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php declare(strict_types = 1);

namespace Fapi\FapiClient\Rest;

use Fapi\FapiClient\AuthorizationException;
use Fapi\FapiClient\NotFoundException;
use Fapi\FapiClient\ValidationException;
use Fapi\HttpClient\HttpClientException;
use Fapi\HttpClient\HttpMethod;
use Fapi\HttpClient\HttpRequest;
use Fapi\HttpClient\HttpStatusCode;
use Fapi\HttpClient\IHttpClient;
use Fapi\HttpClient\RedirectHelper;
use Fapi\HttpClient\Utils\Json;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use function http_build_query;
use function is_array;
use function is_string;
use function rtrim;

class FapiRestClient
{

    private string $apiUrl;

    public function __construct(
        private string $username,
        private string $password,
        string $apiUrl,
        private IHttpClient $httpClient,
    )
    {
        $this->apiUrl = rtrim($apiUrl, '/');
    }

    public function getCurrentUsername(): string
    {
        return $this->username;
    }

    public function checkConnection(): void
    {
        $this->getSingularResource('/');
    }

    /**
     * @param array<mixed> $parameters
     * @return array<mixed>
     */
    public function getResources(string $path, string $resourcesKey, array $parameters = [], int $options = 0): array
    {
        if ($parameters !== []) {
            $path .= '?' . $this->formatUrlParameters($parameters);
        }

        $httpResponse = $this->sendHttpRequest(HttpMethod::GET, $path);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S200_OK) {
            return $this->getResourcesResponseData($httpResponse, $resourcesKey, $options);
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    /**
     * @param array<mixed> $parameters
     * @return array<mixed>|null
     */
    public function getResource(string $path, string|int $id, array $parameters = [], int $options = 0): array|null
    {
        $path .= '/' . $id;

        if ($parameters !== []) {
            $path .= '?' . $this->formatUrlParameters($parameters);
        }

        $httpResponse = $this->sendHttpRequest(HttpMethod::GET, $path);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S200_OK) {
            return $this->getResourceResponseData($httpResponse, $options);
        }

        if ($httpResponse->getStatusCode() === HttpStatusCode::S404_NOT_FOUND) {
            return null;
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    /**
     * @param array<mixed> $parameters
     * @return array<mixed>
     */
    public function getSingularResource(string $path, array $parameters = [], int $options = 0): array
    {
        if ($parameters !== []) {
            $path .= '?' . $this->formatUrlParameters($parameters);
        }

        $httpResponse = $this->sendHttpRequest(HttpMethod::GET, $path);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S200_OK) {
            return $this->getResourceResponseData($httpResponse, $options);
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    /**
     * @param array<mixed> $data
     * @return array<mixed>
     */
    public function createResource(string $path, array $data, int $options = 0): array
    {
        $httpResponse = $this->sendHttpRequest(HttpMethod::POST, $path, $data);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S201_CREATED) {
            return $this->getResourceResponseData($httpResponse, $options);
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    /**
     * @param array<mixed> $data
     * @return array<mixed>
     */
    public function updateResource(string $path, int|string $id, array $data, int $options = 0): array
    {
        $httpResponse = $this->sendHttpRequest(HttpMethod::PUT, $path . '/' . $id, $data);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S200_OK) {
            return $this->getResourceResponseData($httpResponse, $options);
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    public function deleteResource(string $path, int|string $id): void
    {
        $httpResponse = $this->sendHttpRequest(HttpMethod::DELETE, $path . '/' . $id);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S200_OK) {
            return;
        }

        if ($httpResponse->getStatusCode() === HttpStatusCode::S404_NOT_FOUND) {
            return;
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    /**
     * @param array<mixed> $parameters
     */
    public function sendEmailWithInvoice(array $parameters): void
    {
        $httpResponse = $this->sendHttpRequest(HttpMethod::POST, '/invoices/send-email', $parameters);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S200_OK) {
            return;
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    /**
     * @return array<mixed>
     */
    public function generateQrCode(int $id): array
    {
        $url = '/invoices/generate-qr-code/' . $id;
        $httpResponse = $this->sendHttpRequest(HttpMethod::GET, $url);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S200_OK) {
            return $this->getResponseData($httpResponse);
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    /**
     * @param array<mixed> $parameters
     */
    public function getInvoicePdf(int $id, array $parameters = []): string|null
    {
        $path = '/invoices/' . $id . '.pdf';

        if ($parameters !== []) {
            $path .= '?' . $this->formatUrlParameters($parameters);
        }

        $httpResponse = $this->sendHttpRequest(HttpMethod::GET, $path);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S200_OK) {
            return (string) $httpResponse->getBody();
        }

        if ($httpResponse->getStatusCode() === HttpStatusCode::S404_NOT_FOUND) {
            return null;
        }

        $this->processErrorStatusCodeIfNeeded($httpResponse);

        throw new InvalidStatusCodeException('Api return invalid status code: ' . $httpResponse->getStatusCode());
    }

    /**
     * @param array<mixed>|null $data
     * @param array<mixed> $headers
     */
    private function sendHttpRequest(
        string $method,
        string $path,
        array|null $data = null,
        array $headers = [],
    ): ResponseInterface
    {
        $url = $this->apiUrl . $path;

        if (!isset($headers['Content-Type'])) {
            $headers['Content-Type'] = 'application/json';
        }

        if (!isset($headers['Accept'])) {
            $headers['Accept'] = 'application/json';
        }

        $options = [
            'auth' => [$this->username, $this->password],
            'headers' => $headers,
        ];

        if ($data !== null) {
            $options['json'] = $data;
        }

        try {
            $httpRequest = HttpRequest::from($url, $method, $options);
            $httpResponse = $this->httpClient->sendRequest($httpRequest);

            return RedirectHelper::followRedirects($this->httpClient, $httpResponse, $httpRequest, 3);
        } catch (HttpClientException $e) {
            throw new RestClientException('Failed to send an HTTP request.', 0, $e);
        }
    }

    /**
     * @param array<mixed> $parameters
     */
    private function formatUrlParameters(array $parameters): string
    {
        return http_build_query($parameters, '', '&');
    }

    /**
     * @return array<mixed>
     */
    private function getResourcesResponseData(
        ResponseInterface $httpResponse,
        string $resourcesKey,
        int $options,
    ): array
    {
        $responseData = $this->getResponseData($httpResponse);

        if (!is_array($responseData)) {
            throw new InvalidResponseBodyException('Response data is not an array.');
        }

        if (!isset($responseData[$resourcesKey])) {
            throw new InvalidResponseBodyException('Response data does not contain attribute with resources.');
        }

        $resources = $responseData[$resourcesKey];

        if (!is_array($resources)) {
            throw new InvalidResponseBodyException('Resources must be an array.');
        }

        foreach ($resources as $resource) {
            $this->validateResource($resource, $options);
        }

        return $resources;
    }

    /**
     * @return array<mixed>
     */
    private function getResourceResponseData(ResponseInterface $httpResponse, int $options): array
    {
        $resource = $this->getResponseData($httpResponse);

        $this->validateResource($resource, $options);

        return $resource;
    }

    /**
     * @param array<mixed>|string $resource
     */
    private function validateResource(array|string $resource, int $options): void
    {
        if ((bool) ($options & FapiRestClientOptions::STRING_RESOURCE)) {
            if (!is_string($resource)) {
                throw new InvalidResponseBodyException('Resource must be a string.');
            }

            return;
        }

        if (!is_array($resource)) {
            throw new InvalidResponseBodyException('Resource must be an array.');
        }
    }

    private function getErrorMessage(ResponseInterface $httpResponse): string
    {
        $responseData = $this->getResponseData($httpResponse);

        return $responseData['message'] ?? '';
    }

    private function getResponseData(ResponseInterface $httpResponse): mixed
    {
        try {
            return Json::decode((string) $httpResponse->getBody(), Json::FORCE_ARRAY);
        } catch (Throwable $e) {
            throw new InvalidResponseBodyException('Response body is not a valid JSON.', 0, $e);
        }
    }

    private function processErrorStatusCodeIfNeeded(ResponseInterface $httpResponse): void
    {
        $message = $this->getErrorMessage($httpResponse);

        if ($httpResponse->getStatusCode() === HttpStatusCode::S400_BAD_REQUEST) {
            throw new ValidationException($message);
        }

        if ($httpResponse->getStatusCode() === HttpStatusCode::S401_UNAUTHORIZED) {
            throw new AuthorizationException($message);
        }

        if ($httpResponse->getStatusCode() === HttpStatusCode::S404_NOT_FOUND) {
            throw new NotFoundException($message);
        }
    }

}