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:
- composer self-update && composer --version
- 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;';
- psql -U postgres -c 'CREATE DATABASE yiitest;';
- echo 'elasticsearch version ' && curl http://localhost:9200/

127
extensions/elasticsearch/Connection.php

@ -9,8 +9,8 @@ namespace yii\elasticsearch;
use Yii;
use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\helpers\Json;
/**
* 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>
* @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
@ -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
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()
{
@ -92,7 +105,7 @@ abstract class Connection extends Component
if (strncmp($host, 'inet[/', 6) == 0) {
$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'];
if (empty($this->nodes)) {
throw new Exception('cluster autodetection did not find any active node.');
@ -161,34 +174,34 @@ abstract class Connection extends Component
return new QueryBuilder($this);
}
public function get($url, $options = [], $body = null, $validCodes = [])
public function get($url, $options = [], $body = null)
{
$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)
{
$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)
{
$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)
{
$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)
{
$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 = [])
@ -201,10 +214,102 @@ abstract class Connection extends Component
$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()
{

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)
{
if (is_array($fields)) {
if (is_array($fields) || $fields === null) {
$this->fields = $fields;
} else {
$this->fields = func_get_args();

3
extensions/elasticsearch/composer.json

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

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

@ -3,7 +3,6 @@
namespace yiiunit\extensions\elasticsearch;
use yii\elasticsearch\Connection;
use yii\elasticsearch\GuzzleConnection;
/**
* @group elasticsearch
@ -12,7 +11,7 @@ class ElasticSearchConnectionTest extends ElasticSearchTestCase
{
public function testOpen()
{
$connection = new GuzzleConnection();
$connection = new Connection();
$connection->autodetectCluster;
$connection->nodes = [
['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\elasticsearch\Connection;
use yii\elasticsearch\GuzzleConnection;
use yiiunit\TestCase;
Yii::setAlias('@yii/elasticsearch', __DIR__ . '/../../../../extensions/elasticsearch');
@ -43,7 +42,7 @@ class ElasticSearchTestCase extends TestCase
{
$databases = $this->getParam('databases');
$params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array();
$db = new GuzzleConnection();
$db = new Connection();
if ($reset) {
$db->open();
}

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

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

Loading…
Cancel
Save