From 8db9b374e485868992260e3f88500234fcc237c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Bechyn=C4=9B?= Date: Fri, 26 May 2017 10:53:41 +0200 Subject: [PATCH] SoapFault handling refactored: client now returns server fault codes + more details in message --- src/BeSimple/SoapClient/Curl/Curl.php | 5 +- src/BeSimple/SoapClient/SoapClient.php | 274 ++++++------------ .../SoapClientNativeMethodsTrait.php | 166 +++++++++++ .../SoapCommon/Fault/SoapFaultParser.php | 31 ++ .../SoapCommon/Fault/SoapFaultParserTest.php | 25 ++ ...apServerAndSoapClientCommunicationTest.php | 39 +++ tests/SwaSenderSoapFaultEndpoint.php | 9 + 7 files changed, 361 insertions(+), 188 deletions(-) create mode 100644 src/BeSimple/SoapClient/SoapClientNativeMethodsTrait.php create mode 100644 src/BeSimple/SoapCommon/Fault/SoapFaultParser.php create mode 100644 tests/BeSimple/SoapCommon/Fault/SoapFaultParserTest.php create mode 100644 tests/SwaSenderSoapFaultEndpoint.php diff --git a/src/BeSimple/SoapClient/Curl/Curl.php b/src/BeSimple/SoapClient/Curl/Curl.php index 65ee796..8048832 100644 --- a/src/BeSimple/SoapClient/Curl/Curl.php +++ b/src/BeSimple/SoapClient/Curl/Curl.php @@ -152,10 +152,11 @@ class Curl preg_match('/HTTP\/(1\.[0-1]+) ([0-9]{3}) (.*)/', $executeSoapCallResponse, $httpResponseMessages); $httpResponseMessage = trim(array_pop($httpResponseMessages)); $curlErrorMessage = sprintf( - 'Curl error "%s" with message: %s occurred while connecting to %s', + 'Curl error "%s" with message: %s occurred while connecting to %s with HTTP response code %s', curl_errno($curlSession), curl_error($curlSession), - $location + $location, + $httpResponseCode ); if (!is_integer($httpResponseCode) || $httpResponseCode >= 400 || $httpResponseCode === 0) { diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index e80bf7a..2ecd385 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -19,6 +19,7 @@ use BeSimple\SoapClient\Curl\CurlOptionsBuilder; use BeSimple\SoapClient\Curl\CurlResponse; use BeSimple\SoapClient\SoapOptions\SoapClientOptions; use BeSimple\SoapCommon\Fault\SoapFaultEnum; +use BeSimple\SoapCommon\Fault\SoapFaultParser; use BeSimple\SoapCommon\Fault\SoapFaultPrefixEnum; use BeSimple\SoapCommon\Fault\SoapFaultSourceGetter; use BeSimple\SoapCommon\Mime\PartFactory; @@ -40,16 +41,14 @@ use SoapFault; */ class SoapClient extends \SoapClient { + use SoapClientNativeMethodsTrait; + /** @var SoapClientOptions */ protected $soapClientOptions; /** @var SoapOptions */ protected $soapOptions; /** @var Curl */ private $curl; - /** @var SoapAttachment[] */ - private $soapAttachmentsOnRequestStorage; - /** @var SoapResponse */ - private $soapResponseStorage; public function __construct(SoapClientOptions $soapClientOptions, SoapOptions $soapOptions) { @@ -76,33 +75,6 @@ class SoapClient extends \SoapClient @parent::__construct($wsdlPath, $soapClientOptions->toArray() + $soapOptions->toArray()); } - /** - * Avoid using __call directly, it's deprecated even in \SoapClient. - * - * @deprecated - */ - public function __call($function_name, $arguments) - { - throw new Exception( - 'The __call method is deprecated. Use __soapCall/soapCall instead.' - ); - } - - /** - * Using __soapCall returns only response string, use soapCall instead. - * - * @param string $function_name - * @param array $arguments - * @param array|null $options - * @param null $input_headers - * @param array|null $output_headers - * @return string - */ - public function __soapCall($function_name, $arguments, $options = null, $input_headers = null, &$output_headers = null) - { - return $this->soapCall($function_name, $arguments, $options, $input_headers, $output_headers)->getResponseContent(); - } - /** * @param string $functionName * @param array $arguments @@ -125,95 +97,13 @@ class SoapClient extends \SoapClient } catch (SoapFault $soapFault) { if (SoapFaultSourceGetter::isNativeSoapFault($soapFault)) { - $soapResponse = $this->getSoapResponseFromStorage(); - if ($soapResponse instanceof SoapResponse) { - $soapFault = $this->throwSoapFaultByTracing( - SoapFaultPrefixEnum::PREFIX_PHP . '-' . $soapFault->getCode(), - $soapFault->getMessage(), - new SoapResponseTracingData( - 'Content-Type: ' . $soapResponse->getRequest()->getContentType(), - $soapResponse->getRequest()->getContent(), - 'Content-Type: ' . $soapResponse->getContentType(), - $soapResponse->getResponseContent() - ) - ); - } else { - $soapFault = new SoapFault( - SoapFaultPrefixEnum::PREFIX_PHP . '-unresolved', - 'Got SoapFault message with no response: '.$soapFault->getMessage() - ); - } + $soapFault = $this->decorateNativeSoapFault($soapFault); } throw $soapFault; } } - /** - * This is not performing any HTTP requests, but it is getting data from SoapClient that are needed for this Client - * - * @param string $request Request string - * @param string $location Location - * @param string $action SOAP action - * @param int $version SOAP version - * @param int $oneWay 0|1 - * - * @return string - */ - public function __doRequest($request, $location, $action, $version, $oneWay = 0) - { - $soapResponse = $this->performSoapRequest( - $request, - $location, - $action, - $version, - $this->getSoapAttachmentsOnRequestFromStorage() - ); - $this->setSoapResponseToStorage($soapResponse); - - return $soapResponse->getResponseContent(); - } - - /** @deprecated */ - public function __getLastRequestHeaders() - { - $this->checkTracing(); - - throw new Exception( - 'The __getLastRequestHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' - ); - } - - /** @deprecated */ - public function __getLastRequest() - { - $this->checkTracing(); - - throw new Exception( - 'The __getLastRequest method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' - ); - } - - /** @deprecated */ - public function __getLastResponseHeaders() - { - $this->checkTracing(); - - throw new Exception( - 'The __getLastResponseHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' - ); - } - - /** @deprecated */ - public function __getLastResponse() - { - $this->checkTracing(); - - throw new Exception( - 'The __getLastResponse method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' - ); - } - /** * Custom request method to be able to modify the SOAP messages. * $oneWay parameter is not used at the moment. @@ -226,7 +116,7 @@ class SoapClient extends \SoapClient * * @return SoapResponse */ - private function performSoapRequest($request, $location, $action, $version, array $soapAttachments = []) + protected function performSoapRequest($request, $location, $action, $version, array $soapAttachments = []) { $soapRequest = $this->createSoapRequest($location, $action, $version, $request, $soapAttachments); @@ -273,22 +163,10 @@ class SoapClient extends \SoapClient */ private function performHttpSoapRequest(SoapRequest $soapRequest) { - if ($soapRequest->getVersion() === SOAP_1_1) { - $headers = [ - 'Content-Type: ' . $soapRequest->getContentType(), - 'SOAPAction: "' . $soapRequest->getAction() . '"', - 'Connection: ' . ($this->soapOptions->isConnectionKeepAlive() ? 'Keep-Alive' : 'close'), - ]; - } else { - $headers = [ - 'Content-Type: ' . $soapRequest->getContentType() . '; action="' . $soapRequest->getAction() . '"', - 'Connection: ' . ($this->soapOptions->isConnectionKeepAlive() ? 'Keep-Alive' : 'close'), - ]; - } $curlResponse = $this->curl->executeCurlWithCachedSession( $soapRequest->getLocation(), $soapRequest->getContent(), - $headers + $this->getHttpHeadersBySoapVersion($soapRequest) ); $soapResponseTracingData = new SoapResponseTracingData( $curlResponse->getHttpRequestHeaders(), @@ -310,26 +188,42 @@ class SoapClient extends \SoapClient $this->getAttachmentFilters(), $this->soapOptions->getAttachmentType() ); - - } else { - - return $soapResponse; } - } else if ($curlResponse->curlStatusFailed()) { + + return $soapResponse; + + } + if ($curlResponse->curlStatusFailed()) { + + if ($curlResponse->getHttpResponseStatusCode() >= 500) { + $soapFault = SoapFaultParser::parseSoapFault( + $curlResponse->getResponseBody() + ); + + return $this->throwSoapFaultByTracing( + $soapFault->faultcode, + sprintf( + 'SOAP HTTP call failed: %s with Message: %s and Code: %s', + $curlResponse->getCurlErrorMessage(), + $soapFault->getMessage(), + $soapFault->faultcode + ), + $soapResponseTracingData + ); + } return $this->throwSoapFaultByTracing( SoapFaultEnum::SOAP_FAULT_HTTP.'-'.$curlResponse->getHttpResponseStatusCode(), $curlResponse->getCurlErrorMessage(), $soapResponseTracingData ); - } else { - - return $this->throwSoapFaultByTracing( - SoapFaultEnum::SOAP_FAULT_SOAP_CLIENT_ERROR, - 'Cannot process curl response with unresolved status: ' . $curlResponse->getCurlStatus(), - $soapResponseTracingData - ); } + + return $this->throwSoapFaultByTracing( + SoapFaultEnum::SOAP_FAULT_SOAP_CLIENT_ERROR, + 'Cannot process curl response with unresolved status: ' . $curlResponse->getCurlStatus(), + $soapResponseTracingData + ); } /** @@ -383,18 +277,16 @@ class SoapClient extends \SoapClient $soapResponseTracingData, $soapAttachments ); - - } else { - - return SoapResponseFactory::create( - $curlResponse->getResponseBody(), - $soapRequest->getLocation(), - $soapRequest->getAction(), - $soapRequest->getVersion(), - $curlResponse->getHttpResponseContentType(), - $soapAttachments - ); } + + return SoapResponseFactory::create( + $curlResponse->getResponseBody(), + $soapRequest->getLocation(), + $soapRequest->getAction(), + $soapRequest->getVersion(), + $curlResponse->getHttpResponseContentType(), + $soapAttachments + ); } /** @@ -412,49 +304,59 @@ class SoapClient extends \SoapClient $soapFaultMessage, $soapResponseTracingData ); + } + throw new SoapFault( + $soapFaultCode, + $soapFaultMessage + ); + } + + private function decorateNativeSoapFault(SoapFault $nativePhpSoapFault) + { + $soapResponse = $this->getSoapResponseFromStorage(); + if ($soapResponse instanceof SoapResponse) { + $tracingData = new SoapResponseTracingData( + 'Content-Type: ' . $soapResponse->getRequest()->getContentType(), + $soapResponse->getRequest()->getContent(), + 'Content-Type: ' . $soapResponse->getContentType(), + $soapResponse->getResponseContent() + ); + $soapFault = $this->throwSoapFaultByTracing( + SoapFaultPrefixEnum::PREFIX_PHP . '-' . $nativePhpSoapFault->getCode(), + $nativePhpSoapFault->getMessage(), + $tracingData + ); } else { - - throw new SoapFault( - $soapFaultCode, - $soapFaultMessage + $soapFault = $this->throwSoapFaultByTracing( + $nativePhpSoapFault->faultcode, + $nativePhpSoapFault->getMessage(), + new SoapResponseTracingData( + null, + null, + null, + null + ) ); } + + return $soapFault; } - private function checkTracing() + private function getHttpHeadersBySoapVersion(SoapRequest $soapRequest) { - if ($this->soapClientOptions->getTrace() === false) { - throw new Exception('SoapClientOptions tracing disabled, turn on trace attribute'); + if ($soapRequest->getVersion() === SOAP_1_1) { + + return [ + 'Content-Type: ' . $soapRequest->getContentType(), + 'SOAPAction: "' . $soapRequest->getAction() . '"', + 'Connection: ' . ($this->soapOptions->isConnectionKeepAlive() ? 'Keep-Alive' : 'close'), + ]; } - } - private function setSoapResponseToStorage(SoapResponse $soapResponseStorage) - { - $this->soapResponseStorage = $soapResponseStorage; - } - - /** - * @param SoapAttachment[] $soapAttachments - */ - private function setSoapAttachmentsOnRequestToStorage(array $soapAttachments) - { - $this->soapAttachmentsOnRequestStorage = $soapAttachments; - } - - private function getSoapAttachmentsOnRequestFromStorage() - { - $soapAttachmentsOnRequest = $this->soapAttachmentsOnRequestStorage; - $this->soapAttachmentsOnRequestStorage = null; - - return $soapAttachmentsOnRequest; - } - - private function getSoapResponseFromStorage() - { - $soapResponse = $this->soapResponseStorage; - $this->soapResponseStorage = null; - - return $soapResponse; + return [ + 'Content-Type: ' . $soapRequest->getContentType() . '; action="' . $soapRequest->getAction() . '"', + 'Connection: ' . ($this->soapOptions->isConnectionKeepAlive() ? 'Keep-Alive' : 'close'), + ]; } } diff --git a/src/BeSimple/SoapClient/SoapClientNativeMethodsTrait.php b/src/BeSimple/SoapClient/SoapClientNativeMethodsTrait.php new file mode 100644 index 0000000..8fb534d --- /dev/null +++ b/src/BeSimple/SoapClient/SoapClientNativeMethodsTrait.php @@ -0,0 +1,166 @@ +soapCall($function_name, $arguments, $options, $input_headers, $output_headers)->getResponseContent(); + } + + /** + * This is not performing any HTTP requests, but it is getting data from SoapClient that are needed for this Client + * + * @param string $request Request string + * @param string $location Location + * @param string $action SOAP action + * @param int $version SOAP version + * @param int $oneWay 0|1 + * + * @return string + */ + public function __doRequest($request, $location, $action, $version, $oneWay = 0) + { + $soapResponse = $this->performSoapRequest( + $request, + $location, + $action, + $version, + $this->getSoapAttachmentsOnRequestFromStorage() + ); + $this->setSoapResponseToStorage($soapResponse); + + return $soapResponse->getResponseContent(); + } + + /** @deprecated */ + public function __getLastRequestHeaders() + { + $this->checkTracing(); + + throw new Exception( + 'The __getLastRequestHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' + ); + } + + /** @deprecated */ + public function __getLastRequest() + { + $this->checkTracing(); + + throw new Exception( + 'The __getLastRequest method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' + ); + } + + /** @deprecated */ + public function __getLastResponseHeaders() + { + $this->checkTracing(); + + throw new Exception( + 'The __getLastResponseHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' + ); + } + + /** @deprecated */ + public function __getLastResponse() + { + $this->checkTracing(); + + throw new Exception( + 'The __getLastResponse method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' + ); + } + + private function checkTracing() + { + if ($this->soapClientOptions->getTrace() === false) { + throw new Exception('SoapClientOptions tracing disabled, turn on trace attribute'); + } + } + + private function setSoapResponseToStorage(SoapResponse $soapResponseStorage) + { + $this->soapResponseStorage = $soapResponseStorage; + } + + /** + * @param SoapAttachment[] $soapAttachments + */ + private function setSoapAttachmentsOnRequestToStorage(array $soapAttachments) + { + $this->soapAttachmentsOnRequestStorage = $soapAttachments; + } + + private function getSoapAttachmentsOnRequestFromStorage() + { + $soapAttachmentsOnRequest = $this->soapAttachmentsOnRequestStorage; + $this->soapAttachmentsOnRequestStorage = null; + + return $soapAttachmentsOnRequest; + } + + private function getSoapResponseFromStorage() + { + $soapResponse = $this->soapResponseStorage; + $this->soapResponseStorage = null; + + return $soapResponse; + } +} diff --git a/src/BeSimple/SoapCommon/Fault/SoapFaultParser.php b/src/BeSimple/SoapCommon/Fault/SoapFaultParser.php new file mode 100644 index 0000000..3004b0c --- /dev/null +++ b/src/BeSimple/SoapCommon/Fault/SoapFaultParser.php @@ -0,0 +1,31 @@ +xpath('//faultcode'); + if ($faultCode === false || count($faultCode) === 0) { + $faultCode = 'Unable to parse faultCode'; + } + $faultString = $simpleXMLElement->xpath('//faultstring'); + if ($faultString === false || count($faultString) === 0) { + $faultString = 'Unable to parse faultString'; + } + + return new SoapFault( + (string)$faultCode[0], + (string)$faultString[0] + ); + } +} diff --git a/tests/BeSimple/SoapCommon/Fault/SoapFaultParserTest.php b/tests/BeSimple/SoapCommon/Fault/SoapFaultParserTest.php new file mode 100644 index 0000000..b1fb2a8 --- /dev/null +++ b/tests/BeSimple/SoapCommon/Fault/SoapFaultParserTest.php @@ -0,0 +1,25 @@ +911This is a dummy SoapFault.'; + $soapFault = SoapFaultParser::parseSoapFault($soapFaultXml); + + self::assertInstanceOf(SoapFault::class, $soapFault); + self::assertEquals( + '911', + $soapFault->faultcode + ); + self::assertEquals( + 'This is a dummy SoapFault.', + $soapFault->getMessage() + ); + } +} diff --git a/tests/BeSimple/SoapServerAndSoapClientCommunicationTest.php b/tests/BeSimple/SoapServerAndSoapClientCommunicationTest.php index d4404b9..7dbff0b 100644 --- a/tests/BeSimple/SoapServerAndSoapClientCommunicationTest.php +++ b/tests/BeSimple/SoapServerAndSoapClientCommunicationTest.php @@ -17,6 +17,7 @@ use Fixtures\DummyServiceMethodWithIncomingLargeSwaRequest; use Fixtures\DummyServiceMethodWithOutgoingLargeSwaRequest; use Fixtures\GenerateTestRequest; use PHPUnit_Framework_TestCase; +use SoapFault; use SoapHeader; class SoapServerAndSoapClientCommunicationTest extends PHPUnit_Framework_TestCase @@ -124,6 +125,44 @@ class SoapServerAndSoapClientCommunicationTest extends PHPUnit_Framework_TestCas ); } + public function testSoapCallSwaWithLargeSwaResponseWithSoapFault() + { + $soapClient = $this->getSoapBuilder()->buildWithSoapHeader( + SoapClientOptionsBuilder::createWithEndpointLocation( + self::TEST_HTTP_URL.'/SwaSenderSoapFaultEndpoint.php' + ), + SoapOptionsBuilder::createSwaWithClassMap( + self::TEST_HTTP_URL.'/SwaSenderEndpoint.php?wsdl', + new ClassMap([ + 'GenerateTestRequest' => GenerateTestRequest::class, + ]), + SoapOptions::SOAP_CACHE_TYPE_NONE + ), + new SoapHeader('http://schema.testcase', 'SoapHeader', [ + 'user' => 'admin', + ]) + ); + + $this->setExpectedException(SoapFault::class); + + try { + $soapClient->soapCall('dummyServiceMethodWithOutgoingLargeSwa', []); + } catch (SoapFault $e) { + self::assertEquals( + '911', + $e->faultcode + ); + self::assertEquals( + 'SOAP HTTP call failed: Curl error "0" with message: occurred while connecting to http://localhost:8000/tests/SwaSenderSoapFaultEndpoint.php with HTTP response code 500 with Message: This is a dummy SoapFault. and Code: 911', + $e->getMessage() + ); + + throw $e; + } + + self::fail('Expected SoapFault was not thrown'); + } + public function testSoapCallWithLargeSwaRequest() { $soapClient = $this->getSoapBuilder()->buildWithSoapHeader( diff --git a/tests/SwaSenderSoapFaultEndpoint.php b/tests/SwaSenderSoapFaultEndpoint.php new file mode 100644 index 0000000..ad92e3e --- /dev/null +++ b/tests/SwaSenderSoapFaultEndpoint.php @@ -0,0 +1,9 @@ +fault( + 911, + 'This is a dummy SoapFault.' +);