ClientBuilder.php 19 KB

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