vendor/typesense/typesense-php/src/ApiCall.php line 74

Open in your IDE?
  1. <?php
  2. namespace Typesense;
  3. use Exception;
  4. use Http\Client\Exception as HttpClientException;
  5. use Http\Client\Exception\HttpException;
  6. use Http\Client\HttpClient;
  7. use Psr\Http\Message\StreamInterface;
  8. use Psr\Log\LoggerInterface;
  9. use Typesense\Exceptions\HTTPStatus0Error;
  10. use Typesense\Exceptions\ObjectAlreadyExists;
  11. use Typesense\Exceptions\ObjectNotFound;
  12. use Typesense\Exceptions\ObjectUnprocessable;
  13. use Typesense\Exceptions\RequestMalformed;
  14. use Typesense\Exceptions\RequestUnauthorized;
  15. use Typesense\Exceptions\ServerError;
  16. use Typesense\Exceptions\ServiceUnavailable;
  17. use Typesense\Exceptions\TypesenseClientError;
  18. use Typesense\Lib\Configuration;
  19. use Typesense\Lib\Node;
  20. /**
  21.  * Class ApiCall
  22.  *
  23.  * @package \Typesense
  24.  * @date    4/5/20
  25.  * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
  26.  */
  27. class ApiCall
  28. {
  29.     private const API_KEY_HEADER_NAME 'X-TYPESENSE-API-KEY';
  30.     /**
  31.      * @var HttpClient
  32.      */
  33.     private HttpClient $client;
  34.     /**
  35.      * @var Configuration
  36.      */
  37.     private Configuration $config;
  38.     /**
  39.      * @var array|Node[]
  40.      */
  41.     private static array $nodes;
  42.     /**
  43.      * @var Node|null
  44.      */
  45.     private static ?Node $nearestNode;
  46.     /**
  47.      * @var int
  48.      */
  49.     private int $nodeIndex;
  50.     /**
  51.      * @var LoggerInterface
  52.      */
  53.     public LoggerInterface $logger;
  54.     /**
  55.      * ApiCall constructor.
  56.      *
  57.      * @param Configuration $config
  58.      */
  59.     public function __construct(Configuration $config)
  60.     {
  61.         $this->config        $config;
  62.         $this->logger        $config->getLogger();
  63.         $this->client        $config->getClient();
  64.         static::$nodes       $this->config->getNodes();
  65.         static::$nearestNode $this->config->getNearestNode();
  66.         $this->nodeIndex     0;
  67.         $this->initializeNodes();
  68.     }
  69.     /**
  70.      *  Initialize Nodes
  71.      */
  72.     private function initializeNodes(): void
  73.     {
  74.         if (static::$nearestNode !== null) {
  75.             $this->setNodeHealthCheck(static::$nearestNodetrue);
  76.         }
  77.         foreach (static::$nodes as &$node) {
  78.             $this->setNodeHealthCheck($nodetrue);
  79.         }
  80.     }
  81.     /**
  82.      * @param string $endPoint
  83.      * @param array $params
  84.      * @param bool $asJson
  85.      *
  86.      * @return string|array
  87.      * @throws TypesenseClientError
  88.      * @throws Exception|HttpClientException
  89.      */
  90.     public function get(string $endPoint, array $paramsbool $asJson true)
  91.     {
  92.         return $this->makeRequest('get'$endPoint$asJson, [
  93.             'query' => $params ?? [],
  94.         ]);
  95.     }
  96.     /**
  97.      * @param string $endPoint
  98.      * @param mixed $body
  99.      *
  100.      * @param bool $asJson
  101.      * @param array $queryParameters
  102.      *
  103.      * @return array|string
  104.      * @throws TypesenseClientError
  105.      * @throws HttpClientException
  106.      */
  107.     public function post(string $endPoint$bodybool $asJson true, array $queryParameters = [])
  108.     {
  109.         return $this->makeRequest('post'$endPoint$asJson, [
  110.             'data' => $body ?? [],
  111.             'query' => $queryParameters ?? []
  112.         ]);
  113.     }
  114.     /**
  115.      * @param string $endPoint
  116.      * @param array $body
  117.      *
  118.      * @param bool $asJson
  119.      * @param array $queryParameters
  120.      *
  121.      * @return array
  122.      * @throws TypesenseClientError|HttpClientException
  123.      */
  124.     public function put(string $endPoint, array $bodybool $asJson true, array $queryParameters = []): array
  125.     {
  126.         return $this->makeRequest('put'$endPoint$asJson, [
  127.             'data' => $body ?? [],
  128.             'query' => $queryParameters ?? []
  129.         ]);
  130.     }
  131.     /**
  132.      * @param string $endPoint
  133.      * @param array $body
  134.      *
  135.      * @param bool $asJson
  136.      * @param array $queryParameters
  137.      *
  138.      * @return array
  139.      * @throws TypesenseClientError|HttpClientException
  140.      */
  141.     public function patch(string $endPoint, array $bodybool $asJson true, array $queryParameters = []): array
  142.     {
  143.         return $this->makeRequest('patch'$endPoint$asJson, [
  144.             'data' => $body ?? [],
  145.             'query' => $queryParameters ?? []
  146.         ]);
  147.     }
  148.     /**
  149.      * @param string $endPoint
  150.      *
  151.      * @param bool $asJson
  152.      * @param array $queryParameters
  153.      *
  154.      * @return array
  155.      * @throws TypesenseClientError|HttpClientException
  156.      */
  157.     public function delete(string $endPointbool $asJson true, array $queryParameters = []): array
  158.     {
  159.         return $this->makeRequest('delete'$endPoint$asJson, [
  160.             'query' => $queryParameters ?? []
  161.         ]);
  162.     }
  163.     /**
  164.      * Makes the actual http request, along with retries
  165.      *
  166.      * @param string $method
  167.      * @param string $endPoint
  168.      * @param bool $asJson
  169.      * @param array $options
  170.      *
  171.      * @return string|array
  172.      * @throws TypesenseClientError|HttpClientException
  173.      * @throws Exception
  174.      */
  175.     private function makeRequest(string $methodstring $endPointbool $asJson, array $options)
  176.     {
  177.         $numRetries    0;
  178.         $lastException null;
  179.         while ($numRetries $this->config->getNumRetries() + 1) {
  180.             $numRetries++;
  181.             $node $this->getNode();
  182.             try {
  183.                 $url   $node->url() . $endPoint;
  184.                 $reqOp $this->getRequestOptions();
  185.                 if (isset($options['data'])) {
  186.                     if (is_string($options['data']) || $options['data'] instanceof StreamInterface) {
  187.                         $reqOp['body'] = $options['data'];
  188.                     } else {
  189.                         $reqOp['body'] = \json_encode($options['data']);
  190.                     }
  191.                 }
  192.                 if (isset($options['query'])) {
  193.                     foreach ($options['query'] as $key => $value) :
  194.                         if (is_bool($value)) {
  195.                             $options['query'][$key] = ($value) ? 'true' 'false';
  196.                         }
  197.                     endforeach;
  198.                     $reqOp['query'] = http_build_query($options['query']);
  199.                 }
  200.                 $response $this->client->send(
  201.                     \strtoupper($method),
  202.                     $url '?' . ($reqOp['query'] ?? ''),
  203.                     $reqOp['headers'] ?? [],
  204.                     $reqOp['body'] ?? null
  205.                 );
  206.                 $statusCode $response->getStatusCode();
  207.                 if ($statusCode && $statusCode 500) {
  208.                     $this->setNodeHealthCheck($nodetrue);
  209.                 }
  210.                 if (!(200 <= $statusCode && $statusCode 300)) {
  211.                     $errorMessage json_decode($response->getBody()
  212.                             ->getContents(), true512JSON_THROW_ON_ERROR)['message'] ?? 'API error.';
  213.                     throw $this->getException($statusCode)
  214.                         ->setMessage($errorMessage);
  215.                 }
  216.                 return $asJson json_decode($response->getBody()
  217.                     ->getContents(), true512JSON_THROW_ON_ERROR) : $response->getBody()
  218.                     ->getContents();
  219.             } catch (HttpException $exception) {
  220.                 if (
  221.                     $exception->getResponse()
  222.                         ->getStatusCode() === 408
  223.                 ) {
  224.                     continue;
  225.                 }
  226.                 $this->setNodeHealthCheck($nodefalse);
  227.                 throw $this->getException($exception->getResponse()
  228.                     ->getStatusCode())
  229.                     ->setMessage($exception->getMessage());
  230.             } catch (TypesenseClientError HttpClientException $exception) {
  231.                 $this->setNodeHealthCheck($nodefalse);
  232.                 throw $exception;
  233.             } catch (Exception $exception) {
  234.                 $this->setNodeHealthCheck($nodefalse);
  235.                 $lastException $exception;
  236.                 sleep($this->config->getRetryIntervalSeconds());
  237.             }
  238.         }
  239.         if ($lastException) {
  240.             throw $lastException;
  241.         }
  242.     }
  243.     /**
  244.      * @return array
  245.      */
  246.     private function getRequestOptions(): array
  247.     {
  248.         return [
  249.             'headers' => [
  250.                 static::API_KEY_HEADER_NAME => $this->config->getApiKey(),
  251.             ]
  252.         ];
  253.     }
  254.     /**
  255.      * @param Node $node
  256.      *
  257.      * @return bool
  258.      */
  259.     private function nodeDueForHealthCheck(Node $node): bool
  260.     {
  261.         $currentTimestamp time();
  262.         return ($currentTimestamp $node->getLastAccessTs()) > $this->config->getHealthCheckIntervalSeconds();
  263.     }
  264.     /**
  265.      * @param Node $node
  266.      * @param bool $isHealthy
  267.      */
  268.     public function setNodeHealthCheck(Node $nodebool $isHealthy): void
  269.     {
  270.         $node->setHealthy($isHealthy);
  271.         $node->setLastAccessTs(time());
  272.     }
  273.     /**
  274.      * Returns a healthy host from the pool in a round-robin fashion
  275.      * Might return an unhealthy host periodically to check for recovery.
  276.      *
  277.      * @return Node
  278.      */
  279.     public function getNode(): Lib\Node
  280.     {
  281.         if (static::$nearestNode !== null) {
  282.             if (static::$nearestNode->isHealthy() || $this->nodeDueForHealthCheck(static::$nearestNode)) {
  283.                 return static::$nearestNode;
  284.             }
  285.         }
  286.         $i 0;
  287.         while ($i count(static::$nodes)) {
  288.             $i++;
  289.             $node            = static::$nodes[$this->nodeIndex];
  290.             $this->nodeIndex = ($this->nodeIndex 1) % count(static::$nodes);
  291.             if ($node->isHealthy() || $this->nodeDueForHealthCheck($node)) {
  292.                 return $node;
  293.             }
  294.         }
  295.         /**
  296.          * None of the nodes are marked healthy, but some of them could have become healthy since last health check.
  297.          * So we will just return the next node.
  298.          */
  299.         return static::$nodes[$this->nodeIndex];
  300.     }
  301.     /**
  302.      * @param int $httpCode
  303.      *
  304.      * @return TypesenseClientError
  305.      */
  306.     public function getException(int $httpCode): TypesenseClientError
  307.     {
  308.         switch ($httpCode) {
  309.             case 0:
  310.                 return new HTTPStatus0Error();
  311.             case 400:
  312.                 return new RequestMalformed();
  313.             case 401:
  314.                 return new RequestUnauthorized();
  315.             case 404:
  316.                 return new ObjectNotFound();
  317.             case 409:
  318.                 return new ObjectAlreadyExists();
  319.             case 422:
  320.                 return new ObjectUnprocessable();
  321.             case 500:
  322.                 return new ServerError();
  323.             case 503:
  324.                 return new ServiceUnavailable();
  325.             default:
  326.                 return new TypesenseClientError();
  327.         }
  328.     }
  329.     /**
  330.      * @return LoggerInterface
  331.      */
  332.     public function getLogger()
  333.     {
  334.         return $this->logger;
  335.     }
  336. }