diff --git a/lib/triagens/ArangoDb/Connection.php b/lib/triagens/ArangoDb/Connection.php index 86b64946..3ddc3cb4 100644 --- a/lib/triagens/ArangoDb/Connection.php +++ b/lib/triagens/ArangoDb/Connection.php @@ -335,8 +335,14 @@ private function executeRequest($method, $url, $data) $traceFunc = $this->_options[ConnectionOptions::OPTION_TRACE]; if ($traceFunc) { + // call tracer func - $traceFunc('send', $request); + if($this->_options[ConnectionOptions::OPTION_ENHANCED_TRACE]){ + $parsed = HttpHelper::parseHttpMessage($request); + $traceFunc(new TraceRequest(HttpHelper::parseHeaders($parsed['header']), $method, $url, $data)); + }else{ + $traceFunc('send', $request); + } } // set socket timeout for this scope @@ -363,16 +369,22 @@ private function executeRequest($method, $url, $data) $scope->leave(); - if ($traceFunc) { - // call tracer func - $traceFunc('receive', $result); + if ($status['timed_out']) { + throw new ClientException('Got a timeout when waiting on the server\'s response'); } - if ($status['timed_out']) { - throw new ClientException('Got a timeout when waiting on the server\'s response'); + $response = new HttpResponse($result); + + if ($traceFunc) { + // call tracer func + if($this->_options[ConnectionOptions::OPTION_ENHANCED_TRACE]){ + $traceFunc(new TraceResponse($response->getHeaders(), $response->getHttpCode(), $response->getBody())); + }else{ + $traceFunc('receive', $result); + } } - return new HttpResponse($result); + return $response; } $scope->leave(); diff --git a/lib/triagens/ArangoDb/ConnectionOptions.php b/lib/triagens/ArangoDb/ConnectionOptions.php index a0fd9245..fa3adcf1 100644 --- a/lib/triagens/ArangoDb/ConnectionOptions.php +++ b/lib/triagens/ArangoDb/ConnectionOptions.php @@ -60,6 +60,11 @@ class ConnectionOptions implements */ const OPTION_TRACE = 'trace'; + /** + * Enhanced trace + */ + const OPTION_ENHANCED_TRACE = 'enhancedTrace'; + /** * "Create collections if they don't exist" index constant */ @@ -291,6 +296,7 @@ private static function getDefaults() self::OPTION_IS_VOLATILE => DefaultValues::DEFAULT_IS_VOLATILE, self::OPTION_CONNECTION => DefaultValues::DEFAULT_CONNECTION, self::OPTION_TRACE => null, + self::OPTION_ENHANCED_TRACE => false, self::OPTION_AUTH_USER => null, self::OPTION_AUTH_PASSWD => null, self::OPTION_AUTH_TYPE => null, diff --git a/lib/triagens/ArangoDb/HttpHelper.php b/lib/triagens/ArangoDb/HttpHelper.php index b14c6510..8ea172e7 100644 --- a/lib/triagens/ArangoDb/HttpHelper.php +++ b/lib/triagens/ArangoDb/HttpHelper.php @@ -235,4 +235,54 @@ public static function createConnection(ConnectionOptions $options) return $fp; } + + /** + * Splits a http message into its header and body. + * @param string $httpMessage The http message string. + * @throws ClientException + * @return array + */ + public static function parseHttpMessage($httpMessage) + { + assert(is_string($httpMessage)); + + $barrier = HttpHelper::EOL . HttpHelper::EOL; + $border = strpos($httpMessage, $barrier); + + if ($border === false) { + throw new ClientException('Got an invalid response from the server'); + } + + $result = array(); + + $result['header'] = substr($httpMessage, 0, $border); + $result['body'] = substr($httpMessage, $border + strlen($barrier)); + + return $result; + } + + /** + * Process a string of HTTP headers into an array of header => values. + * @param string $headers - the headers string + * @return array + */ + public static function parseHeaders($headers) + { + $processed = array(); + + foreach (explode(HttpHelper::EOL, $headers) as $lineNumber => $line) { + $line = trim($line); + + if ($lineNumber == 0) { + // first line of result is special, so discard it. + continue; + } else { + // other lines contain key:value-like headers + list($key, $value) = explode(':', $line, 2); + $processed[strtolower(trim($key))] = trim($value); + } + } + + return $processed; + } } diff --git a/lib/triagens/ArangoDb/HttpResponse.php b/lib/triagens/ArangoDb/HttpResponse.php index 6f7174dd..eb1df307 100644 --- a/lib/triagens/ArangoDb/HttpResponse.php +++ b/lib/triagens/ArangoDb/HttpResponse.php @@ -68,16 +68,10 @@ class HttpResponse */ public function __construct($responseString) { - assert(is_string($responseString)); + $parsed = HttpHelper::parseHttpMessage($responseString); - $barrier = HttpHelper::EOL . HttpHelper::EOL; - $border = strpos($responseString, $barrier); - if ($border === false) { - throw new ClientException('Got an invalid response from the server'); - } - - $this->_header = substr($responseString, 0, $border); - $this->_body = substr($responseString, $border + strlen($barrier)); + $this->_header = $parsed['header']; + $this->_body = $parsed['body']; $this->setupHeaders(); } @@ -178,6 +172,7 @@ public function getJson() */ private function setupHeaders() { + //Get the http status code from the first line foreach (explode(HttpHelper::EOL, $this->_header) as $lineNumber => $line) { $line = trim($line); @@ -187,11 +182,12 @@ private function setupHeaders() if (preg_match("/^HTTP\/\d+\.\d+\s+(\d+)/", $line, $matches)) { $this->_httpCode = (int) $matches[1]; } - } else { - // other lines contain key:value-like headers - list($key, $value) = explode(':', $line, 2); - $this->_headers[strtolower(trim($key))] = trim($value); + + break; } } + + //Process the rest of the headers + $this->_headers = HttpHelper::parseHeaders($this->_header); } } diff --git a/lib/triagens/ArangoDb/TraceRequest.php b/lib/triagens/ArangoDb/TraceRequest.php new file mode 100644 index 00000000..0646b693 --- /dev/null +++ b/lib/triagens/ArangoDb/TraceRequest.php @@ -0,0 +1,104 @@ + value) element + * @var array + */ + private $_headers = array(); + + /** + * Stores the http method + * @var string + */ + private $_method; + + /** + * Stores the request url + * @var string + */ + private $_requestUrl; + + /** + * Store the string of the body + * @var string + */ + private $_body; + + /** + * The http message type + * @var string + */ + private $_type = "request"; + + /** + * Set up the request trace + * @param array $headers - the array of http headers + * @param string $method - the request method + * @param string $requestUrl - the request url + * @param string $body - the string of http body + */ + public function __construct($headers, $method, $requestUrl, $body) + { + $this->_headers = $headers; + $this->_method = $method; + $this->_requestUrl = $requestUrl; + $this->_body = $body; + } + + /** + * Get an array of the request headers + * @return array + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Get the request method + * @return string + */ + public function getMethod() + { + return $this->_method; + } + + /** + * Get the request url + * @return string + */ + public function getRequestUrl() + { + return $this->_requestUrl; + } + + /** + * Get the body of the request + * @return string + */ + public function getBody() + { + return $this->_body; + } + + /** + * Get the http message type + * @return string + */ + public function getType() + { + return $this->_type; + } +} diff --git a/lib/triagens/ArangoDb/TraceResponse.php b/lib/triagens/ArangoDb/TraceResponse.php new file mode 100644 index 00000000..c3383641 --- /dev/null +++ b/lib/triagens/ArangoDb/TraceResponse.php @@ -0,0 +1,150 @@ + value) element + * @var array + */ + private $_headers = array(); + + /** + * The http status code + * @var int + */ + private $_httpCode; + + /** + * The raw body of the response + * @var string + */ + private $_body; + + /** + * The type of http message + * @var string + */ + private $_type = "response"; + + /** + * Used to look up the definition for an http code + * @var array + */ + private $_httpCodeDefinitions = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + ); + + /** + * Set up the response trace + * @param array $headers - the array of http headers + * @param int $httpCode - the http code + * @param string $body - the string of http body + */ + public function __construct($headers, $httpCode, $body) + { + $this->_headers = $headers; + $this->_httpCode = $httpCode; + $this->_body = $body; + } + + /** + * Get an array of the response headers + * @return array + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Get the http response code + * @return int + */ + public function getHttpCode() + { + return $this->_httpCode; + } + + /** + * Get the http code definition + * @param int $code - the http code + * @throws ClientException + * @return string + */ + public function getHttpCodeDefinition() + { + if(!isset($this->_httpCodeDefinitions[$this->getHttpCode()])){ + throw new ClientException("Invalid http code provided."); + } + + return $this->_httpCodeDefinitions[$this->getHttpCode()]; + } + + /** + * Get the response body + * @return string + */ + public function getBody() + { + return $this->_body; + } + + /** + * Get the http message type + * @return string + */ + public function getType() + { + return $this->_type; + } +} diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 64cbbfda..191c26d2 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -12,6 +12,19 @@ class ConnectionTest extends \PHPUnit_Framework_TestCase { + + public function setUp() + { + $this->connection = getConnection(); + $this->collectionHandler = new \triagens\ArangoDb\CollectionHandler($this->connection); + + try { + $this->collectionHandler->drop('ArangoDB_PHP_TestSuite_TestTracer'); + } catch (\Exception $e) { + //Silence the exception + } + } + /** * Test if Connection instance can be initialized */ @@ -46,4 +59,92 @@ public function testGetApiVersion() $response = $connection->getClientVersion(); $this->assertTrue($response !== "", 'Version String is empty!'); } + + /** + * Test the basic tracer + */ + public function testBasicTracer() + { + //Setup + $self = $this; //Hack for PHP 5.3 compatibility + $basicTracer = function($type, $data) use($self){ + $self->assertContains($type, array('send', 'receive'), 'Basic tracer\'s type should only be \'send\' or \'receive\''); + $self->assertInternalType('string', $data, 'Basic tracer data is not a string!.'); + }; + + $options = getConnectionOptions(); + $options[ConnectionOptions::OPTION_TRACE] = $basicTracer; + + $connection = new Connection($options); + $collectionHandler = new CollectionHandler($connection); + + //Try creating a collection + $collectionHandler->create('ArangoDB_PHP_TestSuite_TestTracer'); + + //Delete the collection + try { + $collectionHandler->drop('ArangoDB_PHP_TestSuite_TestTracer'); + } catch (Exception $e) { + } + } + + /** + * Test the enhanced tracer + */ + public function testEnhancedTracer() + { + //Setup + $self = $this; //Hack for PHP 5.3 compatibility + + $enhancedTracer = function($data) use($self){ + $self->assertTrue($data instanceof TraceRequest || $data instanceof TraceResponse, '$data must be instance of TraceRequest or TraceResponse.'); + + $self->assertInternalType('array', $data->getHeaders(), 'Headers should be an array!'); + $self->assertNotEmpty($data->getHeaders(), 'Headers should not be an empty array!'); + $self->assertInternalType('string', $data->getBody(), 'Body must be a string!'); + + if($data instanceof TraceRequest){ + $self->assertContains($data->getMethod(), + array(HttpHelper::METHOD_DELETE, HttpHelper::METHOD_GET, HttpHelper::METHOD_HEAD, HttpHelper::METHOD_PATCH, HttpHelper::METHOD_POST, HttpHelper::METHOD_PUT), + 'Invalid http method!' + ); + + $self->assertInternalType('string', $data->getRequestUrl(), 'Request url must be a string!'); + $self->assertEquals('request', $data->getType()); + }else{ + $self->assertInternalType('integer', $data->getHttpCode(), 'Http code must be an integer!'); + $self->assertInternalType('string', $data->getHttpCodeDefinition(), 'Http code definition must be a string!'); + $self->assertEquals('response', $data->getType()); + } + }; + + $options = getConnectionOptions(); + $options[ConnectionOptions::OPTION_TRACE] = $enhancedTracer; + $options[ConnectionOptions::OPTION_ENHANCED_TRACE] = true; + + $connection = new Connection($options); + $collectionHandler = new CollectionHandler($connection); + + //Try creating a collection + $collectionHandler->create('ArangoDB_PHP_TestSuite_TestTracer'); + + //Delete the collection + try { + $collectionHandler->drop('ArangoDB_PHP_TestSuite_TestTracer'); + } catch (Exception $e) { + } + } + + public function tearDown() + { + unset($this->connection); + + try { + $this->collectionHandler->drop('ArangoDB_PHP_TestSuite_TestTracer'); + } catch (\Exception $e) { + //Silence the exception + } + + unset($this->collectionHandler); + } }