vendor/elasticsearch/elasticsearch/src/Elasticsearch/ClientBuilder.php line 500

Open in your IDE?
  1. <?php
  2. declare(strict_types 1);
  3. namespace Elasticsearch;
  4. use Elasticsearch\Common\Exceptions\InvalidArgumentException;
  5. use Elasticsearch\Common\Exceptions\RuntimeException;
  6. use Elasticsearch\Common\Exceptions\ElasticCloudIdParseException;
  7. use Elasticsearch\Common\Exceptions\AuthenticationConfigException;
  8. use Elasticsearch\ConnectionPool\AbstractConnectionPool;
  9. use Elasticsearch\ConnectionPool\Selectors\RoundRobinSelector;
  10. use Elasticsearch\ConnectionPool\Selectors\SelectorInterface;
  11. use Elasticsearch\ConnectionPool\StaticNoPingConnectionPool;
  12. use Elasticsearch\Connections\Connection;
  13. use Elasticsearch\Connections\ConnectionFactory;
  14. use Elasticsearch\Connections\ConnectionFactoryInterface;
  15. use Elasticsearch\Namespaces\NamespaceBuilderInterface;
  16. use Elasticsearch\Serializers\SerializerInterface;
  17. use Elasticsearch\ConnectionPool\Selectors;
  18. use Elasticsearch\Serializers\SmartSerializer;
  19. use GuzzleHttp\Ring\Client\CurlHandler;
  20. use GuzzleHttp\Ring\Client\CurlMultiHandler;
  21. use GuzzleHttp\Ring\Client\Middleware;
  22. use Psr\Log\LoggerInterface;
  23. use Psr\Log\NullLogger;
  24. use Monolog\Logger;
  25. use Monolog\Handler\StreamHandler;
  26. use Monolog\Processor\IntrospectionProcessor;
  27. /**
  28.  * Class ClientBuilder
  29.  *
  30.  * @category Elasticsearch
  31.  * @package  Elasticsearch\Common\Exceptions
  32.  * @author   Zachary Tong <zach@elastic.co>
  33.  * @license  http://www.apache.org/licenses/LICENSE-2.0 Apache2
  34.  * @link     http://elastic.co
  35.  */
  36. class ClientBuilder
  37. {
  38.     /**
  39.      * @var Transport
  40.      */
  41.     private $transport;
  42.     /**
  43.      * @var callable
  44.      */
  45.     private $endpoint;
  46.     /**
  47.      * @var NamespaceBuilderInterface[]
  48.      */
  49.     private $registeredNamespacesBuilders = [];
  50.     /**
  51.      * @var ConnectionFactoryInterface
  52.      */
  53.     private $connectionFactory;
  54.     private $handler;
  55.     /**
  56.      * @var LoggerInterface
  57.      */
  58.     private $logger;
  59.     /**
  60.      * @var LoggerInterface
  61.      */
  62.     private $tracer;
  63.     /**
  64.      * @var string
  65.      */
  66.     private $connectionPool StaticNoPingConnectionPool::class;
  67.     /**
  68.      * @var string
  69.      */
  70.     private $serializer SmartSerializer::class;
  71.     /**
  72.      * @var string
  73.      */
  74.     private $selector RoundRobinSelector::class;
  75.     /**
  76.      * @var array
  77.      */
  78.     private $connectionPoolArgs = [
  79.         'randomizeHosts' => true
  80.     ];
  81.     /**
  82.      * @var array
  83.      */
  84.     private $hosts;
  85.     /**
  86.      * @var array
  87.      */
  88.     private $connectionParams;
  89.     /**
  90.      * @var int
  91.      */
  92.     private $retries;
  93.     /**
  94.      * @var bool
  95.      */
  96.     private $sniffOnStart false;
  97.     /**
  98.      * @var null|array
  99.      */
  100.     private $sslCert null;
  101.     /**
  102.      * @var null|array
  103.      */
  104.     private $sslKey null;
  105.     /**
  106.      * @var null|bool|string
  107.      */
  108.     private $sslVerification null;
  109.     public static function create(): ClientBuilder
  110.     {
  111.         return new static();
  112.     }
  113.     /**
  114.      * Can supply first parm to Client::__construct() when invoking manually or with dependency injection
  115.      */
  116.     public function getTransport(): Transport
  117.     {
  118.         return $this->transport;
  119.     }
  120.     /**
  121.      * Can supply second parm to Client::__construct() when invoking manually or with dependency injection
  122.      */
  123.     public function getEndpoint(): callable
  124.     {
  125.         return $this->endpoint;
  126.     }
  127.     /**
  128.      * Can supply third parm to Client::__construct() when invoking manually or with dependency injection
  129.      *
  130.      * @return NamespaceBuilderInterface[]
  131.      */
  132.     public function getRegisteredNamespacesBuilders(): array
  133.     {
  134.         return $this->registeredNamespacesBuilders;
  135.     }
  136.     /**
  137.      * Build a new client from the provided config.  Hash keys
  138.      * should correspond to the method name e.g. ['connectionPool']
  139.      * corresponds to setConnectionPool().
  140.      *
  141.      * Missing keys will use the default for that setting if applicable
  142.      *
  143.      * Unknown keys will throw an exception by default, but this can be silenced
  144.      * by setting `quiet` to true
  145.      *
  146.      * @param  bool $quiet False if unknown settings throw exception, true to silently
  147.      *                     ignore unknown settings
  148.      * @throws Common\Exceptions\RuntimeException
  149.      */
  150.     public static function fromConfig(array $configbool $quiet false): Client
  151.     {
  152.         $builder = new self;
  153.         foreach ($config as $key => $value) {
  154.             $method "set$key";
  155.             if (method_exists($builder$method)) {
  156.                 $builder->$method($value);
  157.                 unset($config[$key]);
  158.             }
  159.         }
  160.         if ($quiet === false && count($config) > 0) {
  161.             $unknown implode(array_keys($config));
  162.             throw new RuntimeException("Unknown parameters provided: $unknown");
  163.         }
  164.         return $builder->build();
  165.     }
  166.     /**
  167.      * @throws \RuntimeException
  168.      */
  169.     public static function defaultHandler(array $multiParams = [], array $singleParams = []): callable
  170.     {
  171.         $future null;
  172.         if (extension_loaded('curl')) {
  173.             $config array_merge([ 'mh' => curl_multi_init() ], $multiParams);
  174.             if (function_exists('curl_reset')) {
  175.                 $default = new CurlHandler($singleParams);
  176.                 $future = new CurlMultiHandler($config);
  177.             } else {
  178.                 $default = new CurlMultiHandler($config);
  179.             }
  180.         } else {
  181.             throw new \RuntimeException('Elasticsearch-PHP requires cURL, or a custom HTTP handler.');
  182.         }
  183.         return $future Middleware::wrapFuture($default$future) : $default;
  184.     }
  185.     /**
  186.      * @throws \RuntimeException
  187.      */
  188.     public static function multiHandler(array $params = []): CurlMultiHandler
  189.     {
  190.         if (function_exists('curl_multi_init')) {
  191.             return new CurlMultiHandler(array_merge([ 'mh' => curl_multi_init() ], $params));
  192.         } else {
  193.             throw new \RuntimeException('CurlMulti handler requires cURL.');
  194.         }
  195.     }
  196.     /**
  197.      * @throws \RuntimeException
  198.      */
  199.     public static function singleHandler(): CurlHandler
  200.     {
  201.         if (function_exists('curl_reset')) {
  202.             return new CurlHandler();
  203.         } else {
  204.             throw new \RuntimeException('CurlSingle handler requires cURL.');
  205.         }
  206.     }
  207.     public function setConnectionFactory(ConnectionFactoryInterface $connectionFactory): ClientBuilder
  208.     {
  209.         $this->connectionFactory $connectionFactory;
  210.         return $this;
  211.     }
  212.     /**
  213.      * @param  AbstractConnectionPool|string $connectionPool
  214.      * @throws \InvalidArgumentException
  215.      */
  216.     public function setConnectionPool($connectionPool, array $args = []): ClientBuilder
  217.     {
  218.         if (is_string($connectionPool)) {
  219.             $this->connectionPool $connectionPool;
  220.             $this->connectionPoolArgs $args;
  221.         } elseif (is_object($connectionPool)) {
  222.             $this->connectionPool $connectionPool;
  223.         } else {
  224.             throw new InvalidArgumentException("Serializer must be a class path or instantiated object extending AbstractConnectionPool");
  225.         }
  226.         return $this;
  227.     }
  228.     public function setEndpoint(callable $endpoint): ClientBuilder
  229.     {
  230.         $this->endpoint $endpoint;
  231.         return $this;
  232.     }
  233.     public function registerNamespace(NamespaceBuilderInterface $namespaceBuilder): ClientBuilder
  234.     {
  235.         $this->registeredNamespacesBuilders[] = $namespaceBuilder;
  236.         return $this;
  237.     }
  238.     public function setTransport(Transport $transport): ClientBuilder
  239.     {
  240.         $this->transport $transport;
  241.         return $this;
  242.     }
  243.     /**
  244.      * @param  mixed $handler
  245.      * @return $this
  246.      */
  247.     public function setHandler($handler): ClientBuilder
  248.     {
  249.         $this->handler $handler;
  250.         return $this;
  251.     }
  252.     public function setLogger(LoggerInterface $logger): ClientBuilder
  253.     {
  254.         if (!$logger instanceof LoggerInterface) {
  255.             throw new InvalidArgumentException('$logger must implement \Psr\Log\LoggerInterface!');
  256.         }
  257.         $this->logger $logger;
  258.         return $this;
  259.     }
  260.     public function setTracer(LoggerInterface $tracer): ClientBuilder
  261.     {
  262.         if (!$tracer instanceof LoggerInterface) {
  263.             throw new InvalidArgumentException('$tracer must implement \Psr\Log\LoggerInterface!');
  264.         }
  265.         $this->tracer $tracer;
  266.         return $this;
  267.     }
  268.     /**
  269.      * @param \Elasticsearch\Serializers\SerializerInterface|string $serializer
  270.      */
  271.     public function setSerializer($serializer): ClientBuilder
  272.     {
  273.         $this->parseStringOrObject($serializer$this->serializer'SerializerInterface');
  274.         return $this;
  275.     }
  276.     public function setHosts(array $hosts): ClientBuilder
  277.     {
  278.         $this->hosts $hosts;
  279.         return $this;
  280.     }
  281.     /**
  282.      * Set the APIKey Pair, consiting of the API Id and the ApiKey of the Response from /_security/api_key
  283.      *
  284.      * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html
  285.      *
  286.      * @throws Elasticsearch\Common\Exceptions\AuthenticationConfigException
  287.      */
  288.     public function setApiKey(string $idstring $apiKey): ClientBuilder
  289.     {
  290.         if (isset($this->connectionParams['client']['curl'][CURLOPT_HTTPAUTH]) === true) {
  291.             throw new AuthenticationConfigException("You can't use APIKey - and Basic Authenication together.");
  292.         }
  293.         $this->connectionParams['client']['headers']['Authorization'] = [
  294.             'ApiKey ' base64_encode($id ':' $apiKey)
  295.         ];
  296.         return $this;
  297.     }
  298.     /**
  299.      * Set the APIKey Pair, consiting of the API Id and the ApiKey of the Response from /_security/api_key
  300.      *
  301.      * @param string $username
  302.      * @param string $password
  303.      *
  304.      * @throws Elasticsearch\Common\Exceptions\AuthenticationConfigException
  305.      */
  306.     public function setBasicAuthentication(string $usernamestring $password): ClientBuilder
  307.     {
  308.         if (isset($this->connectionParams['client']['headers']['Authorization']) === true) {
  309.             throw new AuthenticationConfigException("You can't use APIKey - and Basic Authenication together.");
  310.         }
  311.         if (isset($this->connectionParams['client']['curl']) === false) {
  312.             $this->connectionParams['client']['curl'] = [];
  313.         }
  314.         $this->connectionParams['client']['curl'] += [
  315.             CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
  316.             CURLOPT_USERPWD  => $username.':'.$password
  317.         ];
  318.         return $this;
  319.     }
  320.     /**
  321.      * Set Elastic Cloud ID to connect to Elastic Cloud
  322.      *
  323.      * @link  https://elastic.co/cloud
  324.      *
  325.      * @param string $cloudId
  326.      */
  327.     public function setElasticCloudId(string $cloudId): ClientBuilder
  328.     {
  329.         // Register the Hosts array
  330.         $this->setHosts([
  331.             [
  332.                 'host'   => $this->parseElasticCloudId($cloudId),
  333.                 'port'   => '',
  334.                 'scheme' => 'https',
  335.             ]
  336.         ]);
  337.         // Merge best practices for the connection
  338.         $this->setConnectionParams([
  339.             'client' => [
  340.                 'curl' => [
  341.                     CURLOPT_ENCODING => 1,
  342.                 ],
  343.             ]
  344.         ]);
  345.         return $this;
  346.     }
  347.     public function setConnectionParams(array $params): ClientBuilder
  348.     {
  349.         $this->connectionParams $params;
  350.         return $this;
  351.     }
  352.     public function setRetries(int $retries): ClientBuilder
  353.     {
  354.         $this->retries $retries;
  355.         return $this;
  356.     }
  357.     /**
  358.      * @param \Elasticsearch\ConnectionPool\Selectors\SelectorInterface|string $selector
  359.      */
  360.     public function setSelector($selector): ClientBuilder
  361.     {
  362.         $this->parseStringOrObject($selector$this->selector'SelectorInterface');
  363.         return $this;
  364.     }
  365.     public function setSniffOnStart(bool $sniffOnStart): ClientBuilder
  366.     {
  367.         $this->sniffOnStart $sniffOnStart;
  368.         return $this;
  369.     }
  370.     /**
  371.      * @param string $cert The name of a file containing a PEM formatted certificate.
  372.      */
  373.     public function setSSLCert(string $certstring $password null): ClientBuilder
  374.     {
  375.         $this->sslCert = [$cert$password];
  376.         return $this;
  377.     }
  378.     /**
  379.      * @param string $key The name of a file containing a private SSL key.
  380.      */
  381.     public function setSSLKey(string $keystring $password null): ClientBuilder
  382.     {
  383.         $this->sslKey = [$key$password];
  384.         return $this;
  385.     }
  386.     /**
  387.      *  @param bool|string $value
  388.      */
  389.     public function setSSLVerification($value true): ClientBuilder
  390.     {
  391.         $this->sslVerification $value;
  392.         return $this;
  393.     }
  394.     public function build(): Client
  395.     {
  396.         $this->buildLoggers();
  397.         if (is_null($this->handler)) {
  398.             $this->handler ClientBuilder::defaultHandler();
  399.         }
  400.         $sslOptions null;
  401.         if (isset($this->sslKey)) {
  402.             $sslOptions['ssl_key'] = $this->sslKey;
  403.         }
  404.         if (isset($this->sslCert)) {
  405.             $sslOptions['cert'] = $this->sslCert;
  406.         }
  407.         if (isset($this->sslVerification)) {
  408.             $sslOptions['verify'] = $this->sslVerification;
  409.         }
  410.         if (!is_null($sslOptions)) {
  411.             $sslHandler = function (callable $handler, array $sslOptions) {
  412.                 return function (array $request) use ($handler$sslOptions) {
  413.                     // Add our custom headers
  414.                     foreach ($sslOptions as $key => $value) {
  415.                         $request['client'][$key] = $value;
  416.                     }
  417.                     // Send the request using the handler and return the response.
  418.                     return $handler($request);
  419.                 };
  420.             };
  421.             $this->handler $sslHandler($this->handler$sslOptions);
  422.         }
  423.         if (is_null($this->serializer)) {
  424.             $this->serializer = new SmartSerializer();
  425.         } elseif (is_string($this->serializer)) {
  426.             $this->serializer = new $this->serializer;
  427.         }
  428.         if (is_null($this->connectionFactory)) {
  429.             if (is_null($this->connectionParams)) {
  430.                 $this->connectionParams = [];
  431.             }
  432.             // Make sure we are setting Content-Type and Accept (unless the user has explicitly
  433.             // overridden it
  434.             if (! isset($this->connectionParams['client']['headers'])) {
  435.                 $this->connectionParams['client']['headers'] = [];
  436.             }
  437.             if (! isset($this->connectionParams['client']['headers']['Content-Type'])) {
  438.                 $this->connectionParams['client']['headers']['Content-Type'] = ['application/json'];
  439.             }
  440.             if (! isset($this->connectionParams['client']['headers']['Accept'])) {
  441.                 $this->connectionParams['client']['headers']['Accept'] = ['application/json'];
  442.             }
  443.             $this->connectionFactory = new ConnectionFactory($this->handler$this->connectionParams$this->serializer$this->logger$this->tracer);
  444.         }
  445.         if (is_null($this->hosts)) {
  446.             $this->hosts $this->getDefaultHost();
  447.         }
  448.         if (is_null($this->selector)) {
  449.             $this->selector = new RoundRobinSelector();
  450.         } elseif (is_string($this->selector)) {
  451.             $this->selector = new $this->selector;
  452.         }
  453.         $this->buildTransport();
  454.         if (is_null($this->endpoint)) {
  455.             $serializer $this->serializer;
  456.             $this->endpoint = function ($class) use ($serializer) {
  457.                 $fullPath '\\Elasticsearch\\Endpoints\\' $class;
  458.                 if ($class === 'Bulk' || $class === 'Msearch' || $class === 'MsearchTemplate' || $class === 'MPercolate') {
  459.                     return new $fullPath($serializer);
  460.                 } else {
  461.                     return new $fullPath();
  462.                 }
  463.             };
  464.         }
  465.         $registeredNamespaces = [];
  466.         foreach ($this->registeredNamespacesBuilders as $builder) {
  467.             /**
  468.  * @var NamespaceBuilderInterface $builder
  469. */
  470.             $registeredNamespaces[$builder->getName()] = $builder->getObject($this->transport$this->serializer);
  471.         }
  472.         return $this->instantiate($this->transport$this->endpoint$registeredNamespaces);
  473.     }
  474.     protected function instantiate(Transport $transport, callable $endpoint, array $registeredNamespaces): Client
  475.     {
  476.         return new Client($transport$endpoint$registeredNamespaces);
  477.     }
  478.     private function buildLoggers(): void
  479.     {
  480.         if (is_null($this->logger)) {
  481.             $this->logger = new NullLogger();
  482.         }
  483.         if (is_null($this->tracer)) {
  484.             $this->tracer = new NullLogger();
  485.         }
  486.     }
  487.     private function buildTransport(): void
  488.     {
  489.         $connections $this->buildConnectionsFromHosts($this->hosts);
  490.         if (is_string($this->connectionPool)) {
  491.             $this->connectionPool = new $this->connectionPool(
  492.                 $connections,
  493.                 $this->selector,
  494.                 $this->connectionFactory,
  495.                 $this->connectionPoolArgs
  496.             );
  497.         } elseif (is_null($this->connectionPool)) {
  498.             $this->connectionPool = new StaticNoPingConnectionPool(
  499.                 $connections,
  500.                 $this->selector,
  501.                 $this->connectionFactory,
  502.                 $this->connectionPoolArgs
  503.             );
  504.         }
  505.         if (is_null($this->retries)) {
  506.             $this->retries count($connections);
  507.         }
  508.         if (is_null($this->transport)) {
  509.             $this->transport = new Transport($this->retries$this->connectionPool$this->logger$this->sniffOnStart);
  510.         }
  511.     }
  512.     private function parseStringOrObject($arg, &$destination$interface): void
  513.     {
  514.         if (is_string($arg)) {
  515.             $destination = new $arg;
  516.         } elseif (is_object($arg)) {
  517.             $destination $arg;
  518.         } else {
  519.             throw new InvalidArgumentException("Serializer must be a class path or instantiated object implementing $interface");
  520.         }
  521.     }
  522.     private function getDefaultHost(): array
  523.     {
  524.         return ['localhost:9200'];
  525.     }
  526.     /**
  527.      * @return \Elasticsearch\Connections\Connection[]
  528.      * @throws RuntimeException
  529.      */
  530.     private function buildConnectionsFromHosts(array $hosts): array
  531.     {
  532.         $connections = [];
  533.         foreach ($hosts as $host) {
  534.             if (is_string($host)) {
  535.                 $host $this->prependMissingScheme($host);
  536.                 $host $this->extractURIParts($host);
  537.             } elseif (is_array($host)) {
  538.                 $host $this->normalizeExtendedHost($host);
  539.             } else {
  540.                 $this->logger->error("Could not parse host: ".print_r($hosttrue));
  541.                 throw new RuntimeException("Could not parse host: ".print_r($hosttrue));
  542.             }
  543.             $connections[] = $this->connectionFactory->create($host);
  544.         }
  545.         return $connections;
  546.     }
  547.     /**
  548.      * @throws RuntimeException
  549.      */
  550.     private function normalizeExtendedHost(array $host): array
  551.     {
  552.         if (isset($host['host']) === false) {
  553.             $this->logger->error("Required 'host' was not defined in extended format: ".print_r($hosttrue));
  554.             throw new RuntimeException("Required 'host' was not defined in extended format: ".print_r($hosttrue));
  555.         }
  556.         if (isset($host['scheme']) === false) {
  557.             $host['scheme'] = 'http';
  558.         }
  559.         if (isset($host['port']) === false) {
  560.             $host['port'] = 9200;
  561.         }
  562.         return $host;
  563.     }
  564.     /**
  565.      * @throws InvalidArgumentException
  566.      */
  567.     private function extractURIParts(string $host): array
  568.     {
  569.         $parts parse_url($host);
  570.         if ($parts === false) {
  571.             throw new InvalidArgumentException("Could not parse URI");
  572.         }
  573.         if (isset($parts['port']) !== true) {
  574.             $parts['port'] = 9200;
  575.         }
  576.         return $parts;
  577.     }
  578.     private function prependMissingScheme(string $host): string
  579.     {
  580.         if (!filter_var($hostFILTER_VALIDATE_URL)) {
  581.             $host 'http://' $host;
  582.         }
  583.         return $host;
  584.     }
  585.     /**
  586.      * Parse the Elastic Cloud Params from the CloudId
  587.      *
  588.      * @param string $cloudId
  589.      *
  590.      * @return string
  591.      *
  592.      * @throws ElasticCloudIdParseException
  593.      */
  594.     private function parseElasticCloudId(string $cloudId): string
  595.     {
  596.         try {
  597.             list($name$encoded) = explode(':'$cloudId);
  598.             list($uri$uuids)    = explode('$'base64_decode($encoded));
  599.             list($es,)            = explode(':'$uuids);
  600.             return $es '.' $uri;
  601.         } catch (\Throwable $t) {
  602.             throw new ElasticCloudIdParseException('could not parse the Cloud ID:' $cloudId);
  603.         }
  604.     }
  605. }