Commit 78463df3 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Configuration des connexions API (ApiCOnnectionManager)

parent d3df70db
Loading
Loading
Loading
Loading
Loading
+6 −11
Changes for config/services.yaml: 6 added lines, 11 removed lines.
Original line number Diff line number Diff line
@@ -12,19 +12,15 @@ services:
  Symfony\Component\DependencyInjection\ContainerInterface:
    alias: service_container

  Unicaen\ApiClientBundle\Connection\ApiConnectionManager:
    arguments:
      $config: '%api_connexions%'

  Unicaen\ApiClientBundle\Service\Rest\Response\DataExtractor\ResponseDataExtractorPluginManager:
    public: true
    arguments:
      $config:
        Unicaen\ApiClientBundle\Service\Rest\Response\DataExtractor\Hal\HalResponseDataExtractor:
          enabled: true
          priority: 30 # plus prioritaire
        Unicaen\ApiClientBundle\Service\Rest\Response\DataExtractor\Solr\SolrResponseDataExtractor:
          enabled: true
          priority: 20
        Unicaen\ApiClientBundle\Service\Rest\Response\DataExtractor\Array\ArrayResponseDataExtractor:
          enabled: true
          priority: 10 # moins prioritaire
      $container: '@service_container'
      $config: '%api_data_extractors%'

  Unicaen\ApiClientBundle\Service\Rest\Response\DataExtractor\Hal\HalResponseDataExtractor:
    public: true
@@ -89,7 +85,6 @@ parameters:
#        # Stratégie de persistence du token JWT
#        #persistence_strategy: Unicaen\ApiClientBundle\ClientMiddleware\Jwt\SimpleCacheTokenPersistence


  unicaen_api:
      api_clients:
        clients: []
+69 −0
Changes for src/Config/ConfigException.php: 69 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Unicaen\ApiClientBundle\Config;

use Exception;
use Throwable;

class ConfigException extends Exception
{
    public static function missingKeyIn($key, string $type, $name = null): self
    {
        return $name ?
            new static(ucfirst($type) . " '$name' : la config doit contenir une clé '$key'") :
            new static(ucfirst($type) . " : une config ne contient pas la clé '$key' attendue");
    }

    public static function missingRootKey($key): self
    {
        return new static("La config de l'appli doit contenir la clé '$key' contenant la config du module");
    }

    public static function missingKey($key, ?string $context = null): self
    {
        return new static(
            "Le tableau de config doit contenir la clé '$key'" .
            ($context ? " ($context)" : '')
        );
    }

    public static function missingKeyInConnections($key): self
    {
        return new static("Clé '$key' introuvable dans les connexions");
    }

    public static function exclusiveKeys(array $keys): self
    {
        return new static("Les clés de config suivantes ne peuvent être présentes toutes les deux: " . implode(', ', $keys));
    }

    public static function atLeastOneKey(array $keys): self
    {
        return new static("L'une des clés de config suivantes doit être présente: " . implode(', ', $keys));
    }

    public static function illegalKey($key): self
    {
        return new static("La clé de config '$key' n'est pas autorisée");
    }

    public static function invalidValueForKey($key, string $expected = null, Throwable $previous = null): self
    {
        return new static(
            "La valeur associée à la clé de config '$key' n'est pas valide." .
            ($expected ? " Attendu : $expected." : ""),
            0,
            $previous
        );
    }

    public static function invalidValue($value, string $expected = null, Throwable $previous = null): self
    {
        return new static(
            "La valeur suivante n'est pas valide: " . print_r($value, true) . '.' .
            ($expected ? " Attendu : $expected." : ""),
            0,
            $previous
        );
    }
}
 No newline at end of file
+269 −0
Changes for src/Connection/ApiConnection.php: 269 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Unicaen\ApiClientBundle\Connection;

use InvalidArgumentException;
use JsonException;
use Laminas\Hydrator\ClassMethodsHydrator;

class ApiConnection
{
    private string $url;
    private bool $proxy = false;
    private bool $verify = false;
    private bool $debug = false;
    private ?string $user = null;
    private ?string $password = null;
    private array $headers = [];
    private string $pageParam = 'page';
    private string $pageSizeParam = 'page_size';
    private int $timeout = 0;
    private int $connectTimeout = 0;

    public static function fromArrayConfig(array $config): self
    {
        $inst = new static();

        $h = new ClassMethodsHydrator();
        $h->hydrate($config, $inst);

        return $inst;
    }

    public function toArray(): array
    {
        $h = new ClassMethodsHydrator();

        return $h->extract($this);
    }

    /**
     * @return string
     */
    public function getUrl(): string
    {
        return $this->url;
    }

    /**
     * @param string $url
     * @return ApiConnection
     */
    public function setUrl(string $url): ApiConnection
    {
        $this->url = $url;
        return $this;
    }

