diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7d4d6b3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +/.php_cs export-ignore +/.styleci.yml export-ignore +/.scrutinizer.yml export-ignore +/.travis.yml export-ignore +/Tests/ export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a398fd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.php_cs.cache +/.phpunit.result.cache +/composer.lock +/phpunit.xml +/vendor/ diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..4acc9ea --- /dev/null +++ b/.php_cs @@ -0,0 +1,14 @@ +in(__DIR__.'/src') + ->in(__DIR__.'/tests') +; + +return PhpCsFixer\Config::create() + ->setRules([ + '@Symfony' => true, + 'array_syntax' => ['syntax' => 'short'], + ]) + ->setFinder($finder) +; \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..28ef3ae --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,9 @@ +filter: + excluded_paths: [vendor/*, Tests/*] +checks: + php: + code_rating: true + duplication: true +tools: + external_code_coverage: + timeout: 1800 # Timeout in seconds. diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4c073ea --- /dev/null +++ b/.travis.yml @@ -0,0 +1,47 @@ +language: php +sudo: false +cache: + directories: + - $HOME/.composer/cache/files + +env: + global: + - PHPUNIT_FLAGS="-v" + +matrix: + fast_finish: true + include: + # Minimum supported dependencies with the latest and oldest PHP version + - php: 7.2 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" + - php: 7.3 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" + + # Test the latest stable release + - php: 7.2 + - php: 7.3 + env: COVERAGE=true PHPUNIT_FLAGS="-v --coverage-text --coverage-clover=coverage.xml" + + # Latest commit to master + - php: 7.3 + env: STABILITY="dev" + + allow_failures: + # Dev-master is allowed to fail. + - env: STABILITY="dev" + +before_install: + - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi + - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; + +install: + - composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction + +script: + - composer validate --strict --no-check-lock + - ./vendor/bin/phpunit $PHPUNIT_FLAGS + +after_success: + - if [[ $COVERAGE = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [[ $COVERAGE = true ]]; then php ocular.phar code-coverage:upload --format=php-clover coverage.xml; fi + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..299387f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Tobias Nyholm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 85bc19c..b634ca0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # elastica-dsn + Client factory with DSN support for ruflin/Elastica diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..da31cc7 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "happyr/elastica-dsn", + "description": "DSN support to ruflin/Elastica", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "require": { + "ruflin/elastica": "^6.1" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "phpunit/phpunit": "^8.2" + }, + "autoload": { + "psr-4": { + "Happyr\\ElasticaDsn\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\Happyr\\ElasticaDsn\\": "tests/" + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..cd8721b --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,33 @@ + + + + + + + + + + + ./tests + + + + + + ./ + + vendor + tests + + + + diff --git a/src/ClientFactory.php b/src/ClientFactory.php new file mode 100644 index 0000000..23ea238 --- /dev/null +++ b/src/ClientFactory.php @@ -0,0 +1,142 @@ + + * @author Rob Frawley 2nd + * @author Nicolas Grekas + */ +final class ClientFactory +{ + public static function create($servers, array $options = []): Client + { + return new Client(self::getConfig($servers, $options)); + } + + /** + * @param string|string[] $servers An array of servers, a DSN, or an array of DSNs + * @param array $options Valid keys are "username" and "password" + */ + public static function getConfig($servers, array $options = []): array + { + if (\is_string($servers)) { + $servers = [$servers]; + } elseif (!\is_array($servers)) { + throw new InvalidArgumentException(\sprintf('ClientFactory::create() expects array or string as first argument, %s given.', \gettype($servers))); + } + + \set_error_handler(function ($type, $msg, $file, $line) { + throw new \ErrorException($msg, 0, $type, $file, $line); + }); + try { + return self::doGetConfig($servers, $options); + } finally { + \restore_error_handler(); + } + } + + private static function doGetConfig(array $input, array $options): array + { + $servers = []; + $username = $options['username'] ?? null; + $password = $options['password'] ?? null; + + // parse any DSN in $servers + foreach ($input as $i => $dsn) { + if (\is_array($dsn)) { + continue; + } + if (0 !== \mb_strpos($dsn, 'elasticsearch:')) { + throw new InvalidArgumentException( + \sprintf('Invalid Elasticsearch DSN: %s does not start with "elasticsearch:"', $dsn) + ); + } + $params = \preg_replace_callback( + '#^elasticsearch:(//)?(?:([^@]*+)@)?#', + function ($m) use (&$username, &$password) { + if (!empty($m[2])) { + list($username, $password) = \explode(':', $m[2], 2) + [1 => null]; + } + + return 'file:'.($m[1] ?? ''); + }, + $dsn + ); + + if (false === $params = \parse_url($params)) { + throw new InvalidArgumentException(\sprintf('Invalid Elasticsearch DSN: %s', $dsn)); + } + + $query = $hosts = []; + if (isset($params['query'])) { + \parse_str($params['query'], $query); + + if (isset($query['host'])) { + if (!\is_array($hosts = $query['host'])) { + throw new InvalidArgumentException(\sprintf('Invalid Elasticsearch DSN: %s', $dsn)); + } + foreach ($hosts as $host => $value) { + if (false === $port = \mb_strrpos($host, ':')) { + $hosts[$host] = ['host' => $host, 'port' => 9200]; + } else { + $hosts[$host] = ['host' => \mb_substr($host, 0, $port), 'port' => (int) \mb_substr($host, 1 + $port)]; + } + } + $hosts = \array_values($hosts); + unset($query['host']); + } + if ($hosts && !isset($params['host']) && !isset($params['path'])) { + $servers = \array_merge($servers, $hosts); + continue; + } + } + + if (!isset($params['host']) && !isset($params['path'])) { + throw new InvalidArgumentException(\sprintf('Invalid Elasticsearch DSN: %s', $dsn)); + } + + if (isset($params['path']) && \preg_match('#/(\d+)$#', $params['path'], $m)) { + $params['path'] = \mb_substr($params['path'], 0, -\mb_strlen($m[0])); + } + + if (isset($params['path']) && \preg_match('#:(\d+)$#', $params['path'], $m)) { + $params['host'] = \mb_substr($params['path'], 0, -\mb_strlen($m[0])); + $params['port'] = $m[1]; + unset($params['path']); + } + + $params += [ + 'host' => $params['host'] ?? $params['path'], + 'port' => !isset($params['port']) ? 9200 : null, + ]; + if ($query) { + $params += $query; + $options = $query + $options; + } + + $servers[] = ['host' => $params['host'], 'port' => $params['port']]; + + if ($hosts) { + $servers = \array_merge($servers, $hosts); + } + } + + $config = ['servers' => $servers]; + if (null !== $username) { + $config['username'] = $username; + } + if (null !== $password) { + $config['password'] = $password; + } + + return $config; + } +} diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 0000000..5e93164 --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,9 @@ +assertEquals($expected, $output); + } + + public function getServers() + { + yield [['elasticsearch:localhost'], [], ['servers' => [['host' => 'localhost', 'port' => 9200]]]]; + yield [['elasticsearch:example.com'], [], ['servers' => [['host' => 'example.com', 'port' => 9200]]]]; + yield [['elasticsearch:localhost:1234'], [], ['servers' => [['host' => 'localhost', 'port' => 1234]]]]; + + yield [['elasticsearch:foo:bar@localhost:1234'], [], [ + 'username' => 'foo', + 'password' => 'bar', + 'servers' => [['host' => 'localhost', 'port' => 1234]], + ]]; + + yield [['elasticsearch:?host[localhost]&host[localhost:9201]&host[127.0.0.1:9202]'], [], [ + 'servers' => [ + ['host' => 'localhost', 'port' => 9200], + ['host' => 'localhost', 'port' => 9201], + ['host' => '127.0.0.1', 'port' => 9202], + ], + ]]; + yield [['elasticsearch:?host[localhost]&host[localhost:9201]&host[127.0.0.1:9202]', 'elasticsearch:localhost:1234'], [], [ + 'servers' => [ + ['host' => 'localhost', 'port' => 9200], + ['host' => 'localhost', 'port' => 9201], + ['host' => '127.0.0.1', 'port' => 9202], + ['host' => 'localhost', 'port' => 1234], + ], + ]]; + + yield [['elasticsearch:foo:bar@?host[localhost:9201]'], [], [ + 'username' => 'foo', + 'password' => 'bar', + 'servers' => [['host' => 'localhost', 'port' => 9201]], + ]]; + } +}