diff --git a/composer.json b/composer.json index 16e7eb8a6c01c5bec7fc5d48105365295aede85f..240bf04a84ba5033ce0c6f7d9ade446ceb24327c 100644 --- a/composer.json +++ b/composer.json @@ -10,11 +10,22 @@ } ], "require": { - "php": ">=7.0.0" + "php": ">=7.0.0", + "guzzlehttp/guzzle": "^6.3" + }, + "autoload": { + "psr-4": { + "Library\\": "src/library" + } }, "require-dev": { "phpunit/phpunit": ">=4.8 < 6.0" }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, "scripts": { "test": "phpunit" } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000000000000000000000000000000000000..c9a0bb995e837590f018973b78fa383506f4441c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,10 @@ +<phpunit bootstrap="vendor/autoload.php"> + <testsuites> + <testsuite name="php-modules"> + <directory>tests</directory> + </testsuite> + </testsuites> + <php> + <env name="UNITTEST" value="yes"/> + </php> +</phpunit> diff --git a/src/library/Webquery.php b/src/library/Webquery.php new file mode 100644 index 0000000000000000000000000000000000000000..8a49bc8a88a5561f4b489cf542c75dca54e0be0d --- /dev/null +++ b/src/library/Webquery.php @@ -0,0 +1,321 @@ +<?php + +namespace Library; + +use GuzzleHttp\Psr7\Request; + + +class Webquery { + use \Library\LoggerTrait; + + /** + * @var array log context + */ + protected $context = ['webquery']; + /** + * @var \GuzzleHttp\Client psr-7 http client + */ + protected $http_client; + /** + * @var string webquery host name, default: 'library.wur.nl' + */ + protected $host = 'library.wur.nl'; + /** + * @var string webquery service name + */ + protected $service = ''; + /** + * @var string webquery suffix name, default: 'xml' + */ + protected $suffix = 'xml'; + /** + * @var GuzzleHttp\Psr7\Response + */ + protected $response; + /** + * @var SimpleXMLElement parsed xml from response + */ + protected $resp_xml; + /** + * @var array with children and attributes from error element + */ + protected $resp_error; + + + public function __construct() { + + } + + /** + * setter for http_client + * @param \GuzzleHttp\Client $http_client + * @return $this + */ + public function set_http_client(\GuzzleHttp\Client $http_client) { + + $this->http_client = $http_client; + return $this; + } + + /** + * getter for http_client + * if no http_client is set a default http_client is created + * @return \GuzzleHttp\Client + */ + public function get_http_client() { + + if (!isset($this->http_client)) { + $this->http_client = new \GuzzleHttp\Client(); + } + return $this->http_client; + } + + /** + * set host name for webquery url + * @param sting $host webquery host + * @return $this + */ + public function set_host($host) { + + $this->host = $host; + return $this; + } + + /** + * set webquery service name for url + * @param string $service webquery service + * @return $this + */ + public function set_service($service) { + + $this->service = $service; + return $this; + } + + /** + * set webquery suffix for url + * @param string $suffix webquery suffix + * @return $this + */ + public function set_suffix($suffix) { + + $this->suffix = $suffix; + return $this; + } + + public function get_url() { + + return "http://$this->host/WebQuery/$this->service/new_$this->suffix"; + } + +/** + * convert xml tree to x-www-form-urlencoded string + * @param SimpleXMLElement $xml xml tree + * @return string xml as x-www-form-urlencoded string + */ + public function xml_to_wwwform($xml) { + + $result = []; + foreach($xml as $key=>$value) { + $result[] = "$key=".($value->count() > 0 ? '&'.$this->xml_to_wwwform($value) : urlencode($value)); + } + return implode('&', $result); + } + + /** + * create new record in webquery + * uses properties host, service and suffix for the url + * @param SimpleXMLElement $xml post data (xmol tree) + * @return boolean true on success + */ + public function create_record($xml) { + + array_push($this->context, 'create_record'); + $this->reset_response(); + + if (empty($this->service)) { + $this->error("create_record: service is not set", $this->context); + array_pop($this->context); + return false; + } + + $url = $this->get_url(); + $headers = ['Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8']; + $body = $this->xml_to_wwwform($xml); + $this->debug("request url: $url", $this->context); + $this->debug("request data: $body", $this->context); + $request = new Request('POST', $url, $headers, $body); + $options = [ + 'timeout' => 2, + 'http_errors' => false, // disable exceptions on 4xx and 5xx responses + ]; + try { + $this->response = $this->http_client->send($request, $options); + } catch (\GuzzleHttp\Exception\RequestException $ex) { + $this->error("exception posting to webquery: ".$ex->getMessage(), $this->context); + $this->resp_error = [ + 'status' => '500', + 'code' => $ex->getCode(), + 'message' => 'request exception: '.$ex->getMessage(), + ]; + array_pop($this->context); + return false; + } + + $success = $this->is_success(); + $code = $this->get_error_code(); + $message = $this->get_error_message(); + if ($success) { + $this->info("success, code=$code, message=$message, isn=".$this->get_isn(), $this->context); + } else { + $this->error("failed, code=$code, message=$message", $this->context); + } + array_pop($this->context); + return $success; + } + + /** + * retuns success status of last webquery action + * @return boolean true on success + */ + public function is_success() { + + // try to parse response xml + $this->resp_xml = $this->parse_xml($this->response->getBody()); + if (empty($this->resp_xml)) { + $fake_resp = '<root><error status="500"><code>NO_XML_RESP</code><message>failed to parse xml response</message></error></root>'; + $this->resp_xml = $this->parse_xml($fake_resp); + return false; + } + + // get content of first error element + $error = $this->resp_xml->error[0]; + if (isset($error)) { + foreach ($error->attributes() as $attr => $value) { + $this->resp_error[$attr] = (string) $value; + } + foreach ($error->children() as $key => $value) { + $this->resp_error[$key] = (string) $value; + } + } + + if ($this->response->getStatusCode() != 200) { + return false; + } + if (!isset($this->resp_error['status']) || $this->resp_error['status'] != 200) { + return false; + } + + return true; + } + + /** + * return response object of last webquery action + * @return psr-7 response object + */ + public function get_response() { + + return $this->response; + } + + /** + * retuns content of first webquery error element of last webquery action + * @return array with all webquery error attributes and elements + */ + public function get_error() { + + return $this->resp_error; + } + + /** + * return value of given error property of last webquery action + * @param string $name webquery error property name (attribute or ellement + * @return string value or empty string if not set + */ + public function get_error_value($name) { + + return isset($this->resp_error[$name]) ? $this->resp_error[$name] : ''; + } + + /** + * return value of webquery error code of last webquery action + * @return string value of code error property or empty string + */ + public function get_error_code() { + + return $this->get_error_value('code'); + } + + /** + * return value of webquery error status of last webquery action + * @return string value of status error property or empty string + */ + public function get_error_status() { + + return $this->get_error_value('status'); + } + + /** + * return value of webquery error message of last webquery action + * the return message is a composite of the message, field and xmlerror properties + * @return string value of message error property or empty string + */ + public function get_error_message() { + + $message = $this->get_error_value('message'); + $field = $this->get_error_value('field'); + if (!empty($field)) { + $message = "$message: $field"; + } + $xmlerror = $this->get_error_value('xmlerror'); + return empty($xmlerror) ? $message : "$message ($xmlerror)"; + } + + /** + * return value of webquery error isn of last webquery action + * isn is set even if webquery failed to create a record + * @return string value of isn error property or empty string + */ + public function get_isn() { + + return $this->get_error_value('isn'); + } + + + protected function reset_response() { + + $this->response = null; + $this->resp_xml = null; + $this->resp_error = []; + } + + /** + * parse xml content from message + * + * @param $text + * @return SimpleXMLElement parsed xml or false on errors + */ + public function parse_xml($text) { + + if (trim($text) == '') { + $this->error("xml string is empty", $this->context); + return false; + } + + libxml_use_internal_errors(true); + $xml = simplexml_load_string($text); + $errors = libxml_get_errors(); + libxml_clear_errors(); + $error_count = count($errors); + if ($error_count > 0) { + $this->error("parsing xml string failed with $error_count errors (max. 10 listed", $this->context); + // only log first 10 error messages + foreach (array_slice($errors, 0, 9) as $error) { + $this->error('xml parse error: ' . print_r($error, true), $this->context); + } + return false; + } + return $xml; + } + +} diff --git a/tests/library/WebqueryTest.php b/tests/library/WebqueryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b46759d381626b4eb742c3220eefbbefd855bd3b --- /dev/null +++ b/tests/library/WebqueryTest.php @@ -0,0 +1,126 @@ +<?php + +namespace Tests\Functional\Library; + +use GuzzleHttp\Psr7\Response; + +class WebqueryTest extends \PHPUnit_Framework_TestCase { + + + public function test_get_url() { + + // with default settings + $wq = new \Library\Webquery(); + $expect = 'http://library.wur.nl/WebQuery//new_xml'; + $this->assertEquals($expect, $wq->get_url(), 'no properties set, default url'); + + // with url properties set + $wq = new \Library\Webquery(); + $wq->set_host('devel.library.wur.nl') + ->set_service('testservice') + ->set_suffix('record'); + $expect = 'http://devel.library.wur.nl/WebQuery/testservice/new_record'; + $this->assertEquals($expect, $wq->get_url(), 'no properties set, default url'); + } + + + public function test_xml_to_wwwform() { + + $xml = get_test_xml(); + $wq = new \Library\Webquery(); + $wwwform = $wq->xml_to_wwwform($xml); + $expect = 'sub1=subval1&sub2=&child1=childval1&child2=childval2&sub3=subval+3'; + $this->assertEquals($expect, $wwwform, 'record in x-www-form-urlencoded string'); + } + + public function test_create_record() { + + // create mock http client that stores requests and responses + $request_history = []; + $responses = [ + new Response(200, [], '<recordset><error status="200" isn="13"><code>WQW_OK</code><message>record created</message></error></recordset>'), + new Response(500, [], '<recordset><error status="501"><code>WQE_FAILED</code><message>record not created</message></error></recordset>'), + ]; + $mock_httpclient = get_mock_httpclient($responses, $request_history); + + $wq = new \Library\Webquery(); + $wq->set_http_client($mock_httpclient); + //$wq->set_logger(new \Monolog\Logger('create_record')); + + $wq->set_host('devel.library.wur.nl') + ->set_service('testservice'); + + $xml = get_test_xml(); + + // first request succeeds + $result = $wq->create_record($xml); + $this->assertTrue($result, 'first request return value'); + $this->assertEquals('200', $wq->get_error_status(), 'first response error status'); + $this->assertEquals('WQW_OK', $wq->get_error_code(), 'first response error code'); + $this->assertEquals('record created', $wq->get_error_message(), 'first response error message'); + $this->assertEquals('13', $wq->get_isn(), 'first response isn'); + + // second request fails + $wq->set_suffix('record'); + $result = $wq->create_record($xml); + $this->assertFalse($result, 'second request return value'); + $this->assertEquals('501', $wq->get_error_status(), 'second response error status'); + $this->assertEquals('WQE_FAILED', $wq->get_error_code(), 'second response error code'); + $this->assertEquals('record not created', $wq->get_error_message(), 'second response error message'); + $this->assertEquals('', $wq->get_isn(), 'second response isn'); + + $this->assertEquals(2, count($request_history), 'request history count'); + // check first request + $request = $request_history[0]['request']; + $this->assertEquals('POST', $request->getMethod(), 'request 1 method'); + $expect = 'http://devel.library.wur.nl/WebQuery/testservice/new_xml'; + $this->assertEquals($expect, $request->getUri(), 'request 1 url'); + $expect = 'sub1=subval1&sub2=&child1=childval1&child2=childval2&sub3=subval+3'; + $this->assertEquals($expect, $request->getBody(), 'request 1 body'); + // check second request + $request = $request_history[1]['request']; + $this->assertEquals('POST', $request->getMethod(), 'request 2 method'); + $expect = 'http://devel.library.wur.nl/WebQuery/testservice/new_record'; + $this->assertEquals($expect, $request->getUri(), 'request 2 url'); + $expect = 'sub1=subval1&sub2=&child1=childval1&child2=childval2&sub3=subval+3'; + $this->assertEquals($expect, $request->getBody(), 'request 2 body'); + + } + + +} + +/*********** Helper Functions **************/ + +function get_test_xml() { + + $xml_txt = <<<EOT +<record> + <sub1>subval1</sub1> + <sub2> + <child1>childval1</child1> + <child2>childval2</child2> + </sub2> + <sub3>subval 3</sub3> +</record> +EOT; + $wq = new \Library\Webquery(); + return $wq->parse_xml($xml_txt); +} + + +function get_mock_httpclient(array $mock_responses, array &$history = null) { + + foreach ($mock_responses as $key => $value) { + if (is_int($value)) { + $mock_responses[$key] = new \GuzzleHttp\Psr7\Response($value); + } + } + $mock_handler = new \GuzzleHttp\Handler\MockHandler($mock_responses); + $mock_stack = \GuzzleHttp\HandlerStack::create($mock_handler); + if (isset($history)) { + $mock_history = \GuzzleHttp\Middleware::history($history); + $mock_stack->push($mock_history); + } + return new \GuzzleHttp\Client(['handler' => $mock_stack]); +}