    /**
     * @return bool|array
     */
    public function getProxy()
    {
        return $this->proxy;
    }

    /**
     * Met l'option de config sous la forme littérale.
     *
     * Si l'option est un tableau du genre :
     *    ```
     *    ['http' => 'proxy.unicaen.fr:3128', 'https' => 'proxy.unicaen.fr:3128', 'no' => ['localhost', '.unicaen.fr']],
     *    ```
     * le résultat est :
     *    ```
     *    HTTP: proxy.unicaen.fr:3128
     *    HTTPS: proxy.unicaen.fr:3128
     *    NO: localhost, .unicaen.fr
     *    ```
     *
     * @return string
     */
    public function getProxyToString(): string
    {
        if (is_array($this->proxy)) {
            $proxy = $this->proxy;

            return array_reduce(array_keys($proxy), function($previous, $key) use ($proxy) {
                $value = is_array($proxy[$key]) ? implode(', ', $proxy[$key]) : $proxy[$key];
                return $previous . strtoupper($key) . ': ' . $value . PHP_EOL;
            }, '');
        }

        return $this->proxy;
    }

    /**
     * @param bool|array $proxy
     * @return ApiConnection
     */
    public function setProxy($proxy): ApiConnection
    {
        $this->proxy = $proxy;
        return $this;
    }

    /**
     * @return bool
     */
    public function getVerify(): bool
    {
        return $this->verify;
    }

    /**
     * @param bool $verify
     * @return ApiConnection
     */
    public function setVerify(bool $verify = true): ApiConnection
    {
        $this->verify = $verify;
        return $this;
    }

    public function getDebug(): bool
    {
        return $this->debug;
    }

    public function setDebug(bool $debug = true): static
    {
        $this->debug = $debug;
        return $this;
    }

    /**
     * @return string
     */
    public function getUser(): ?string
    {
        return $this->user;
    }

    /**
     * @param string|null $user
     * @return ApiConnection
     */
    public function setUser(?string $user = null): ApiConnection
    {
        $this->user = $user;
        return $this;
    }

    /**
     * @return string
     */
    public function getPassword(): ?string
    {
        return $this->password;
    }

    /**
     * @param string|null $password
     * @return ApiConnection
     */
    public function setPassword(?string $password = null): ApiConnection
    {
        $this->password = $password;
        return $this;
    }

    public function getHeaders(): array
    {
        return $this->headers;
    }

    public function setHeaders(string|array $headers): self
    {
        if (is_string($headers)) {
            try {
                $headers = json_decode($headers, true, 512, JSON_THROW_ON_ERROR);
            } catch (JsonException $e) {
                throw new InvalidArgumentException(sprintf(
                    "Impossible de décoder les headers spécifiés au format JSON : %s. %s",
                    $headers,
                    $e->getMessage()
                ));
            }
        }

        $this->headers = $headers;

        return $this;
    }

    /**
     * @return string
     */
    public function getPageParam(): string
    {
        return $this->pageParam;
    }

    /**
     * @param string $pageParam
     * @return ApiConnection
     */
    public function setPageParam(string $pageParam): ApiConnection
    {
        $this->pageParam = $pageParam;

        return $this;
    }

    /**
     * @return string
     */
    public function getPageSizeParam(): string
    {
        return $this->pageSizeParam;
    }

    /**
     * @param string $pageSizeParam
     * @return ApiConnection
     */
    public function setPageSizeParam(string $pageSizeParam): ApiConnection
    {
        $this->pageSizeParam = $pageSizeParam;

        return $this;
    }

    /**
     * @return int
     */
    public function getTimeout(): int
    {
        return $this->timeout;
    }

    /**
     * @param int $timeout
     * @return self
     */
    public function setTimeout(int $timeout): self
    {
        $this->timeout = $timeout;
        return $this;
    }

    /**
     * @return int
     */
    public function getConnectTimeout(): int
    {
        return $this->connectTimeout;
    }

    /**
     * @param int $connectTimeout
     * @return self
     */
    public function setConnectTimeout(int $connectTimeout): self
    {
        $this->connectTimeout = $connectTimeout;
        return $this;
    }

}
 No newline at end of file
+58 −0
Changes for src/Connection/ApiConnectionManager.php: 58 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Unicaen\ApiClientBundle\Connection;

use Unicaen\ApiClientBundle\Config\ConfigException;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;

class ApiConnectionManager
{
    protected ?array $connections = null;

    public function __construct(private readonly array $config = [])
    {
    }

    public function hasConnectionByName(string $connectionName): bool
    {
        return
            isset($this->connections[$connectionName]) ||
            array_key_exists($connectionName, $this->config);
    }

