diff --git a/.travis.yml b/.travis.yml index 2cd2ad2..c2495e3 100644 --- a/.travis.yml +++ b/.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/ diff --git a/extensions/elasticsearch/Connection.php b/extensions/elasticsearch/Connection.php index efbf72f..bae13dc 100644 --- a/extensions/elasticsearch/Connection.php +++ b/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 * @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() { diff --git a/extensions/elasticsearch/Exception.php b/extensions/elasticsearch/Exception.php new file mode 100644 index 0000000..aa58338 --- /dev/null +++ b/extensions/elasticsearch/Exception.php @@ -0,0 +1,43 @@ + + * @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'); + } +} \ No newline at end of file diff --git a/extensions/elasticsearch/GuzzleConnection.php b/extensions/elasticsearch/GuzzleConnection.php deleted file mode 100644 index b34a944..0000000 --- a/extensions/elasticsearch/GuzzleConnection.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @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)); - } - -} \ No newline at end of file diff --git a/extensions/elasticsearch/Query.php b/extensions/elasticsearch/Query.php index 2ba1876..7da9051 100644 --- a/extensions/elasticsearch/Query.php +++ b/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(); diff --git a/extensions/elasticsearch/composer.json b/extensions/elasticsearch/composer.json index 9f5ed3a..c72cd81 100644 --- a/extensions/elasticsearch/composer.json +++ b/extensions/elasticsearch/composer.json @@ -18,7 +18,8 @@ } ], "require": { - "yiisoft/yii2": "*" + "yiisoft/yii2": "*", + "ext-curl": "*" }, "autoload": { "psr-0": { "yii\\elasticsearch\\": "" } diff --git a/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php b/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php index 9e37466..60b2428 100644 --- a/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php +++ b/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]'], diff --git a/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php b/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php index dc639d7..e0435a7 100644 --- a/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php +++ b/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(); } diff --git a/tests/unit/extensions/elasticsearch/QueryTest.php b/tests/unit/extensions/elasticsearch/QueryTest.php index a520433..da2558e 100644 --- a/tests/unit/extensions/elasticsearch/QueryTest.php +++ b/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']);