Browse Source

implemented connection based on CUrl

far better than depending on fat guzzle
tags/2.0.0-beta
Carsten Brandt 11 years ago
parent
commit
1c08c06da8
  1. 1
      .travis.yml
  2. 127
      extensions/elasticsearch/Connection.php
  3. 43
      extensions/elasticsearch/Exception.php
  4. 62
      extensions/elasticsearch/GuzzleConnection.php
  5. 2
      extensions/elasticsearch/Query.php
  6. 3
      extensions/elasticsearch/composer.json
  7. 3
      tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php
  8. 3
      tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php
  9. 3
      tests/unit/extensions/elasticsearch/QueryTest.php

1
.travis.yml

@ -12,7 +12,6 @@ services:
before_script: before_script:
- composer self-update && composer --version - composer self-update && composer --version
- composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist
- composer require guzzle/http v3.7.3 --dev --prefer-dist
- mysql -e 'CREATE DATABASE yiitest;'; - mysql -e 'CREATE DATABASE yiitest;';
- psql -U postgres -c 'CREATE DATABASE yiitest;'; - psql -U postgres -c 'CREATE DATABASE yiitest;';
- echo 'elasticsearch version ' && curl http://localhost:9200/ - echo 'elasticsearch version ' && curl http://localhost:9200/

127
extensions/elasticsearch/Connection.php

