From 0d5c50c97a89492b0bbab8072365d154a74f5c6b Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 23 Mar 2021 07:53:47 +0000 Subject: [PATCH 1/5] Adds auth methods and allows setting options on the client. --- README.md | 29 +++++++++++- src/Request/Request.php | 8 ++++ src/Request/SoapClientRequest.php | 61 +++++++++++++++++++++----- tests/Options/AuthTest.php | 56 ++++++++++++++++++++++++ tests/Options/OptionsTest.php | 73 +++++++++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 tests/Options/AuthTest.php create mode 100644 tests/Options/OptionsTest.php diff --git a/README.md b/README.md index 6d112f6..8b8a20e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ A Laravel SOAP client that provides a clean interface for handling requests and * [Call](#call) * [Parameters](#parameters) * [Nodes](#nodes) -- [Tracing](#Tracing) +- [Options](#options) + * [Tracing](#tracing) + * [Authentication](#authentication) - [Hooks](#hooks) - [Faking](#faking) - [Configuration](#configuration) @@ -148,7 +150,21 @@ Now, just by adding or removing a body to the `soap_node()` the outputted array A node can be made with either the Facade `Soap::node()` or the helper method `soap_node()`. -## Tracing +## Options + +You can set custom options for each soap request that will be passed to the Soap Client using the `withOptions` method. + +```php +Soap::to('...')->withOptions(['soap_version' => SOAP_1_2])->call('...'); +``` + +See [https://www.php.net/manual/en/soapclient.construct.php](https://www.php.net/manual/en/soapclient.construct.php) +for more details and available options. + +Soap also provides a number of methods that add syntactical sugar to the most commonly used options, which are detailed +below. + +### Tracing Soap allows you to easily trace your interactions with the SOAP endpoint being accessed. To trace all requests, set the following in the register method of your `ServiceProvider`: @@ -166,6 +182,15 @@ Now, just this `Response` will have a valid `Trace`. Tracing is null safe. If `$response->getTrace()` is called when a `Trace` hasn't been set, a new `Trace` is returned. This `Trace`'s properties will all return `null`. +### Authentication + +You can authenticate using Basic or Digest by calling `withBasicAuth` and `withDigestAuth` respectively. + +```php +Soap::to('...')->withBasicAuth('username', 'password')->call('...'); +Soap::to('...')->withDigestAuth('username', 'password')->call('...'); +``` + ## Hooks Hooks allow you to perform actions before and after Soap makes a request. diff --git a/src/Request/Request.php b/src/Request/Request.php index 3ae6569..2556ca1 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -32,7 +32,15 @@ public function getMethod(); public function getBody(); + public function getOptions(): array; + public function set($key, $value): self; public function trace($shouldTrace = true): self; + + public function withOptions(array $options): self; + + public function withBasicAuth($login, $password): self; + + public function withDigestAuth($login, $password): self; } diff --git a/src/Request/SoapClientRequest.php b/src/Request/SoapClientRequest.php index 8ca6605..dcde77e 100644 --- a/src/Request/SoapClientRequest.php +++ b/src/Request/SoapClientRequest.php @@ -5,6 +5,7 @@ use RicorocksDigitalAgency\Soap\Parameters\Builder; use RicorocksDigitalAgency\Soap\Response\Response; use RicorocksDigitalAgency\Soap\Support\Tracing\Trace; +use SoapClient; class SoapClientRequest implements Request { @@ -15,7 +16,7 @@ class SoapClientRequest implements Request protected Builder $builder; protected Response $response; protected $hooks = []; - protected $shouldTrace = false; + protected $options = []; public function __construct(Builder $builder) { @@ -56,15 +57,7 @@ protected function getRealResponse() { return tap( Response::new($this->makeRequest()), - fn($response) => $this->shouldTrace ? $this->addTrace($response) : $response - ); - } - - protected function addTrace($response) - { - return $response->setTrace( - Trace::thisXmlRequest($this->client()->__getLastRequest()) - ->thisXmlResponse($this->client()->__getLastResponse()) + fn($response) => data_get($this->options, 'trace') ? $this->addTrace($response) : $response ); } @@ -75,7 +68,13 @@ protected function makeRequest() protected function client() { - return $this->client ??= app(\SoapClient::class, ['wsdl' => $this->endpoint, 'options' => ['trace' => $this->shouldTrace]]); + return $this->client ??= app( + SoapClient::class, + [ + 'wsdl' => $this->endpoint, + 'options' => $this->options + ] + ); } public function getMethod() @@ -88,6 +87,14 @@ public function getBody() return $this->body; } + protected function addTrace($response) + { + return $response->setTrace( + Trace::thisXmlRequest($this->client()->__getLastRequest()) + ->thisXmlResponse($this->client()->__getLastResponse()) + ); + } + public function functions(): array { return $this->client()->__getFunctions(); @@ -128,7 +135,37 @@ public function set($key, $value): Request public function trace($shouldTrace = true): Request { - $this->shouldTrace = $shouldTrace; + $this->options['trace'] = $shouldTrace; + + return $this; + } + + public function withBasicAuth($login, $password): Request + { + $this->options['authentication'] = SOAP_AUTHENTICATION_BASIC; + $this->options['login'] = $login; + $this->options['password'] = $password; + + return $this; + } + + public function withDigestAuth($login, $password): Request + { + $this->options['authentication'] = SOAP_AUTHENTICATION_DIGEST; + $this->options['login'] = $login; + $this->options['password'] = $password; + + return $this; + } + + public function getOptions(): array + { + return $this->options; + } + + public function withOptions(array $options): Request + { + $this->options = array_merge($this->getOptions(), $options); return $this; } } diff --git a/tests/Options/AuthTest.php b/tests/Options/AuthTest.php new file mode 100644 index 0000000..b02fc45 --- /dev/null +++ b/tests/Options/AuthTest.php @@ -0,0 +1,56 @@ +withBasicAuth('hello', 'world') + ->call('Add', ['intA' => 10, 'intB' => 25]); + + Soap::assertSent(function(SoapClientRequest $request, $response) { + return $request->getOptions() == [ + 'authentication' => SOAP_AUTHENTICATION_BASIC, + 'login' => 'hello', + 'password' => 'world', + ]; + }); + } + + /** @test */ + public function digest_auth_can_be_set() + { + Soap::fake(); + + Soap::to(static::EXAMPLE_SOAP_ENDPOINT) + ->withDigestAuth('hello', 'world') + ->call('Add', ['intA' => 10, 'intB' => 25]); + + Soap::assertSent(function(SoapClientRequest $request, $response) { + return $request->getOptions() == [ + 'authentication' => SOAP_AUTHENTICATION_DIGEST, + 'login' => 'hello', + 'password' => 'world', + ]; + }); + } + +} diff --git a/tests/Options/OptionsTest.php b/tests/Options/OptionsTest.php new file mode 100644 index 0000000..b5e0b9e --- /dev/null +++ b/tests/Options/OptionsTest.php @@ -0,0 +1,73 @@ +withOptions(['compression' => SOAP_COMPRESSION_GZIP]) + ->call('Add', ['intA' => 10, 'intB' => 25]); + + Soap::assertSent( + function (SoapClientRequest $request, $response) { + return $request->getOptions() == [ + 'compression' => SOAP_COMPRESSION_GZIP, + ]; + } + ); + } + + /** @test */ + public function it_merges_with_other_options() + { + Soap::fake(); + + Soap::to(static::EXAMPLE_SOAP_ENDPOINT) + ->withBasicAuth('foo', 'bar') + ->withOptions(['compression' => SOAP_COMPRESSION_GZIP]) + ->call('Add', ['intA' => 10, 'intB' => 25]); + + Soap::assertSent( + function (SoapClientRequest $request, $response) { + return $request->getOptions() == [ + 'authentication' => SOAP_AUTHENTICATION_BASIC, + 'login' => 'foo', + 'password' => 'bar', + 'compression' => SOAP_COMPRESSION_GZIP, + ]; + } + ); + } + + /** @test */ + public function it_overrides_previous_values() + { + Soap::fake(); + + Soap::to(static::EXAMPLE_SOAP_ENDPOINT) + ->withOptions(['compression' => SOAP_COMPRESSION_ACCEPT]) + ->withOptions(['compression' => SOAP_COMPRESSION_GZIP]) + ->call('Add', ['intA' => 10, 'intB' => 25]); + + Soap::assertSent( + function (SoapClientRequest $request, $response) { + return $request->getOptions() == [ + 'compression' => SOAP_COMPRESSION_GZIP, + ]; + } + ); + } + +} From fd761240a09faa82e3067e4e4c2f5ba884f405cf Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 23 Mar 2021 10:53:10 +0000 Subject: [PATCH 2/5] Adds the ability to globally apply options. --- README.md | 40 +++++++++++++++++++-- src/Facades/Soap.php | 5 ++- src/Inclusion.php | 21 ++--------- src/OptionSet.php | 22 ++++++++++++ src/Soap.php | 19 ++++++++++ src/Support/Scoped.php | 30 ++++++++++++++++ tests/Inclusions/IncludeTest.php | 22 ++++++++++++ tests/Options/GlobalOptionsTest.php | 56 +++++++++++++++++++++++++++++ 8 files changed, 193 insertions(+), 22 deletions(-) create mode 100644 src/OptionSet.php create mode 100644 src/Support/Scoped.php create mode 100644 tests/Options/GlobalOptionsTest.php diff --git a/README.md b/README.md index 8b8a20e..2f8a817 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A Laravel SOAP client that provides a clean interface for handling requests and - [Options](#options) * [Tracing](#tracing) * [Authentication](#authentication) +- [Global Options](#global-options) - [Hooks](#hooks) - [Faking](#faking) - [Configuration](#configuration) @@ -191,6 +192,28 @@ Soap::to('...')->withBasicAuth('username', 'password')->call('...'); Soap::to('...')->withDigestAuth('username', 'password')->call('...'); ``` +### Global Options + +Sometimes, you may wish to include the same set of options on every SOAP request. You can do that using the `options` +method on the `Soap` facade: + +```php +// Every request will include this options automatically +Soap::options(['login' => 'foo', 'password' => 'bar']); +``` + +You may also want to include options on every request, but only for a certain endpoint or action: + +```php +// Only requests to this endpoint will include these options +Soap::options(['login' => 'foo', 'password' => 'bar'])->for('https://api.example.com'); + +// Only requests to this endpoint and the method Customers will include these options +Soap::options(['login' => 'foo', 'password' => 'bar'])->for('https://api.example.com', 'Customers'); +``` + +These calls are usually placed in the `boot` method of one of your application's Service Providers. + ## Hooks Hooks allow you to perform actions before and after Soap makes a request. @@ -291,18 +314,29 @@ Configuration of Soap is via the `Soap` facade in the `boot()` method in your se ### Include -Parameters can be set to be included with specific endpoints. These can be `arrays` or [nodes](#nodes) +Parameters can be set to be automatically included in all requests. These can be `arrays` or [nodes](#nodes) ```php -Soap::include(['credentials' => soap_node(['user' => '...', 'password' => '...'])])->for('...'); +Soap::include(['credentials' => soap_node(['user' => '...', 'password' => '...'])]); ``` You can even use dot syntax on your array keys to permeate deeper into the request body. ```php -Soap::include(['login.credentials' => soap_node(['user' => '...', 'password' => '...'])])->for('...'); +Soap::include(['login.credentials' => soap_node(['user' => '...', 'password' => '...'])]); +``` + +Often, you'll want to target specific endpoints or actions. You can chain the `for` method to achieve this. + +```php +// Only requests to https://api.example.com will include this data +Soap::include(['credentials' => soap_node(['user' => '...', 'password' => '...'])])->for('https://api.example.com'); + +// Only requests to https://api.example.com calling the Customers method will include this data +Soap::include(['credentials' => soap_node(['user' => '...', 'password' => '...'])])->for('https://api.example.com', 'Customers'); ``` + ## Ray Support This package comes with first party support for Ray, an awesome debugging tool by Spatie! We offer a couple of methods that you diff --git a/src/Facades/Soap.php b/src/Facades/Soap.php index 503a4b9..babd053 100644 --- a/src/Facades/Soap.php +++ b/src/Facades/Soap.php @@ -6,6 +6,8 @@ use Closure; use Illuminate\Support\Facades\Facade; +use RicorocksDigitalAgency\Soap\Inclusion; +use RicorocksDigitalAgency\Soap\OptionSet; use RicorocksDigitalAgency\Soap\Parameters\Node; use RicorocksDigitalAgency\Soap\Request\Request; @@ -15,7 +17,8 @@ * * @method static Request to(string $endpoint) * @method static Node node(array $attributes = []) - * @method static include(array $parameters) + * @method static Inclusion include(array $parameters) + * @method static OptionSet options(array $options) * @method static \RicorocksDigitalAgency\Soap\Soap beforeRequesting(callable $hook) * @method static \RicorocksDigitalAgency\Soap\Soap afterRequesting(callable $hook) * @method static void fake(array|Closure $callable = null) diff --git a/src/Inclusion.php b/src/Inclusion.php index f296604..9dc2d73 100644 --- a/src/Inclusion.php +++ b/src/Inclusion.php @@ -2,32 +2,17 @@ namespace RicorocksDigitalAgency\Soap; -class Inclusion +use RicorocksDigitalAgency\Soap\Support\Scoped; + +class Inclusion extends Scoped { protected $parameters; - protected $endpoint; - protected $method = null; public function __construct($parameters) { $this->parameters = $parameters; } - public function for($endpoint, $method = null) - { - $this->endpoint = $endpoint; - $this->method = $method; - } - - public function matches(string $endpoint, $method = null) - { - if ($this->endpoint !== $endpoint) { - return false; - } - - return empty($this->method) || $this->method === $method; - } - public function getParameters() { return $this->parameters; diff --git a/src/OptionSet.php b/src/OptionSet.php new file mode 100644 index 0000000..c4ed407 --- /dev/null +++ b/src/OptionSet.php @@ -0,0 +1,22 @@ +options = $options; + } + + public function getOptions() + { + return $this->options; + } +} diff --git a/src/Soap.php b/src/Soap.php index 82cc452..a13c7a8 100644 --- a/src/Soap.php +++ b/src/Soap.php @@ -17,6 +17,7 @@ class Soap protected Fakery $fakery; protected $inclusions = []; + protected $optionsSets = []; protected $globalHooks = []; public function __construct(Fakery $fakery) @@ -24,6 +25,7 @@ public function __construct(Fakery $fakery) $this->fakery = $fakery; $this->beforeRequesting(fn($request) => $request->fakeUsing($this->fakery->mockResponseIfAvailable($request))); $this->beforeRequesting(fn($request) => $this->mergeInclusionsFor($request)); + $this->beforeRequesting(fn($request) => $this->mergeOptionsFor($request)); $this->afterRequesting(fn($request, $response) => $this->record($request, $response)); } @@ -47,6 +49,13 @@ public function include($parameters) return $inclusion; } + public function options(array $options) + { + $options = new OptionSet($options); + $this->optionsSets[] = $options; + return $options; + } + protected function mergeInclusionsFor(Request $request) { collect($this->inclusions) @@ -57,6 +66,16 @@ protected function mergeInclusionsFor(Request $request) ->each(fn($value, $key) => $request->set($key, $value)); } + protected function mergeOptionsFor(Request $request) + { + collect($this->optionsSets) + ->filter + ->matches($request->getEndpoint(), $request->getMethod()) + ->map + ->getOptions() + ->each(fn($options) => $request->withOptions($options)); + } + public function beforeRequesting(callable $hook) { ($this->globalHooks['beforeRequesting'] ??= collect())->push($hook); diff --git a/src/Support/Scoped.php b/src/Support/Scoped.php new file mode 100644 index 0000000..f1f2e7f --- /dev/null +++ b/src/Support/Scoped.php @@ -0,0 +1,30 @@ +endpoint = $endpoint; + $this->method = $method; + } + + public function matches(string $endpoint, $method = null) + { + if (empty($this->endpoint)) { + return true; + } + + if ($this->endpoint !== $endpoint) { + return false; + } + + return empty($this->method) || $this->method === $method; + } +} diff --git a/tests/Inclusions/IncludeTest.php b/tests/Inclusions/IncludeTest.php index 083bc2c..03ed2c1 100644 --- a/tests/Inclusions/IncludeTest.php +++ b/tests/Inclusions/IncludeTest.php @@ -8,6 +8,28 @@ class IncludeTest extends TestCase { + /** @test */ + public function it_can_include_an_array_at_the_root_without_using_for() + { + Soap::fake(); + $this->mock(Builder::class) + ->shouldReceive('handle') + ->once() + ->withArgs( + function ($parameters) { + return $parameters == + [ + 'intA' => 10, + 'intB' => 25 + ]; + } + ); + + Soap::include(['intA' => 10]); + Soap::to(static::EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])); + + } + /** @test */ public function it_can_include_an_array_at_the_root_when_specified_using_the_include_method() { diff --git a/tests/Options/GlobalOptionsTest.php b/tests/Options/GlobalOptionsTest.php new file mode 100644 index 0000000..1bf6f8d --- /dev/null +++ b/tests/Options/GlobalOptionsTest.php @@ -0,0 +1,56 @@ + 'foo', 'password' => 'bar']); + Soap::to(static::EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])); + + Soap::assertSent(function($request) { + return $request->getOptions() == ['login' => 'foo', 'password' => 'bar']; + }); + } + + /** @test */ + public function it_can_scope_options_based_on_the_endpoint() + { + Soap::fake(); + + Soap::options(['login' => 'foo', 'password' => 'bar'])->for('https://foo.bar'); + Soap::options(['compression' => SOAP_COMPRESSION_GZIP])->for(static::EXAMPLE_SOAP_ENDPOINT); + + Soap::to(static::EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])); + + Soap::assertSent(function($request) { + return $request->getOptions() == ['compression' => SOAP_COMPRESSION_GZIP]; + }); + } + + /** @test */ + public function it_can_scope_options_based_on_the_endpoint_and_method() + { + Soap::fake(); + + Soap::options(['login' => 'foo', 'password' => 'bar'])->for(static::EXAMPLE_SOAP_ENDPOINT, 'Add'); + Soap::options(['compression' => SOAP_COMPRESSION_GZIP])->for(static::EXAMPLE_SOAP_ENDPOINT, 'Subtract'); + + Soap::to(static::EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])); + + Soap::assertSent(function($request) { + return $request->getOptions() == ['login' => 'foo', 'password' => 'bar']; + }); + } + +} From 6f0dc35203eab1cc59891b803a00c0b3b1698bf7 Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 23 Mar 2021 10:58:44 +0000 Subject: [PATCH 3/5] TOC update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f8a817..717c6c5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A Laravel SOAP client that provides a clean interface for handling requests and - [Options](#options) * [Tracing](#tracing) * [Authentication](#authentication) -- [Global Options](#global-options) + * [Global Options](#global-options) - [Hooks](#hooks) - [Faking](#faking) - [Configuration](#configuration) From 45ffda0258091b284a491ed0db79ab11299e9c70 Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 23 Mar 2021 10:59:28 +0000 Subject: [PATCH 4/5] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 717c6c5..e28d549 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Sometimes, you may wish to include the same set of options on every SOAP request method on the `Soap` facade: ```php -// Every request will include this options automatically +// Every request will include these options automatically Soap::options(['login' => 'foo', 'password' => 'bar']); ``` From 326d8b3881595e99daf72dd7c4dd21d6adb09132 Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 23 Mar 2021 11:06:25 +0000 Subject: [PATCH 5/5] Makes `Scoped` abstract --- src/Support/Scoped.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/Scoped.php b/src/Support/Scoped.php index f1f2e7f..f09b6d4 100644 --- a/src/Support/Scoped.php +++ b/src/Support/Scoped.php @@ -4,7 +4,7 @@ namespace RicorocksDigitalAgency\Soap\Support; -class Scoped +abstract class Scoped { protected $endpoint; protected $method = null;