    /**
     * @throws \Unicaen\ApiClientBundle\Config\ConfigException
     */
    public function getConnectionByName(string $connectionName): ApiConnection
    {
        if (isset($this->connections[$connectionName])) {
            return $this->connections[$connectionName];
        }

        $this->connections[$connectionName] = $this->createConnection($connectionName);

        return $this->connections[$connectionName];
    }


    /**
     * @throws \Unicaen\ApiClientBundle\Config\ConfigException
     */
    private function createConnection(string $connectionName): ApiConnection
    {
        try {
            Assert::keyExists($this->config, $connectionName);
        } catch (InvalidArgumentException $e) {
            throw ConfigException::missingKey($connectionName, "tableau de config des connexions");
        }

        try {
            Assert::isArray($this->config[$connectionName]);
        } catch (InvalidArgumentException $e) {
            throw ConfigException::invalidValueForKey($connectionName, 'array');
        }

        return ApiConnection::fromArrayConfig($this->config[$connectionName]);
    }
}
 No newline at end of file
+57 −150
Changes for src/Service/Rest/RestService.php: 57 added lines, 150 removed lines.
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use stdClass;
use Unicaen\ApiClientBundle\Connection\ApiConnection;
use Unicaen\ApiClientBundle\Service\Rest\Response\DataExtractor\ResponseDataExtractorInterface;
use Unicaen\ApiClientBundle\Service\Rest\Response\DataExtractor\ResponseDataExtractorPluginManager;

@@ -29,6 +30,7 @@ class RestService implements LoggerAwareInterface

    protected array $config;
    protected ?Client $client = null;
    protected ApiConnection $connection;

    public function __construct(
        private readonly ResponseDataExtractorPluginManager $dataExtractorPluginManager
@@ -36,6 +38,15 @@ class RestService implements LoggerAwareInterface
    {
    }

    /**
     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
     */
    public function setConnection(ApiConnection $connection): void
    {
        $this->connection = $connection;
        $this->setConfig($this->connection->toArray());
    }

    /**
     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
     */
@@ -52,33 +63,6 @@ class RestService implements LoggerAwareInterface
        return $this;
    }