@ -9,8 +9,8 @@ namespace yii\elasticsearch;
use Yii; use Yii;
use yii\base\Component; use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\helpers\Json;
/** /**
* elasticsearch Connection is used to connect to an elasticsearch cluster version 0.20 or higher * elasticsearch Connection is used to connect to an elasticsearch cluster version 0.20 or higher
@ -18,7 +18,7 @@ use yii\base\InvalidConfigException;
* @author Carsten Brandt <mail@cebe.cc> * @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
abstract class Connection extends Component class Connection extends Component
{ {
/** /**
* @event Event an event that is triggered after a DB connection is established * @event Event an event that is triggered after a DB connection is established
@ -44,6 +44,19 @@ abstract class Connection extends Component
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth // TODO http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth
public $auth = []; public $auth = [];
/**
* @var float timeout to use for connecting to an elasticsearch node.
* This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option.
* If not set, no explicit timeout will be set for curl.
*/
public $connectionTimeout = null;
/**
* @var float timeout to use when reading the response from an elasticsearch node.
* This value will be used to configure the curl `CURLOPT_TIMEOUT` option.
* If not set, no explicit timeout will be set for curl.
*/
public $dataTimeout = null;
public function init() public function init()
{ {
@ -92,7 +105,7 @@ abstract class Connection extends Component
if (strncmp($host, 'inet[/', 6) == 0) { if (strncmp($host, 'inet[/', 6) == 0) {
$host = substr($host, 6, -1); $host = substr($host, 6, -1);
} }
$response = $this->httpRequest('get', 'http://' . $host . '/_cluster/nodes'); $response = $this->httpRequest('GET', 'http://' . $host . '/_cluster/nodes');
$this->nodes = $response['nodes']; $this->nodes = $response['nodes'];
if (empty($this->nodes)) { if (empty($this->nodes)) {
throw new Exception('cluster autodetection did not find any active node.'); throw new Exception('cluster autodetection did not find any active node.');
@ -161,34 +174,34 @@ abstract class Connection extends Component
return new QueryBuilder($this); return new QueryBuilder($this);
} }
public function get($url, $options = [], $body = null, $validCodes = []) public function get($url, $options = [], $body = null)
{ {
$this->open(); $this->open();
return $this->httpRequest('get', $this->createUrl($url, $options), $body); return $this->httpRequest('GET', $this->createUrl($url, $options), $body);
} }
public function head($url, $options = [], $body = null) public function head($url, $options = [], $body = null)
{ {
$this->open(); $this->open();
return $this->httpRequest('head', $this->createUrl($url, $options), $body); return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body);
} }
public function post($url, $options = [], $body = null) public function post($url, $options = [], $body = null)
{ {
$this->open(); $this->open();
return $this->httpRequest('post', $this->createUrl($url, $options), $body); return $this->httpRequest('POST', $this->createUrl($url, $options), $body);
} }
public function put($url, $options = [], $body = null) public function put($url, $options = [], $body = null)
{ {
$this->open(); $this->open();
return $this->httpRequest('put', $this->createUrl($url, $options), $body); return $this->httpRequest('PUT', $this->createUrl($url, $options), $body);
} }
public function delete($url, $options = [], $body = null) public function delete($url, $options = [], $body = null)
{ {
$this->open(); $this->open();
return $this->httpRequest('delete', $this->createUrl($url, $options), $body); return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body);
} }
private function createUrl($path, $options = []) private function createUrl($path, $options = [])
@ -201,10 +214,102 @@ abstract class Connection extends Component
$url .= '?' . http_build_query($options); $url .= '?' . http_build_query($options);
} }
return $url; $host = $this->nodes[$this->activeNode]['http_address'];
if (strncmp($host, 'inet[/', 6) == 0) {
$host = substr($host, 6, -1);
}
return 'http://' . $host . '/' . $url;
} }
protected abstract function httpRequest($type, $url, $body = null); protected function httpRequest($method, $url, $requestBody = null)
{
$method = strtoupper($method);
// response body and headers
$headers = [];
$body = '';
$options = [
CURLOPT_USERAGENT => 'Yii2 Framework ' . __CLASS__,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HEADER => false,
// http://www.php.net/manual/en/function.curl-setopt.php#82418
CURLOPT_HTTPHEADER => ['Expect:'],
CURLOPT_WRITEFUNCTION => function($curl, $data) use (&$body) {
$body .= $data;
return mb_strlen($data, '8bit');
},
CURLOPT_HEADERFUNCTION => function($curl, $data) use (&$headers) {
foreach(explode("\r\n", $data) as $row) {
if (($pos = strpos($row, ':')) !== false) {
$headers[strtolower(substr($row, 0, $pos))] = trim(substr($row, $pos + 1));
}
}
return mb_strlen($data, '8bit');
},
CURLOPT_CUSTOMREQUEST => $method,
];
if ($this->connectionTimeout !== null) {
$options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout;
}
if ($this->dataTimeout !== null) {
$options[CURLOPT_TIMEOUT] = $this->dataTimeout;
}
if ($requestBody !== null) {
$options[CURLOPT_POSTFIELDS] = $requestBody;
}
if ($method == 'HEAD') {
$options[CURLOPT_NOBODY] = true;
unset($options[CURLOPT_WRITEFUNCTION]);
}
$curl = curl_init($url);
curl_setopt_array($curl, $options);
curl_exec($curl);
$responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($responseCode >= 200 && $responseCode < 300) {
if ($method == 'HEAD') {
return true;
} else {
if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) {
throw new Exception("Incomplete data received from elasticsearch: $len < {$headers['content-length']}", [
'requestMethod' => $method,
'requestUrl' => $url,
'requestBody' => $requestBody,
'responseCode' => $responseCode,
'responseHeaders' => $headers,
'responseBody' => $body,
]);
}
if (isset($headers['content-type']) && !strncmp($headers['content-type'], 'application/json', 16)) {
return Json::decode($body);
}
throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [
'requestMethod' => $method,
'requestUrl' => $url,
'requestBody' => $requestBody,
'responseCode' => $responseCode,
'responseHeaders' => $headers,
'responseBody' => $body,
]);
}
} elseif ($responseCode == 404) {
return false;
} else {
throw new Exception("Elasticsearch request failed with code $responseCode.", [
'requestMethod' => $method,
'requestUrl' => $url,
'requestBody' => $requestBody,
'responseCode' => $responseCode,
'responseHeaders' => $headers,
'responseBody' => $body,
]);
}
}
public function getNodeInfo() public function getNodeInfo()
{ {

43
extensions/elasticsearch/Exception.php

@ -0,0 +1,43 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\elasticsearch;
/**
* Exception represents an exception that is caused by elasticsearch-related operations.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class Exception extends \yii\db\Exception
{
/**
* @var array additional information about the http request that caused the error.
*/
public $errorInfo = [];
/**
* Constructor.
* @param string $message error message
* @param array $errorInfo error info
* @param integer $code error code
* @param \Exception $previous The previous exception used for the exception chaining.
*/
public function __construct($message, $errorInfo = [], $code = 0, \Exception $previous = null)
{
$this->errorInfo = $errorInfo;
parent::__construct($message, $code, $previous);
}
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii', 'Elasticsearch Database Exception');
}
}

62
extensions/elasticsearch/GuzzleConnection.php

