├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── composer.json ├── examples └── psr18-transport.php ├── psalm.xml └── src ├── Exception └── RequestException.php ├── HttpBinding ├── Psr7Converter.php ├── Psr7RequestBuilder.php └── SoapActionDetector.php ├── Middleware ├── RemoveEmptyNodesMiddleware.php ├── SoapHeaderMiddleware.php ├── WSICompliance │ └── QuotedSoapActionMiddleware.php └── Wsdl │ ├── DisableExtensionsMiddleware.php │ └── DisablePoliciesMiddleware.php ├── Psr18Transport.php ├── Wsdl └── Psr18Loader.php └── Xml ├── Loader └── Psr7StreamLoader.php ├── Mapper └── Psr7StreamMapper.php └── XmlMessageManipulator.php /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setFinder( 5 | \Symfony\Component\Finder\Finder::create() 6 | ->in([ 7 | __DIR__ . '/src', 8 | __DIR__ . '/tests', 9 | ]) 10 | ->name('*.php') 11 | ) 12 | ->setRiskyAllowed(true) 13 | ->setRules([ 14 | '@PSR2' => true, 15 | 'align_multiline_comment' => true, 16 | 'array_indentation' => true, 17 | 'declare_strict_types' => true, 18 | 'final_class' => true, 19 | 'global_namespace_import' => [ 20 | 'import_classes' => true, 21 | 'import_constants' => true, 22 | 'import_functions' => true, 23 | ], 24 | 'list_syntax' => [ 25 | 'syntax' => 'short', 26 | ], 27 | 'constant_case' => [ 28 | 'case' => 'lower', 29 | ], 30 | 'multiline_comment_opening_closing' => true, 31 | 'native_function_casing' => true, 32 | 'no_empty_phpdoc' => true, 33 | 'no_leading_import_slash' => true, 34 | 'no_superfluous_phpdoc_tags' => [ 35 | 'allow_mixed' => true, 36 | ], 37 | 'no_unused_imports' => true, 38 | 'no_useless_else' => true, 39 | 'no_useless_return' => true, 40 | 'ordered_imports' => [ 41 | 'imports_order' => ['class', 'function', 'const'], 42 | ], 43 | 'ordered_interfaces' => true, 44 | 'php_unit_test_annotation' => true, 45 | 'php_unit_test_case_static_method_calls' => [ 46 | 'call_type' => 'static', 47 | ], 48 | 'php_unit_method_casing' => [ 49 | 'case' => 'snake_case', 50 | ], 51 | 'single_import_per_statement' => true, 52 | 'single_trait_insert_per_statement' => true, 53 | 'static_lambda' => true, 54 | 'strict_comparison' => true, 55 | 'strict_param' => true, 56 | 'nullable_type_declaration_for_default_null_value' => true, 57 | ]) 58 | ; 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-MAX_INT php-soap 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSR-18 SOAP Transport 2 | 3 | This transport allows you to send SOAP requests over a PSR-18 HTTP client implementation. 4 | You can use any client you want, going from curl, guzzle, httplug, symfony/http-client, ... 5 | It allows you to get full control over the HTTP layer, making it possible to e.g. overcome some well-known issues in `ext-soap`. 6 | This package can best be used together with a [SOAP driver](https://github.com/php-soap/engine) that handles data encoding and decoding. 7 | 8 | # Want to help out? 💚 9 | 10 | - [Become a Sponsor](https://github.com/php-soap/.github/blob/main/HELPING_OUT.md#sponsor) 11 | - [Let us do your implementation](https://github.com/php-soap/.github/blob/main/HELPING_OUT.md#let-us-do-your-implementation) 12 | - [Contribute](https://github.com/php-soap/.github/blob/main/HELPING_OUT.md#contribute) 13 | - [Help maintain these packages](https://github.com/php-soap/.github/blob/main/HELPING_OUT.md#maintain) 14 | 15 | Want more information about the future of this project? Check out this list of the [next big projects](https://github.com/php-soap/.github/blob/main/PROJECTS.md) we'll be working on. 16 | 17 | # Prerequisites 18 | 19 | Choosing what HTTP client you want to use. 20 | This package expects some PSR implementations to be present in order to be installed: 21 | 22 | * PSR-7: `psr/http-message-implementation` like `nyholm/psr7` or `guzzlehttp/psr7` 23 | * PSR-17: `psr/http-factory-implementation` like `nyholm/psr7` or `guzzlehttp/psr7` 24 | * PSR-18: `psr/http-client-implementation` like `symfony/http-client` or `guzzlehttp/guzzle` 25 | 26 | # Installation 27 | 28 | ```bash 29 | composer require php-soap/psr18-transport 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```php 35 | 36 | use Http\Client\Common\PluginClient; 37 | use Soap\Engine\SimpleEngine; 38 | use Soap\Psr18Transport\Psr18Transport; 39 | 40 | $engine = new SimpleEngine( 41 | $driver, 42 | $transport = Psr18Transport::createForClient( 43 | new PluginClient( 44 | $psr18Client, 45 | [...$middleware] 46 | ) 47 | ) 48 | ); 49 | ``` 50 | 51 | ## Middleware 52 | 53 | This package provides some middleware implementations for dealing with some common SOAP issues. 54 | 55 | ### Wsdl\DisableExtensionsMiddleware 56 | 57 | PHP's ext-soap implementation do not support `wsdl:required` attributes since there is no SOAP extension mechanism in PHP. 58 | You will retrieve this exception: "[SoapFault] SOAP-ERROR: Parsing WSDL: Unknown required WSDL extension" 59 | when the WSDL does contain required SOAP extensions. 60 | 61 | This middleware can be used to set the "wsdl:required" 62 | property to false when loading the WSDL so that you don't have to change the WSDL on the server. 63 | 64 | **Usage** 65 | 66 | ```php 67 | use Http\Client\Common\PluginClient; 68 | use Soap\Psr18Transport\Middleware\Wsdl\DisableExtensionsMiddleware; 69 | 70 | $wsdlClient = new PluginClient( 71 | $psr18Client, 72 | [ 73 | new DisableExtensionsMiddleware(), 74 | ] 75 | ); 76 | ``` 77 | 78 | ### Wsdl\DisablePoliciesMiddleware 79 | 80 | PHP's ext-soap client does not support the [Web Services Policy Framework](http://schemas.xmlsoap.org/ws/2004/09/policy/) attributes since there is no such support in PHP. 81 | You will retrieve this exception: "[SoapFault] SOAP-ERROR: Parsing WSDL: Unknown required WSDL extension 'http://schemas.xmlsoap.org/ws/2004/09/policy'" 82 | when the WSDL does contains WS policies. 83 | 84 | This middleware can be used to remove all UsingPolicy and Policy tags on the fly so that you don't have to change the WSDL on the server. 85 | 86 | **Usage** 87 | 88 | ```php 89 | use Http\Client\Common\PluginClient; 90 | use Soap\Psr18Transport\Middleware\Wsdl\DisablePoliciesMiddleware; 91 | 92 | $wsdlClient = new PluginClient( 93 | $psr18Client, 94 | [ 95 | new DisablePoliciesMiddleware(), 96 | ] 97 | ); 98 | ``` 99 | 100 | ### RemoveEmptyNodesMiddleware 101 | 102 | Empty properties are converted into empty nodes in the request XML. 103 | If you need to avoid empty nodes in the request xml, you can add this middleware. 104 | 105 | **Usage** 106 | 107 | ```php 108 | use Http\Client\Common\PluginClient; 109 | use Soap\Psr18Transport\Middleware\RemoveEmptyNodesMiddleware; 110 | 111 | 112 | $httpClient = new PluginClient( 113 | $psr18Client, 114 | [ 115 | new RemoveEmptyNodesMiddleware() 116 | ] 117 | ); 118 | ``` 119 | 120 | ### SoapHeaderMiddleware 121 | 122 | Attaches multiple SOAP headers to the request before sending the SOAP envelope. 123 | 124 | **Usage** 125 | 126 | ```php 127 | use Http\Client\Common\PluginClient; 128 | use Soap\Psr18Transport\Middleware\RemoveEmptyNodesMiddleware; 129 | use Soap\Xml\Builder\Header\Actor; 130 | use Soap\Xml\Builder\Header\MustUnderstand; 131 | use Soap\Xml\Builder\SoapHeader; 132 | 133 | 134 | $httpClient = new PluginClient( 135 | $psr18Client, 136 | [ 137 | new SoapHeaderMiddleware( 138 | new SoapHeader( 139 | $tns, 140 | 'x:Auth', 141 | children( 142 | namespaced_element($tns, 'x:user', value('josbos')), 143 | namespaced_element($tns, 'x:password', value('topsecret')) 144 | ) 145 | ), 146 | new SoapHeader($tns, 'Acting', Actor::next()), 147 | new SoapHeader($tns, 'Understanding', new MustUnderstand()) 148 | ) 149 | ] 150 | ); 151 | ``` 152 | 153 | More information on the SoapHeader configurator can be found in [php-soap/xml](https://github.com/php-soap/xml#soapheaders). 154 | 155 | ### HTTPlug middleware 156 | 157 | This package includes [all basic plugins from httplug](https://docs.php-http.org/en/latest/plugins/). 158 | You can load any additional plugins you want, like e.g. [the logger plugin](https://github.com/php-http/logger-plugin). 159 | 160 | **Examples** 161 | 162 | ```php 163 | use Http\Client\Common\Plugin\AuthenticationPlugin; 164 | use Http\Client\Common\Plugin\BaseUriPlugin; 165 | use Http\Client\Common\Plugin\LoggerPlugin; 166 | use Http\Message\Authentication\BasicAuth; 167 | 168 | 169 | $httpClient = new PluginClient( 170 | $psr18Client, 171 | [ 172 | new BaseUriPlugin($baseLocation), 173 | new AuthenticationPlugin(new BasicAuth($_ENV['user'], $_ENV['pass'])), 174 | new LoggerPlugin($psrLogger), 175 | ] 176 | ); 177 | ``` 178 | 179 | ### Writing your own middleware 180 | 181 | We use httplug for its plugin system. 182 | You can create your own middleware by [following their documentation](https://docs.php-http.org/en/latest/plugins/build-your-own.html). 183 | 184 | ## Authentication 185 | 186 | You can add authentication to both the WSDL fetching and SOAP handling part. 187 | For this, we suggest you to use the default [httplug authentication providers](https://docs.php-http.org/en/latest/message/authentication.html). 188 | 189 | ### NTLM 190 | 191 | Adding NTLM authentication requires you to use a `curl` based PSR-18 HTTP Client. 192 | On those clients, you can set following options: `[CURLOPT_HTTPAUTH => CURLAUTH_NTLM, CURLOPT_USERPWD => 'user:pass']`. 193 | Clients like guzzle and symfony/http-client also support NTLM by setting options during client configuration. 194 | 195 | ## Dealing with XML 196 | 197 | When writing custom SOAP middleware, a frequent task is to transform the request or response XML into a slight variation. 198 | This package provides some shortcut tools around [php-soap/xml](https://github.com/php-soap/xml) to make it easier for you to deal with XML. 199 | 200 | 201 | **Example** 202 | 203 | ```php 204 | use Http\Client\Common\Plugin; 205 | use Http\Promise\Promise; 206 | use Psr\Http\Message\RequestInterface; 207 | use Psr\Http\Message\ResponseInterface; 208 | use Soap\Psr18Transport\Xml\XmlMessageManipulator; 209 | use VeeWee\Xml\Dom\Document; 210 | 211 | class SomeMiddleware implements Plugin 212 | { 213 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise 214 | { 215 | $request = (new XmlMessageManipulator)( 216 | $request, 217 | fn (Document $document) => $document->manipulate( 218 | doSomethingWithRequestXml() 219 | ) 220 | ); 221 | 222 | return $next($request) 223 | ->then(function (ResponseInterface $response): ResponseInterface { 224 | return (new XmlMessageManipulator)( 225 | $response, 226 | fn (Document $document) => $document->manipulate( 227 | doSomethingWithResponseXml() 228 | ) 229 | ); 230 | }); 231 | } 232 | } 233 | ``` 234 | 235 | ## Loading WSDL with PSR-18 clients 236 | 237 | For loading WSDL's, you might want to use a PSR-18 client to do the hard HTTP work. 238 | This allows you for advances setups in which the WSDL is put behind basic authentication. 239 | This package provides a PSR-18 [WSDL loader](https://github.com/php-soap/wsdl#wsdl-loader) that can be used to load HTTP locations with your favourite HTTP client. 240 | It can be used in combinations with for example the WSDL loaders from the [php-soap/ext-soap-engine](https://github.com/php-soap/ext-soap-engine). 241 | 242 | ### Psr18Loader 243 | 244 | **Examples** 245 | 246 | ```php 247 | use Http\Client\Common\PluginClient; 248 | use Soap\Psr18Transport\Wsdl\Psr18Loader; 249 | use Soap\Wsdl\Loader\FlatteningLoader; 250 | 251 | $loader = Psr18Loader::createForClient( 252 | $wsdlClient = new PluginClient( 253 | $psr18Client, 254 | [...$middleware] 255 | ) 256 | ); 257 | 258 | // If you want to flatten all imports whilst using this PSR-18 loader: 259 | $loader = new FlatteningLoader($loader); 260 | 261 | 262 | $payload = $loader('http://some.wsdl'); 263 | ``` 264 | 265 | *NOTE:* If you want to flatten the imports inside the WSDL, you'll have to combine this loader with the the [FlatteningLoader](https://github.com/php-soap/wsdl#flatteningloader). 266 | 267 | 268 | ## Async SOAP calls 269 | 270 | Since PHP 8.1, fibers are introduced to PHP. 271 | This means that you can use any fiber based PSR-18 client in order to send async calls. 272 | 273 | Here is a short example for `react/http` in combination with `react/async`. 274 | 275 | ```sh 276 | composer require react/async veewee/psr18-react-browser 277 | ``` 278 | 279 | *(There currently is no official fiber based PSR-18 implementation of either AMP or ReactPHP. Therefore, [a small bridge can be used intermediately](https://github.com/veewee/psr18-react-browser))* 280 | 281 | 282 | Usage: 283 | 284 | ```php 285 | use Http\Client\Common\PluginClient; 286 | use Soap\Engine\SimpleEngine; 287 | use Soap\ExtSoapEngine\ExtSoapDriver; 288 | use Soap\ExtSoapEngine\ExtSoapOptions; 289 | use Soap\ExtSoapEngine\Wsdl\TemporaryWsdlLoaderProvider; 290 | use Soap\Psr18Transport\Psr18Transport; 291 | use Soap\Psr18Transport\Wsdl\Psr18Loader; 292 | use Soap\Wsdl\Loader\FlatteningLoader; 293 | use Veewee\Psr18ReactBrowser\Psr18ReactBrowserClient; 294 | use function React\Async\async; 295 | use function React\Async\await; 296 | use function React\Async\parallel; 297 | 298 | $asyncHttpClient = Psr18ReactBrowserClient::default(); 299 | $engine = new SimpleEngine( 300 | ExtSoapDriver::createFromClient( 301 | $client = AbusedClient::createFromOptions( 302 | ExtSoapOptions::defaults('http://www.dneonline.com/calculator.asmx?wsdl', []) 303 | ->disableWsdlCache() 304 | ) 305 | ), 306 | $transport = Psr18Transport::createForClient( 307 | new PluginClient( 308 | $asyncHttpClient, 309 | [...$middleware] 310 | ) 311 | ) 312 | ); 313 | 314 | $add = async(fn ($a, $b) => $engine->request('Add', [['intA' => $a, 'intB' => $b]])); 315 | $addWithLogger = fn ($a, $b) => $add($a, $b)->then( 316 | function ($result) use ($a, $b) { 317 | echo "SUCCESS {$a}+{$b} = ${result}!" . PHP_EOL; 318 | return $result; 319 | }, 320 | function (Exception $e) { 321 | echo 'ERROR: ' . $e->getMessage() . PHP_EOL; 322 | } 323 | ); 324 | 325 | $results = await(parallel([ 326 | fn() => $addWithLogger(1, 2), 327 | fn() => $addWithLogger(3, 4), 328 | fn() => $addWithLogger(5, 6) 329 | ])); 330 | 331 | var_dump($results); 332 | ``` 333 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-soap/psr18-transport", 3 | "description": "PSR-18 HTTP Client transport for SOAP", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Soap\\Psr18Transport\\": "src/" 9 | } 10 | }, 11 | "autoload-dev": { 12 | "psr-4": { 13 | "SoapTest\\Psr18Transport\\": "tests/" 14 | } 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Toon Verwerft", 19 | "email": "toonverwerft@gmail.com" 20 | } 21 | ], 22 | "require": { 23 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0", 24 | "ext-dom": "*", 25 | "php-soap/engine": "^2.13", 26 | "php-soap/wsdl": "^1.12", 27 | "php-soap/xml": "^1.8", 28 | "php-http/discovery": "^1.12", 29 | "psr/http-client-implementation": "^1.0", 30 | "psr/http-factory-implementation": "^1.0", 31 | "psr/http-message-implementation": "^1.0", 32 | "psr/http-message": "^1.0.1|^2.0", 33 | "veewee/xml": "^3.0", 34 | "php-http/client-common": "^2.3" 35 | }, 36 | "require-dev": { 37 | "ext-soap": "*", 38 | "nyholm/psr7": "^1.5", 39 | "php-http/mock-client": "^1.5", 40 | "php-soap/ext-soap-engine": "^1.7", 41 | "php-soap/engine-integration-tests": "^1.9", 42 | "phpunit/phpunit": "^10.0 || ^11.0", 43 | "guzzlehttp/guzzle": "^7.5" 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "php-http/discovery": true 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/psr18-transport.php: -------------------------------------------------------------------------------- 1 | disableWsdlCache() 17 | ) 18 | ), 19 | $transport = new TraceableTransport( 20 | $client, 21 | Psr18Transport::createForClient(new \GuzzleHttp\Client()) 22 | ) 23 | ); 24 | 25 | $result = $engine->request('Add', [['intA' => 1, 'intB' => 2]]); 26 | 27 | var_dump($result); 28 | var_dump($transport->collectLastRequestInfo()); 29 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Exception/RequestException.php: -------------------------------------------------------------------------------- 1 | getMessage(), (int) $exception->getCode(), $exception); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/HttpBinding/Psr7Converter.php: -------------------------------------------------------------------------------- 1 | requestFactory = $requestFactory; 22 | $this->streamFactory = $streamFactory; 23 | } 24 | 25 | public function convertSoapRequest(SoapRequest $request): RequestInterface 26 | { 27 | $builder = new Psr7RequestBuilder($this->requestFactory, $this->streamFactory); 28 | 29 | $request->isSOAP11() ? $builder->isSOAP11() : $builder->isSOAP12(); 30 | $builder->setEndpoint($request->getLocation()); 31 | $builder->setSoapAction($request->getAction()); 32 | $builder->setSoapMessage($request->getRequest()); 33 | 34 | return $builder->getHttpRequest(); 35 | } 36 | 37 | public function convertSoapResponse(ResponseInterface $response): SoapResponse 38 | { 39 | return new SoapResponse( 40 | (string) $response->getBody() 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/HttpBinding/Psr7RequestBuilder.php: -------------------------------------------------------------------------------- 1 | requestFactory = $requestFactory; 38 | $this->streamFactory = $streamFactory; 39 | } 40 | 41 | /** 42 | * @throws RequestException 43 | */ 44 | public function getHttpRequest(): RequestInterface 45 | { 46 | $this->validate(); 47 | 48 | try { 49 | $request = $this->requestFactory 50 | ->createRequest($this->httpMethod, $this->endpoint) 51 | ->withBody($this->prepareMessage()); 52 | 53 | foreach ($this->prepareHeaders() as $name => $value) { 54 | $request = $request->withHeader($name, $value); 55 | } 56 | } catch (InvalidArgumentException $e) { 57 | throw RequestException::fromException($e); 58 | } 59 | 60 | return $request; 61 | } 62 | 63 | public function setEndpoint(string $endpoint): void 64 | { 65 | $this->endpoint = $endpoint; 66 | } 67 | 68 | /** 69 | * Mark as SOAP 1.1 70 | */ 71 | public function isSOAP11(): void 72 | { 73 | $this->soapVersion = self::SOAP11; 74 | } 75 | 76 | /** 77 | * Mark as SOAP 1.2 78 | */ 79 | public function isSOAP12(): void 80 | { 81 | $this->soapVersion = self::SOAP12; 82 | } 83 | 84 | 85 | 86 | public function setSoapAction(string $soapAction): void 87 | { 88 | $this->soapAction = $soapAction; 89 | } 90 | 91 | public function setSoapMessage(string $content): void 92 | { 93 | $this->soapMessage = $this->streamFactory->createStream($content); 94 | $this->hasSoapMessage = true; 95 | } 96 | 97 | public function setHttpMethod(string $method): void 98 | { 99 | $this->httpMethod = $method; 100 | } 101 | 102 | /** 103 | * @throws RequestException 104 | */ 105 | private function validate(): void 106 | { 107 | if (!$this->endpoint) { 108 | throw RequestException::noEndpoint(); 109 | } 110 | 111 | if (!$this->hasSoapMessage && $this->httpMethod === 'POST') { 112 | throw RequestException::noMessage(); 113 | } 114 | 115 | /** 116 | * SOAP 1.1 only defines HTTP binding with POST method. 117 | * @link https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383527 118 | */ 119 | if ($this->soapVersion === self::SOAP11 && $this->httpMethod !== 'POST') { 120 | throw RequestException::postNotAllowedForSoap11(); 121 | } 122 | 123 | /** 124 | * SOAP 1.2 only defines HTTP binding with POST and GET methods. 125 | * @link https://www.w3.org/TR/2007/REC-soap12-part0-20070427/#L10309 126 | */ 127 | if ($this->soapVersion === self::SOAP12 && !in_array($this->httpMethod, ['GET', 'POST'], true)) { 128 | throw RequestException::invalidMethodForSoap12(); 129 | } 130 | } 131 | 132 | /** 133 | * @return array 134 | */ 135 | private function prepareHeaders(): array 136 | { 137 | if ($this->soapVersion === self::SOAP11) { 138 | return $this->prepareSoap11Headers(); 139 | } 140 | 141 | return $this->prepareSoap12Headers(); 142 | } 143 | 144 | /** 145 | * @link https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383526 146 | * @return array 147 | */ 148 | private function prepareSoap11Headers(): array 149 | { 150 | $headers = []; 151 | $headers['SOAPAction'] = $this->prepareQuotedSoapAction($this->soapAction); 152 | $headers['Content-Type'] = 'text/xml; charset="utf-8"'; 153 | 154 | return array_filter($headers); 155 | } 156 | 157 | /** 158 | * SOAPAction header is removed in SOAP 1.2 and now expressed as a value of 159 | * an (optional) "action" parameter of the "application/soap+xml" media type. 160 | * @link https://www.w3.org/TR/soap12-part0/#L4697 161 | * @return array 162 | */ 163 | private function prepareSoap12Headers(): array 164 | { 165 | $headers = []; 166 | if ($this->httpMethod !== 'POST') { 167 | $headers['Accept'] = 'application/soap+xml'; 168 | return $headers; 169 | } 170 | 171 | $soapAction = $this->prepareQuotedSoapAction($this->soapAction); 172 | $headers['Content-Type'] = 'application/soap+xml; charset="utf-8"' . '; action='.$soapAction; 173 | 174 | return array_filter($headers); 175 | } 176 | 177 | private function prepareMessage(): StreamInterface 178 | { 179 | if ($this->httpMethod === 'POST') { 180 | return $this->soapMessage ?? $this->streamFactory->createStream(''); 181 | } 182 | 183 | return $this->streamFactory->createStream(''); 184 | } 185 | 186 | private function prepareQuotedSoapAction(string $soapAction): string 187 | { 188 | $soapAction = trim($soapAction, '"\''); 189 | 190 | return '"'.$soapAction.'"'; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/HttpBinding/SoapActionDetector.php: -------------------------------------------------------------------------------- 1 | trim($action, '"\''); 15 | $header = $request->getHeader('SOAPAction'); 16 | if ($header) { 17 | return $normalize($header[0]); 18 | } 19 | 20 | $contentTypes = $request->getHeader('Content-Type'); 21 | if ($contentTypes) { 22 | $contentType = $contentTypes[0]; 23 | foreach (explode(';', $contentType) as $part) { 24 | if (strpos($part, 'action=') !== false) { 25 | return $normalize(explode('=', $part)[1]); 26 | } 27 | } 28 | } 29 | 30 | throw new RequestException('SOAP Action not found in HTTP headers.', $request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Middleware/RemoveEmptyNodesMiddleware.php: -------------------------------------------------------------------------------- 1 | xpath(new EnvelopePreset($xml)); 22 | 23 | do { 24 | $emptyNodes = $xpath->query('//soap:Envelope/*//*[not(node())]'); 25 | $emptyNodes->forEach( 26 | static fn (DOMNode $element) => remove($element) 27 | ); 28 | } while ($emptyNodes->count()); 29 | } 30 | ); 31 | 32 | return $next($request); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Middleware/SoapHeaderMiddleware.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $configurators; 22 | 23 | /** 24 | * @no-named-arguments 25 | * @param list $configurators 26 | */ 27 | public function __construct(callable ... $configurators) 28 | { 29 | $this->configurators = $configurators; 30 | } 31 | 32 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise 33 | { 34 | return $next((new XmlMessageManipulator)( 35 | $request, 36 | function (Document $document) { 37 | /** @var list $headers */ 38 | $headers = $document->build(new SoapHeaders(...$this->configurators)); 39 | 40 | return $document->manipulate(new PrependSoapHeaders(...$headers)); 41 | } 42 | )); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Middleware/WSICompliance/QuotedSoapActionMiddleware.php: -------------------------------------------------------------------------------- 1 | withHeader('SOAPAction', '"'.$soapAction.'"')); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Middleware/Wsdl/DisableExtensionsMiddleware.php: -------------------------------------------------------------------------------- 1 | then(function (ResponseInterface $response): ResponseInterface { 22 | return (new XmlMessageManipulator)( 23 | $response, 24 | fn (Document $document) => $this->disableExtensions($document) 25 | ); 26 | }); 27 | } 28 | 29 | private function disableExtensions(Document $document): void 30 | { 31 | $namespace = $document->locate(root_namespace_uri()); 32 | $document->xpath(new WsdlPreset($document)) 33 | ->query('//wsdl:binding//*[@wsdl:required]') 34 | ->expectAllOfType(DOMElement::class) 35 | ->forEach( 36 | static function (DOMElement $element) use ($namespace): void { 37 | namespaced_attribute($namespace ?? '', 'wsdl:required', 'false')($element); 38 | } 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Middleware/Wsdl/DisablePoliciesMiddleware.php: -------------------------------------------------------------------------------- 1 | then(function (ResponseInterface $response): ResponseInterface { 21 | return (new XmlMessageManipulator)( 22 | $response, 23 | fn (Document $document) => $this->disablePolicies($document) 24 | ); 25 | }); 26 | } 27 | 28 | public function disablePolicies(Document $document): void 29 | { 30 | $xpath = $document->xpath( 31 | namespaces([ 32 | 'wsd' => 'http://schemas.xmlsoap.org/ws/2004/09/policy' 33 | ]) 34 | ); 35 | 36 | // remove all "UsingPolicy" tags 37 | $xpath->query('//wsd:UsingPolicy') 38 | ->expectAllOfType(DOMElement::class) 39 | ->forEach( 40 | static function (DOMElement $element): void { 41 | remove($element); 42 | } 43 | ); 44 | 45 | // remove all "Policy" tags 46 | $xpath->query('//wsd:Policy') 47 | ->expectAllOfType(DOMElement::class) 48 | ->forEach( 49 | static function (DOMElement $element): void { 50 | remove($element); 51 | } 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Psr18Transport.php: -------------------------------------------------------------------------------- 1 | client = $client; 25 | $this->converter = $converter; 26 | } 27 | 28 | public static function createWithDefaultClient(): self 29 | { 30 | return self::createForClient(Psr18ClientDiscovery::find()); 31 | } 32 | 33 | public static function createForClient(ClientInterface $client): self 34 | { 35 | return new self( 36 | $client, 37 | new Psr7Converter( 38 | Psr17FactoryDiscovery::findRequestFactory(), 39 | Psr17FactoryDiscovery::findStreamFactory() 40 | ) 41 | ); 42 | } 43 | 44 | public function request(SoapRequest $request): SoapResponse 45 | { 46 | $psr7Request = $this->converter->convertSoapRequest($request); 47 | $psr7Response = $this->client->sendRequest($psr7Request); 48 | 49 | return $this->converter->convertSoapResponse($psr7Response); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Wsdl/Psr18Loader.php: -------------------------------------------------------------------------------- 1 | client = $client; 20 | $this->requestFactory = $requestFactory; 21 | } 22 | 23 | public static function createForClient(ClientInterface $client): self 24 | { 25 | return new self( 26 | $client, 27 | Psr17FactoryDiscovery::findRequestFactory(), 28 | ); 29 | } 30 | 31 | public function __invoke(string $location): string 32 | { 33 | $response = $this->client->sendRequest( 34 | $this->requestFactory->createRequest('GET', $location) 35 | ); 36 | 37 | $body = $response->getBody(); 38 | $body->rewind(); 39 | 40 | return (string) $body; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Xml/Loader/Psr7StreamLoader.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 20 | } 21 | 22 | /** 23 | * @throws RequestException 24 | */ 25 | public function __invoke(DOMDocument $document): void 26 | { 27 | $this->stream->rewind(); 28 | $contents = (string) $this->stream; 29 | if (!$contents) { 30 | throw RequestException::noMessage(); 31 | } 32 | 33 | xml_string_loader($contents)($document); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Xml/Mapper/Psr7StreamMapper.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class Psr7StreamMapper implements Mapper 16 | { 17 | public function __invoke(DOMDocument $document): StreamInterface 18 | { 19 | $factory = Psr17FactoryDiscovery::findStreamFactory(); 20 | $stream = $factory->createStream($document->saveXML()); 21 | $stream->rewind(); 22 | 23 | return $stream; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/XmlMessageManipulator.php: -------------------------------------------------------------------------------- 1 | getBody()))); 24 | $manipulator($document); 25 | 26 | return $message->withBody($document->map(new Psr7StreamMapper())); 27 | } 28 | } 29 | --------------------------------------------------------------------------------