//    /**
//     * Interroge l'API pour obtenir tous les enregistrements.
//     *
//     * @return stdClass[]
//     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
//     */
//    public function fetchAll(RestConnection $connection, SourceInterface $source): array
//    {
//
//    }
//
//    /**
//     * Interroge l'API pour obtenir seulement le 1er enregistrement ($pageSize = $page = 1).
//     *
//     * @return stdClass[]
//     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
//     */
//    public function fetchFirstRow(RestConnection $connection, SourceInterface $source): array
//    {
//
//    }
//
//    private function buildUrl(RestConnection $connection, SourceInterface $source): string
//    {
//
//    }

    /**
     * Envoie une requête GET.
     *
@@ -109,7 +93,7 @@ class RestService implements LoggerAwareInterface

        $p = $pageIncrement === 1 ? 1 : 0;
        do {
            $finalUrl = $url;
            $finalUrl = $this->buildUrl($url);

            $params = [];
            if ($paginated) {
@@ -186,30 +170,52 @@ class RestService implements LoggerAwareInterface
        while (true);
    }

    /**
     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
     */
    public function sendPostRequest(string $url, string $data): stdClass
    {
        $finalUrl = $this->buildUrl($url);

        $request = new Request('POST', $finalUrl, body: $data);

        return $this->sendRequest($request);
    }

    /**
     * Envoie une requête PUT.
     *
     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
     */
    public function sendPutRequest(string $uri, stdClass $data, ?array $headers = []): stdClass
    public function sendPutRequest(string $url, string $data, ?array $headers = []): stdClass
    {
        $request = new Request('PUT', $uri, $headers, json_encode($data));
        $finalUrl = $this->buildUrl($url);

        $request = new Request('PUT', $finalUrl, $headers, $data);

        return $this->sendRequest($request);
    }

    protected function normalizedSelect(iterable|string $select): iterable
    /**
     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
     */
    public function sendDeleteRequest(string $url): ?stdClass
    {
        if (is_iterable($select)) {
            return $select;
        } elseif (is_string($select)) {
            return [$select];
        $finalUrl = $this->buildUrl($url);

        $request = new Request('DELETE', $finalUrl);

        return $this->sendRequest($request);
    }

        throw new InvalidArgumentException(sprintf(
            "Un type de select inattendu a été spécifié : %s",
            gettype($select)
        ));
    private function buildUrl(string $uri): string
    {
        $urlPrefix = $this->connection->getUrl();

        $urlPrefix = rtrim($urlPrefix, '/');
        $finalUrl = ltrim($uri, '/');

        return $urlPrefix . '/' . $finalUrl;
    }

    /**
@@ -254,6 +260,9 @@ class RestService implements LoggerAwareInterface
        } else {
            $this->config['proxy'] = ['no' => 'localhost'];
        }
        if (array_key_exists('debug', $config)) {
            $this->config['debug'] = $config['debug'];
        }
    }

    /**
@@ -268,7 +277,7 @@ class RestService implements LoggerAwareInterface
    }

    /**
     * @return Client
     * Retourne le Client Guzzle utilisé, pour plus de liberté.
     */
    public function getClient(): Client
    {
@@ -279,6 +288,9 @@ class RestService implements LoggerAwareInterface
        return $this->client;
    }

    /**
     * Instancie un nouveau Client Guzzle à partir de la config courante.
     */
    private function createClient(): Client
    {
        return new Client($this->config);
@@ -291,7 +303,7 @@ class RestService implements LoggerAwareInterface
     *
     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
     */
    private function sendRequest(Request $request): array|stdClass
    private function sendRequest(Request $request): array|stdClass|null
    {
        $uri = $request->getUri();

@@ -313,129 +325,24 @@ class RestService implements LoggerAwareInterface
            throw RestServiceException::unexpectedError($e, $uri);
        }

        if ($request->getMethod() !== 'POST' && $request->getMethod() !== 'DELETE') {
            if ($response->getStatusCode() !== 200) {
                throw RestServiceException::unexpectedResponse($response, $uri);
            }
        }

        $body = $response->getBody();

        /** @var stdClass|stdClass[] $json */
        $json = json_decode($body);

        if ($request->getMethod() !== 'DELETE') {
            if ($json === null) {
                // NULL is returned if the json cannot be decoded or if the encoded data is deeper than the recursion limit.
                throw RestServiceException::invalidJSONResponse((string)$body, $uri);
            }
        }

        return $json;
    }

//    /**
//     * @throws AssertionFailedException
//     */
//    private function loadConfig(array $config): void
//    {
//        Assertion::keyIsset($config, 'url');
//
//        $this->config = [
//            'base_uri' => $config['url'],
//            'headers' => [
//                'Accept' => 'application/json',
//            ],
//        ];
//
//        if (array_key_exists('user', $config)) {
//            $this->config['auth'] = [
//                $config['user'],
//                $config['password']
//            ];
//        }
//
//        if (array_key_exists('verify', $config)) {
//            $this->config['verify'] = $config['verify'];
//        }
//        if (array_key_exists('timeout', $config)) {
//            $this->config['timeout'] = $config['timeout'];
//        }
//        if (array_key_exists('connect_timeout', $config)) {
//            $this->config['connect_timeout'] = $config['connect_timeout'];
//        }
//        if (array_key_exists('proxy', $config)) {
//            $this->config['proxy'] = $config['proxy'];
//        } else {
//            $this->config['proxy'] = ['no' => 'localhost'];
//        }
//    }
//
//    public function setClient(Client $client): static
//    {
//        $this->client = $client;
//
//        return $this;
//    }
//
//    public function getClient(): Client
//    {
//        if ($this->client === null) {
//            $this->client = $this->createClient();
//        }
//
//        return $this->client;
//    }
//
//    private function createClient(): Client
//    {
//        return new Client($this->config);
//    }
//
//    /**
//     * Envoie une requête à l'API et retourne la réponse.
//     *
//     * @return stdClass|stdClass[] Réponse de l'API
//     *
//     * @throws \Unicaen\ApiClientBundle\Service\Rest\RestServiceException
//     */
//    private function sendRequest(Request $request): array|stdClass
//    {
//        $uri = $request->getUri();
//
//        $client = $this->getClient();
//        $response = null;
//        try {
//            $debut = microtime(true);
//            $response = $client->send($request);
//            $fin = microtime(true);
//            $this->logger->debug(
//                sprintf("Requête %s vers %s en %.2f s.", $request->getMethod(), $uri, $fin - $debut)
//            );
//        } catch (ClientException $e) {
//            throw RestServiceException::clientError($e, $uri);
//        } catch (ServerException $e) {
//            throw RestServiceException::serverError($e, $uri);
//        } catch (RequestException $e) {
//            throw RestServiceException::networkError($e, $uri);
//        } catch (GuzzleException $e) {
//        } catch (Exception $e) {
//            throw RestServiceException::unexpectedError($e, $uri);
//        }
//        if (!$response) {
//            throw RestServiceException::emptyResponse($uri);
//        }
//
//        if ($response->getStatusCode() !== 200) {
//            throw RestServiceException::unexpectedResponse($response, $uri);
//        }
//
//        $body = $response->getBody();
//
//        /** @var stdClass|stdClass[] $json */
//        $json = json_decode($body);
//
//        if ($json === null) {
//            // NULL is returned if the json cannot be decoded or if the encoded data is deeper than the recursion limit.
//            throw RestServiceException::invalidJSONResponse((string)$body, $uri);
//        }
//
//        return $json;
//    }
}
 No newline at end of file
Loading