@ -1,62 +0,0 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\elasticsearch;
use Guzzle\Http\Exception\ClientErrorResponseException;
use yii\base\Exception;
use yii\helpers\Json;
/**
* Class GuzzleConnection
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class GuzzleConnection extends Connection
{
/**
* @var \Guzzle\Http\Client
*/
private $_http;
protected function httpRequest($type, $url, $body = null)
{
if ($this->_http === null) {
$this->_http = new \Guzzle\Http\Client('http://localhost:9200/');// TODO use active node
//$guzzle->setDefaultOption()
}
$requestOptions = [];
if ($type == 'head') {
$requestOptions['exceptions'] = false;
}
if ($type == 'get' && $body !== null) {
$type = 'post';
}
try{
$response = $this->_http->createRequest(
strtoupper($type)
, $url,
null,
$body,
$requestOptions
)->send();
} catch(ClientErrorResponseException $e) {
if ($e->getResponse()->getStatusCode() == 404) {
return false;
}
throw new Exception("elasticsearch error:\n\n"
. $body . "\n\n" . $e->getMessage()
. print_r(Json::decode($e->getResponse()->getBody(true)), true), 0, $e);
}
if ($type == 'head') {
return $response->getStatusCode() == 200;
}
return Json::decode($response->getBody(true));
}
}

2
extensions/elasticsearch/Query.php

@ -483,7 +483,7 @@ class Query extends Component implements QueryInterface
*/ */
public function fields($fields) public function fields($fields)
{ {
if (is_array($fields)) { if (is_array($fields) || $fields === null) {
$this->fields = $fields; $this->fields = $fields;
} else { } else {
$this->fields = func_get_args(); $this->fields = func_get_args();

3
extensions/elasticsearch/composer.json

@ -18,7 +18,8 @@
} }
], ],
"require": { "require": {
"yiisoft/yii2": "*" "yiisoft/yii2": "*",
"ext-curl": "*"
}, },
"autoload": { "autoload": {
"psr-0": { "yii\\elasticsearch\\": "" } "psr-0": { "yii\\elasticsearch\\": "" }

3
tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php

@ -3,7 +3,6 @@
namespace yiiunit\extensions\elasticsearch; namespace yiiunit\extensions\elasticsearch;
use yii\elasticsearch\Connection; use yii\elasticsearch\Connection;
use yii\elasticsearch\GuzzleConnection;
/** /**
* @group elasticsearch * @group elasticsearch
@ -12,7 +11,7 @@ class ElasticSearchConnectionTest extends ElasticSearchTestCase
{ {
public function testOpen() public function testOpen()
{ {
$connection = new GuzzleConnection(); $connection = new Connection();
$connection->autodetectCluster; $connection->autodetectCluster;
$connection->nodes = [ $connection->nodes = [
['http_address' => 'inet[/127.0.0.1:9200]'], ['http_address' => 'inet[/127.0.0.1:9200]'],

3
tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php

@ -4,7 +4,6 @@ namespace yiiunit\extensions\elasticsearch;
use Yii; use Yii;
use yii\elasticsearch\Connection; use yii\elasticsearch\Connection;
use yii\elasticsearch\GuzzleConnection;
use yiiunit\TestCase; use yiiunit\TestCase;
Yii::setAlias('@yii/elasticsearch', __DIR__ . '/../../../../extensions/elasticsearch'); Yii::setAlias('@yii/elasticsearch', __DIR__ . '/../../../../extensions/elasticsearch');
@ -43,7 +42,7 @@ class ElasticSearchTestCase extends TestCase
{ {
$databases = $this->getParam('databases'); $databases = $this->getParam('databases');
$params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array(); $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array();
$db = new GuzzleConnection(); $db = new Connection();
if ($reset) { if ($reset) {
$db->open(); $db->open();
} }

3
tests/unit/extensions/elasticsearch/QueryTest.php

@ -33,6 +33,9 @@ class QueryTest extends ElasticSearchTestCase
$query->fields(['name', 'status']); $query->fields(['name', 'status']);
$this->assertEquals(['name', 'status'], $query->fields); $this->assertEquals(['name', 'status'], $query->fields);
$query->fields('name', 'status');
$this->assertEquals(['name', 'status'], $query->fields);
$result = $query->one($this->getConnection()); $result = $query->one($this->getConnection());
$this->assertEquals(2, count($result['_source'])); $this->assertEquals(2, count($result['_source']));
$this->assertArrayHasKey('status', $result['_source']); $this->assertArrayHasKey('status', $result['_source']);

Loading…
Cancel
Save