From d88bd25f2c027c33867c67b564aeb45c4d429552 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Sat, 17 Aug 2019 20:40:19 +0200 Subject: [PATCH] Support DSN in Client constructor for config argument (#1640) --- CHANGELOG.md | 3 + lib/Elastica/Client.php | 83 +++++----- lib/Elastica/ClientConfiguration.php | 189 ++++++++++++++++++++++ test/Elastica/ClientConfigurationTest.php | 179 ++++++++++++++++++++ test/Elastica/ClientTest.php | 31 +++- 5 files changed, 437 insertions(+), 48 deletions(-) create mode 100644 lib/Elastica/ClientConfiguration.php create mode 100644 test/Elastica/ClientConfigurationTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f3c5a4f6e..b14d6421e9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file based on the * hits.total is now an object in the search response [hits.total](https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-7.0.html#_literal_hits_total_literal_is_now_an_object_in_the_search_response) * Elastica\Reindex does not return an Index anymore but a Response. * Elastica\Reindex->run() does not refresh the new Index after completion anymore. Use `$reindex->setParam(Reindex::REFRESH, 'wait_for')` instead. +* Elastica\Client `$_config` field is now a `ClientConfiguration` instead of an array ### Bugfixes * Always set the Guzzle `base_uri` to support connecting to multiple ES hosts. [#1618](https://github.com/ruflin/Elastica/pull/1618) @@ -26,6 +27,8 @@ All notable changes to this project will be documented in this file based on the * Added `ParentAggregation` [#1616](https://github.com/ruflin/Elastica/pull/1616) * Elastica\Reindex missing options (script, remote, wait_for_completion, scroll...) * Added `AdjacencyMatrix` aggregation [#1642](https://github.com/ruflin/Elastica/pull/1642) +* Support string DSN in `\Elastica\Client` constructor for config argument [#1640](https://github.com/ruflin/Elastica/issues/1640) +* Move Client configuration in a dedicated class ### Improvements * Added `native_function_invocation` CS rule [#1606](https://github.com/ruflin/Elastica/pull/1606) diff --git a/lib/Elastica/Client.php b/lib/Elastica/Client.php index ca0b16d4c8..5f8ccab0aa 100644 --- a/lib/Elastica/Client.php +++ b/lib/Elastica/Client.php @@ -21,31 +21,9 @@ class Client { /** - * Config with defaults. - * - * log: Set to true, to enable logging, set a string to log to a specific file - * retryOnConflict: Use in \Elastica\Client::updateDocument - * bigintConversion: Set to true to enable the JSON bigint to string conversion option (see issue #717) - * - * @var array - */ - protected $_config = [ - 'host' => null, - 'port' => null, - 'path' => null, - 'url' => null, - 'proxy' => null, - 'transport' => null, - 'persistent' => true, - 'timeout' => null, - 'connections' => [], // host, port, path, timeout, transport, compression, persistent, timeout, username, password, config -> (curl, headers, url) - 'roundRobin' => false, - 'log' => false, - 'retryOnConflict' => 0, - 'bigintConversion' => false, - 'username' => null, - 'password' => null, - ]; + * @var ClientConfiguration + */ + protected $_config; /** * @var callback @@ -80,20 +58,30 @@ class Client /** * Creates a new Elastica client. * - * @param array $config OPTIONAL Additional config options + * @param array|string $config OPTIONAL Additional config or DSN of options * @param callback $callback OPTIONAL Callback function which can be used to be notified about errors (for example connection down) * @param LoggerInterface $logger + * + * @throws \Elastica\Exception\InvalidException */ - public function __construct(array $config = [], $callback = null, LoggerInterface $logger = null) + public function __construct($config = [], $callback = null, LoggerInterface $logger = null) { + if (\is_string($config)) { + $configuration = ClientConfiguration::fromDsn($config); + } elseif (\is_array($config)) { + $configuration = ClientConfiguration::fromArray($config); + } else { + throw new InvalidException('Config parameter must be an array or a string.'); + } + + $this->_config = $configuration; $this->_callback = $callback; - if (!$logger && isset($config['log']) && $config['log']) { - $logger = new Log($config['log']); + if (!$logger && $configuration->get('log')) { + $logger = new Log($configuration->get('log')); } $this->_logger = $logger ?: new NullLogger(); - $this->setConfig($config); $this->_initConnections(); } @@ -124,8 +112,9 @@ protected function _initConnections() $connections[] = Connection::create($this->_prepareConnectionParams($connection)); } - if (isset($this->_config['servers'])) { - foreach ($this->getConfig('servers') as $server) { + if ($this->_config->has('servers')) { + $servers = $this->_config->get('servers'); + foreach ($servers as $server) { $connections[] = Connection::create($this->_prepareConnectionParams($server)); } } @@ -135,7 +124,7 @@ protected function _initConnections() $connections[] = Connection::create($this->_prepareConnectionParams($this->getConfig())); } - if (!isset($this->_config['connectionStrategy'])) { + if (!$this->_config->has('connectionStrategy')) { if (true === $this->getConfig('roundRobin')) { $this->setConfigValue('connectionStrategy', 'RoundRobin'); } else { @@ -180,7 +169,7 @@ protected function _prepareConnectionParams(array $config) public function setConfig(array $config) { foreach ($config as $key => $value) { - $this->_config[$key] = $value; + $this->_config->set($key, $value); } return $this; @@ -198,15 +187,7 @@ public function setConfig(array $config) */ public function getConfig($key = '') { - if (empty($key)) { - return $this->_config; - } - - if (!\array_key_exists($key, $this->_config)) { - throw new InvalidException('Config key is not set: '.$key); - } - - return $this->_config[$key]; + return $this->_config->get($key); } /** @@ -230,7 +211,7 @@ public function setConfigValue($key, $value) */ public function getConfigValue($keys, $default = null) { - $value = $this->_config; + $value = $this->_config->getAll(); foreach ((array) $keys as $key) { if (isset($value[$key])) { $value = $value[$key]; @@ -267,7 +248,13 @@ public function getIndex($name) public function addHeader($header, $headerValue) { if (\is_string($header) && \is_string($headerValue)) { - $this->_config['headers'][$header] = $headerValue; + if ($this->_config->has('headers')) { + $headers = $this->_config->get('headers'); + } else { + $headers = []; + } + $headers[$header] = $headerValue; + $this->_config->set('headers', $headers); } else { throw new InvalidException('Header must be a string'); } @@ -287,8 +274,10 @@ public function addHeader($header, $headerValue) public function removeHeader($header) { if (\is_string($header)) { - if (\array_key_exists($header, $this->_config['headers'])) { - unset($this->_config['headers'][$header]); + if ($this->_config->has('headers')) { + $headers = $this->_config->get('headers'); + unset($headers[$header]); + $this->_config->set('headers', $headers); } } else { throw new InvalidException('Header must be a string'); diff --git a/lib/Elastica/ClientConfiguration.php b/lib/Elastica/ClientConfiguration.php new file mode 100644 index 0000000000..a2abb25013 --- /dev/null +++ b/lib/Elastica/ClientConfiguration.php @@ -0,0 +1,189 @@ + + */ +class ClientConfiguration +{ + /** + * Config with defaults. + * + * log: Set to true, to enable logging, set a string to log to a specific file + * retryOnConflict: Use in \Elastica\Client::updateDocument + * bigintConversion: Set to true to enable the JSON bigint to string conversion option (see issue #717) + * + * @var array + */ + protected $configuration = [ + 'host' => null, + 'port' => null, + 'path' => null, + 'url' => null, + 'proxy' => null, + 'transport' => null, + 'persistent' => true, + 'timeout' => null, + 'connections' => [], // host, port, path, timeout, transport, compression, persistent, timeout, username, password, config -> (curl, headers, url) + 'roundRobin' => false, + 'log' => false, + 'retryOnConflict' => 0, + 'bigintConversion' => false, + 'username' => null, + 'password' => null, + ]; + + /** + * Create configuration. + * + * @param array $config Additional config + * + * @return ClientConfiguration + */ + public static function fromArray(array $config): self + { + $clientConfiguration = new self(); + foreach ($config as $key => $value) { + $clientConfiguration->set($key, $value); + } + + return $clientConfiguration; + } + + /** + * Create configuration from Dsn string. + * + * @param string $dsn + * + * @return ClientConfiguration + */ + public static function fromDsn(string $dsn): self + { + if (false === $parsedDsn = \parse_url($dsn)) { + throw new InvalidException(\sprintf("DSN '%s' is invalid.", $dsn)); + } + + $clientConfiguration = new self(); + + if (isset($parsedDsn['scheme'])) { + $clientConfiguration->set('transport', $parsedDsn['scheme']); + } + + if (isset($parsedDsn['host'])) { + $clientConfiguration->set('host', $parsedDsn['host']); + } + + if (isset($parsedDsn['user'])) { + $clientConfiguration->set('username', \urldecode($parsedDsn['user'])); + } + + if (isset($parsedDsn['pass'])) { + $clientConfiguration->set('password', \urldecode($parsedDsn['pass'])); + } + + if (isset($parsedDsn['port'])) { + $clientConfiguration->set('port', $parsedDsn['port']); + } + + if (isset($parsedDsn['path'])) { + $clientConfiguration->set('path', $parsedDsn['path']); + } + + $options = []; + if (isset($parsedDsn['query'])) { + \parse_str($parsedDsn['query'], $options); + } + + foreach ($options as $optionName => $optionValue) { + if ('false' === $optionValue) { + $optionValue = false; + } elseif ('true' === $optionValue) { + $optionValue = true; + } elseif (\is_numeric($optionValue)) { + $optionValue = (int) $optionValue; + } + + $clientConfiguration->set($optionName, $optionValue); + } + + return $clientConfiguration; + } + + /** + * Returns a specific config key or the whole + * config array if not set. + * + * @param string $key Config key + * + * @throws \Elastica\Exception\InvalidException + * + * @return mixed Config value + */ + public function get(string $key) + { + if (empty($key)) { + return $this->configuration; + } + + if (!$this->has($key)) { + throw new InvalidException('Config key is not set: '.$key); + } + + return $this->configuration[$key]; + } + + /** + * Returns boolean indicates if configuration has key. + * + * @param string $key Key to check + * + * @return bool + */ + public function has(string $key): bool + { + return \array_key_exists($key, $this->configuration); + } + + /** + * Return all configuration. + * + * @return array + */ + public function getAll(): array + { + return $this->configuration; + } + + /** + * @param string $key Key to set + * @param mixed $value Value + */ + public function set(string $key, $value): void + { + $this->configuration[$key] = $value; + } + + /** + * Add value to a key. If original value is not an array, value is wrapped. + * + * @param string $key Key to add + * @param mixed $value Value + */ + public function add(string $key, $value): void + { + if (!\array_key_exists($key, $this->configuration)) { + $this->configuration[$key] = [$value]; + } else { + if (\is_array($this->configuration[$key])) { + $this->configuration[$key][] = $value; + } else { + $this->configuration[$key] = [$this->configuration[$key], $value]; + } + } + } +} diff --git a/test/Elastica/ClientConfigurationTest.php b/test/Elastica/ClientConfigurationTest.php new file mode 100644 index 0000000000..f1b17af356 --- /dev/null +++ b/test/Elastica/ClientConfigurationTest.php @@ -0,0 +1,179 @@ +expectException(\Elastica\Exception\InvalidException::class); + $this->expectExceptionMessage("DSN 'test:0' is invalid."); + + ClientConfiguration::fromDsn('test:0'); + } + + public function testFromSimpleDsn() + { + $configuration = ClientConfiguration::fromDsn('192.168.1.1:9201'); + + $expected = [ + 'host' => '192.168.1.1', + 'port' => '9201', + 'path' => null, + 'url' => null, + 'proxy' => null, + 'transport' => null, + 'persistent' => true, + 'timeout' => null, + 'connections' => [], + 'roundRobin' => false, + 'log' => false, + 'retryOnConflict' => 0, + 'bigintConversion' => false, + 'username' => null, + 'password' => null, + ]; + + $this->assertEquals($expected, $configuration->getAll()); + } + + public function testFromDsnWithParameters() + { + $configuration = ClientConfiguration::fromDsn('https://user:p4ss@foo.com:9201/my-path?proxy=https://proxy.com&persistent=false&timeout=45&roundRobin=true&log=true&retryOnConflict=2&bigintConversion=true&extra=abc'); + $expected = [ + 'host' => 'foo.com', + 'port' => '9201', + 'path' => '/my-path', + 'url' => null, + 'proxy' => 'https://proxy.com', + 'transport' => 'https', + 'persistent' => false, + 'timeout' => 45, + 'connections' => [], + 'roundRobin' => true, + 'log' => true, + 'retryOnConflict' => 2, + 'bigintConversion' => true, + 'username' => 'user', + 'password' => 'p4ss', + 'extra' => 'abc', + ]; + + $this->assertEquals($expected, $configuration->getAll()); + } + + public function testFromEmptyArray() + { + $configuration = ClientConfiguration::fromArray([]); + + $expected = [ + 'host' => null, + 'port' => null, + 'path' => null, + 'url' => null, + 'proxy' => null, + 'transport' => null, + 'persistent' => true, + 'timeout' => null, + 'connections' => [], // host, port, path, timeout, transport, compression, persistent, timeout, username, password, config -> (curl, headers, url) + 'roundRobin' => false, + 'log' => false, + 'retryOnConflict' => 0, + 'bigintConversion' => false, + 'username' => null, + 'password' => null, + ]; + + $this->assertEquals($expected, $configuration->getAll()); + } + + public function testFromArray() + { + $configuration = ClientConfiguration::fromArray([ + 'username' => 'Jdoe', + 'extra' => 'abc', + ]); + + $expected = [ + 'host' => null, + 'port' => null, + 'path' => null, + 'url' => null, + 'proxy' => null, + 'transport' => null, + 'persistent' => true, + 'timeout' => null, + 'connections' => [], // host, port, path, timeout, transport, compression, persistent, timeout, username, password, config -> (curl, headers, url) + 'roundRobin' => false, + 'log' => false, + 'retryOnConflict' => 0, + 'bigintConversion' => false, + 'username' => 'Jdoe', + 'password' => null, + 'extra' => 'abc', + ]; + + $this->assertEquals($expected, $configuration->getAll()); + } + + public function testHas() + { + $configuration = new ClientConfiguration(); + $this->assertTrue($configuration->has('host')); + $this->assertFalse($configuration->has('inexistantKey')); + } + + public function testGet() + { + $configuration = new ClientConfiguration(); + $this->assertTrue($configuration->get('persistent')); + + $expected = [ + 'host' => null, + 'port' => null, + 'path' => null, + 'url' => null, + 'proxy' => null, + 'transport' => null, + 'persistent' => true, + 'timeout' => null, + 'connections' => [], + 'roundRobin' => false, + 'log' => false, + 'retryOnConflict' => 0, + 'bigintConversion' => false, + 'username' => null, + 'password' => null, + ]; + + $this->assertEquals($expected, $configuration->get('')); + + $this->expectException(\Elastica\Exception\InvalidException::class); + $configuration->get('invalidKey'); + } + + public function testAdd() + { + $keyName = 'myKey'; + + $configuration = new ClientConfiguration(); + $this->assertFalse($configuration->has($keyName)); + + $configuration->add($keyName, 'FirstValue'); + $this->assertEquals(['FirstValue'], $configuration->get($keyName)); + + $configuration->add($keyName, 'SecondValue'); + $this->assertEquals(['FirstValue', 'SecondValue'], $configuration->get($keyName)); + + $configuration->set('otherKey', 'value'); + $this->assertEquals('value', $configuration->get('otherKey')); + $configuration->add('otherKey', 'nextValue'); + $this->assertEquals(['value', 'nextValue'], $configuration->get('otherKey')); + } +} diff --git a/test/Elastica/ClientTest.php b/test/Elastica/ClientTest.php index fb1500d06a..42c58d6e1d 100644 --- a/test/Elastica/ClientTest.php +++ b/test/Elastica/ClientTest.php @@ -17,7 +17,6 @@ use Elastica\Script\Script; use Elastica\Test\Base as BaseTest; use Elastica\Type; -use Elasticsearch\Endpoints\Get; use Elasticsearch\Endpoints\Indices\Stats; use Elasticsearch\Endpoints\Search; @@ -32,6 +31,36 @@ public function testConstruct() $this->assertCount(1, $client->getConnections()); } + /** + * @group unit + */ + public function testConstructWithDsn() + { + $client = new Client('https://user:p4ss@foo.com:9200?persistent=false&retryOnConflict=2'); + $this->assertCount(1, $client->getConnections()); + + $expected = [ + 'host' => 'foo.com', + 'port' => '9200', + 'path' => null, + 'url' => null, + 'proxy' => null, + 'transport' => 'https', + 'persistent' => false, + 'timeout' => null, + 'connections' => [], + 'roundRobin' => false, + 'log' => false, + 'retryOnConflict' => 2, + 'bigintConversion' => false, + 'username' => 'user', + 'password' => 'p4ss', + 'connectionStrategy' => 'Simple', + ]; + + $this->assertEquals($expected, $client->getConfig()); + } + /** * @group functional */