diff --git a/README.md b/README.md index 58de45f..ba23559 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,25 @@ -# Guzzle Commands +# Guzzle Command -This library uses Guzzle and provides the foundations to create fully-featured -web service clients by abstracting Guzzle HTTP *requests* and *responses* into -higher-level *commands* and *results*. A *middleware* system, analogous to, but -separate from, the one in the HTTP layer may be used to customize client -behavior when preparing commands into requests and processing responses into -results. +Guzzle Command provides the foundation for building command-based web service clients on top of Guzzle. A command represents one service operation, and a result represents the processed response from that operation. -### Commands +Use this package when you are building an SDK-style client with named operations such as `listUsers()` or `createOrder()`. If you only need to send ordinary HTTP requests, install [`guzzlehttp/guzzle`](https://github.com/guzzle/guzzle/blob/8.0/README.md) instead. -Key-value pair objects representing an operation of a web service. Commands -have a name and a set of parameters. +For declarative service descriptions that define operations from API metadata, see [Guzzle Services](https://github.com/guzzle/guzzle-services/blob/2.0/README.md). -### Results +## Installation -Key-value pair objects representing the processed result of executing an -operation of a web service. - -## Installing - -This project can be installed using [Composer](https://getcomposer.org/): - -``` +```bash composer require guzzlehttp/command ``` ## Version Guidance -| Version | Status | PHP Version | -|---------|---------------------|--------------| -| 1.x | Latest | >=7.2.5,<8.6 | -| 2.x | Experimental | >=7.4,<8.6 | - -See [UPGRADING.md](UPGRADING.md) for upgrade notes. - -## Service Clients +| Version | Status | PHP Version | +|---------|--------------|--------------| +| 2.x | Experimental | >=7.4,<8.6 | +| 1.x | Latest | >=7.2.5,<8.6 | -Service Clients are web service clients that implement the -`GuzzleHttp\Command\ServiceClientInterface` and use an underlying Guzzle HTTP -client (`GuzzleHttp\ClientInterface`) to communicate with the service. Service -clients create and execute *commands* (`GuzzleHttp\Command\CommandInterface`), -which encapsulate operations within the web service, including the operation -name and parameters. This library provides a generic implementation of a service -client: the `GuzzleHttp\Command\ServiceClient` class. - -## Instantiating a Service Client - -The provided service client implementation (`GuzzleHttp\Command\ServiceClient`) -can be instantiated by providing the following arguments: - -1. A fully-configured Guzzle HTTP client that will be used to perform the - underlying HTTP requests. That is, an instance of an object implementing - `GuzzleHttp\ClientInterface` such as `new GuzzleHttp\Client()`. -1. A callable that transforms a Command into a Request. The callable is invoked - as `callable(GuzzleHttp\Command\CommandInterface): Psr\Http\Message\RequestInterface`. -1. A callable that transforms a Response into a Result. The callable is invoked - as `callable(Psr\Http\Message\ResponseInterface, Psr\Http\Message\RequestInterface, GuzzleHttp\Command\CommandInterface): GuzzleHttp\Command\ResultInterface`. -1. Optionally, a Guzzle HandlerStack (`GuzzleHttp\HandlerStack`), which can be - used to add command-level middleware to the service client. - -Below is an example configured to send and receive JSON payloads: +## Quick Start ```php use GuzzleHttp\Client as HttpClient; @@ -68,7 +28,6 @@ use GuzzleHttp\Command\Result; use GuzzleHttp\Command\ResultInterface; use GuzzleHttp\Command\ServiceClient; use GuzzleHttp\Psr7\Request; -use GuzzleHttp\UriTemplate\UriTemplate; use GuzzleHttp\Utils; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -78,8 +37,8 @@ $client = new ServiceClient( function (CommandInterface $command): RequestInterface { return new Request( 'POST', - UriTemplate::expand('/{command}', ['command' => $command->getName()]), - ['Accept' => 'application/json', 'Content-Type' => 'application/json'], + '/' . rawurlencode($command->getName()), + ['Content-Type' => 'application/json'], Utils::jsonEncode($command->toArray()) ); }, @@ -88,225 +47,23 @@ $client = new ServiceClient( RequestInterface $request, CommandInterface $command ): ResultInterface { - return new Result( - Utils::jsonDecode((string) $response->getBody(), true) - ); + return new Result(Utils::jsonDecode((string) $response->getBody(), true)); } ); -``` - -## Executing Commands - -Service clients create command objects using the ``getCommand()`` method. - -```php -$commandName = 'foo'; -$arguments = ['baz' => 'bar']; -$command = $client->getCommand($commandName, $arguments); -``` - -After creating a command, you may execute the command using the `execute()` -method of the client. - -```php -$result = $client->execute($command); -``` - -The result of executing a command will be an instance of an object implementing -`GuzzleHttp\Command\ResultInterface`. Result objects are `ArrayAccess`-ible and -contain the data parsed from HTTP response. - -Service clients have magic methods that act as shortcuts to executing commands -by name without having to create the ``Command`` object in a separate step -before executing it. - -```php -$result = $client->foo(['baz' => 'bar']); -``` - -### Per-command HTTP options - -`GuzzleHttp\Command\ServiceClient` reserves the `@http` command parameter for -per-command Guzzle request options. When a command is executed, the service -client reads `$command['@http']`, removes it from the command, transforms the -remaining command data into a PSR-7 request, and passes the `@http` array to the -underlying Guzzle HTTP client. - -This is intended for trusted application code that needs to adjust transport -behavior for a single command, such as setting a shorter timeout. Treat `@http` -as a reserved control key, not as an operation parameter. Do not pass untrusted -input directly into command arguments without filtering it first. If external -input can include `@http`, that input may be able to influence the underlying -HTTP request or transfer depending on the configured Guzzle client and handler. -The `@http` value must be an array of Guzzle request options. Be especially -careful with options that affect the target URI, proxy, TLS verification, -headers, body, response sink, redirects, or timeouts. - -Build command arguments from an allowlist of expected operation parameters, or -explicitly reject reserved keys such as `@http` before creating commands: - -```php -if (array_key_exists('@http', $input)) { - throw new InvalidArgumentException('"@http" is reserved.'); -} - -$command = $client->getCommand('foo', [ - 'baz' => (string) $input['baz'], -]); -``` - -When setting per-command HTTP options intentionally, only expose and validate the -specific options your application needs: - -```php -use GuzzleHttp\RequestOptions; - -$command = $client->getCommand('foo', [ - 'baz' => 'bar', - '@http' => [ - RequestOptions::CONNECT_TIMEOUT => 1.0, - RequestOptions::TIMEOUT => 2.0, - ], -]); - -$result = $client->execute($command); -``` - -Because `@http` is removed during execution, create a new command if you need to -execute the same operation again with the same per-command HTTP options. - -## Asynchronous Commands - -Commands can be executed asynchronously using `executeAsync()`. This method -returns a `GuzzleHttp\Promise\PromiseInterface`. -```php -use GuzzleHttp\Command\ResultInterface; - -// Create and execute an asynchronous command. -$command = $client->getCommand('foo', ['baz' => 'bar']); -$promise = $client->executeAsync($command); - -$promise->then(function (ResultInterface $result) { - echo $result['fizz']; //> 'buzz' -})->wait(); -``` - -Synchronous execution is equivalent to waiting on the asynchronous operation: - -```php -$result = $promise->wait(); - -echo $result['fizz']; //> 'buzz' -``` - -Magic methods may also be used asynchronously by appending `Async` to the -operation name. For example, `fooAsync()` creates a `foo` command and executes it -asynchronously: - -```php -$promise = $client->fooAsync(['baz' => 'bar']); -$result = $promise->wait(); +$result = $client->createUser(['name' => 'Ada']); ``` -If built-in execution fails, the promise is typically rejected with a -`GuzzleHttp\Command\Exception\CommandException`. When HTTP errors are enabled, -4xx and 5xx responses are represented by `CommandClientException` and -`CommandServerException`, respectively, when the underlying Guzzle exception -contains a response. Custom middleware and handlers may reject with other values. - -## Concurrent Requests +The service client can also execute commands asynchronously and run many commands with a configurable concurrency limit. -Use `executeAll()` or `executeAllAsync()` to execute multiple commands with a -fixed concurrency limit. Both methods accept an array or iterator that yields -`CommandInterface` objects. +## Documentation -`executeAll()` waits for the pool to finish and returns an array keyed like the -input commands. Successful entries contain results. Failed entries contain the -rejection reason, typically a `CommandException`. Callback keys may be integers, -strings, or `null`. Returned array keys follow PHP array-key normalization; -numeric-string keys may become integers, and `null` keys are stored as an empty -string. - -```php -use GuzzleHttp\Command\ResultInterface; - -$commands = [ - 'first' => $client->getCommand('foo', ['baz' => 'bar']), - 'second' => $client->getCommand('foo', ['baz' => 'qux']), -]; - -$results = $client->executeAll($commands, [ - 'concurrency' => 10, - 'fulfilled' => function (ResultInterface $result, $key) { - // Called when one command succeeds. - }, - 'rejected' => function ($reason, $key) { - // Called when one command fails. - }, -]); -``` - -`executeAllAsync()` returns a promise for the command pool instead of waiting for -it immediately. Fulfilled and rejected callbacks may also declare the aggregate -promise as a third argument: - -```php -use GuzzleHttp\Command\ResultInterface; -use GuzzleHttp\Promise\PromiseInterface; - -$promise = $client->executeAllAsync($commands, [ - 'concurrency' => 10, - 'fulfilled' => function (ResultInterface $result, $key, PromiseInterface $aggregate) { - // Called when one command succeeds. - }, - 'rejected' => function ($reason, $key, PromiseInterface $aggregate) { - // Called when one command fails. - }, -]); - -$promise->wait(); -``` - -The supported options are: - -* `concurrency`: Maximum number of commands to execute at the same time. The - default is `25`. -* `fulfilled`: Callable invoked as `fulfilled($result, $key)` by `executeAll()` - when an individual command succeeds. `executeAllAsync()` also passes the - aggregate promise as a third argument. -* `rejected`: Callable invoked as `rejected($reason, $key)` by `executeAll()` - when an individual command fails. `executeAllAsync()` also passes the aggregate - promise as a third argument. - -Choose a concurrency value that is appropriate for the remote service and your -application. Very large command lists should generally be streamed with an -iterator rather than built eagerly as a large array. - -## Middleware: Extending the Client - -Middleware can be added to the service client or underlying HTTP client to -implement additional behavior and customize the ``Command``-to-``Result`` and -``Request``-to-``Response`` lifecycles, respectively. - -Command middleware is added to the service client's handler stack and wraps -commands before they are transformed into HTTP requests. Command handlers use the -shape `callable(GuzzleHttp\Command\CommandInterface): GuzzleHttp\Promise\PromiseInterface`. HTTP middleware should be configured on the underlying Guzzle HTTP client instead. - -```php -use GuzzleHttp\Command\CommandInterface; -use GuzzleHttp\RequestOptions; - -$client->getHandlerStack()->push(function (callable $handler) { - return function (CommandInterface $command) use ($handler) { - $http = $command['@http'] ?: []; - $http[RequestOptions::TIMEOUT] = 2.0; - $command['@http'] = $http; - - return $handler($command); - }; -}); -``` +- [Service Clients](docs/service-clients.md) +- [Executing Commands](docs/executing-commands.md) +- [Async and Concurrency](docs/async-and-concurrency.md) +- [Middleware: Extending the Client](docs/middleware-extending-the-client.md) +- [Upgrade Guide](UPGRADING.md) +- [Changelog](CHANGELOG.md) ## Security @@ -314,7 +71,7 @@ If you discover a security vulnerability within this package, please send an ema ## License -Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information. +Guzzle Command is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information. ## For Enterprise diff --git a/docs/async-and-concurrency.md b/docs/async-and-concurrency.md new file mode 100644 index 0000000..1da811b --- /dev/null +++ b/docs/async-and-concurrency.md @@ -0,0 +1,127 @@ +# Async and Concurrency + +This page explains asynchronous command execution and concurrent command pools for Guzzle Command service clients. For promise chaining, waiting, cancellation, and rejection behavior, see the [Guzzle Promises quick start](https://github.com/guzzle/promises/blob/3.0/docs/promise-quick-start.md). + +## Asynchronous Commands + +Commands can be executed asynchronously using `executeAsync()`. This method +returns a `GuzzleHttp\Promise\PromiseInterface`. +See the [Guzzle Promises API](https://github.com/guzzle/promises/blob/3.0/docs/promise-api.md) for +promise helper details. + +```php +use GuzzleHttp\Command\ResultInterface; + +// Create and execute an asynchronous command. +$command = $client->getCommand('foo', ['baz' => 'bar']); +$promise = $client->executeAsync($command); + +$promise->then(function (ResultInterface $result) { + echo $result['fizz']; //> 'buzz' +})->wait(); +``` + +Synchronous execution is equivalent to waiting on the asynchronous operation: + +```php +$result = $promise->wait(); + +echo $result['fizz']; //> 'buzz' +``` + +Magic methods may also be used asynchronously by appending `Async` to the +operation name. For example, `fooAsync()` creates a `foo` command and executes it +asynchronously: + +```php +$promise = $client->fooAsync(['baz' => 'bar']); +$result = $promise->wait(); +``` + +If built-in execution fails, the promise is typically rejected with a +`GuzzleHttp\Command\Exception\CommandException`. When HTTP errors are enabled, +4xx and 5xx responses are represented by `CommandClientException` and +`CommandServerException`, respectively, when the underlying Guzzle exception +contains a response. Custom middleware and handlers may reject with other values. + +## Concurrent Commands + +Use `executeAll()` or `executeAllAsync()` to execute multiple commands with a +concurrency limit. Both methods accept an array or iterator that yields +`CommandInterface` objects. If no concurrency option is provided, the default is +`25` commands at a time. + +`executeAll()` waits for the pool to finish and returns an array keyed like the +input commands. Successful entries contain `ResultInterface` objects. Failed +entries contain the rejection reason, typically a `CommandException`. The method +does not throw merely because one command failed; each failure reason is stored +in the returned array unless the pool itself cannot be created or waited on. +Callback keys may be integers, strings, or `null`. Returned array keys follow +PHP array-key normalization; numeric-string keys may become integers, and `null` +keys are stored as an empty string. + +```php +use GuzzleHttp\Command\ResultInterface; + +$commands = [ + 'first' => $client->getCommand('foo', ['baz' => 'bar']), + 'second' => $client->getCommand('foo', ['baz' => 'qux']), +]; + +$results = $client->executeAll($commands, [ + 'concurrency' => 10, + 'fulfilled' => function (ResultInterface $result, $key) { + // Called when one command succeeds. + }, + 'rejected' => function ($reason, $key) { + // Called when one command fails. + }, +]); +``` + +`executeAllAsync()` returns a promise for the command pool instead of waiting for +it immediately. It resolves with `null` after all commands have settled; it does +not build a result array. Individual command results are delivered to the +`fulfilled` callback, and individual rejection reasons are delivered to the +`rejected` callback. Fulfilled and rejected callbacks may also declare the +aggregate promise as a third argument: + +```php +use GuzzleHttp\Command\ResultInterface; +use GuzzleHttp\Promise\PromiseInterface; + +$promise = $client->executeAllAsync($commands, [ + 'concurrency' => 10, + 'fulfilled' => function (ResultInterface $result, $key, PromiseInterface $aggregate) { + // Called when one command succeeds. + }, + 'rejected' => function ($reason, $key, PromiseInterface $aggregate) { + // Called when one command fails. + }, +]); + +$promise->wait(); +``` + +The supported options are: + +- `concurrency`: Maximum number of commands to execute at the same time. The + default is `25`. This may be an integer or a callable. A callable receives the + current number of pending commands and returns the current concurrency limit, + allowing the limit to change while the pool is running. +- `fulfilled`: Callable invoked as `fulfilled($result, $key)` by `executeAll()` + when an individual command succeeds. `executeAllAsync()` also passes the + aggregate promise as a third argument. +- `rejected`: Callable invoked as `rejected($reason, $key)` by `executeAll()` + when an individual command fails. `executeAllAsync()` also passes the aggregate + promise as a third argument. + +Choose a concurrency value that is appropriate for the remote service and your +application. Very large command lists should generally be streamed with an +iterator rather than built eagerly as a large array. + +## Related + +- [Service Clients](service-clients.md) +- [Executing Commands](executing-commands.md) +- [Middleware: Extending the Client](middleware-extending-the-client.md) diff --git a/docs/executing-commands.md b/docs/executing-commands.md new file mode 100644 index 0000000..9638631 --- /dev/null +++ b/docs/executing-commands.md @@ -0,0 +1,135 @@ +# Executing Commands + +This page covers creating command objects, executing them synchronously, using +magic operation methods, working with command and result collections, and +passing per-command HTTP options to the underlying Guzzle client. + +Service clients create command objects using the `getCommand()` method. + +```php +$commandName = 'foo'; +$arguments = ['baz' => 'bar']; +$command = $client->getCommand($commandName, $arguments); +``` + +After creating a command, you may execute the command using the `execute()` +method of the client. + +```php +$result = $client->execute($command); +``` + +The result of executing a command will be an instance of an object implementing +`GuzzleHttp\Command\ResultInterface`. Results are array-like objects that +contain the data parsed from the HTTP response by the service client's +response-to-result transformer. + +Service clients have magic methods that act as shortcuts to executing commands +by name without having to create the `Command` object in a separate step before +executing it. + +```php +$result = $client->foo(['baz' => 'bar']); +``` + +## Command and Result Data + +Commands and results implement `ArrayAccess`, `Countable`, `IteratorAggregate`, +and `GuzzleHttp\Command\ToArrayInterface`. + +Use array access to read, write, and remove values: + +```php +$command = $client->getCommand('foo', ['baz' => 'bar']); + +$command['baz'] = 'qux'; +unset($command['unused']); + +$result = $client->execute($command); +echo $result['fizz']; +``` + +Reading a missing key returns `null`. For commands, use `hasParam()` when you +need to distinguish a missing parameter from a parameter whose value is `null`. + +Use `count()` to count stored values, iterate with `foreach`, and call +`toArray()` to retrieve the underlying array: + +```php +foreach ($result as $name => $value) { + // Inspect result values. +} + +$data = $result->toArray(); +$total = count($result); +``` + +Commands also provide `hasParam()` to test for a parameter by key, including +parameters set to `null`: + +```php +if ($command->hasParam('baz')) { + // The command contains the baz parameter. +} +``` + +For both commands and results, a `null` array key is normalized to an empty +string when reading, writing, unsetting, or checking values. + +## Per-Command HTTP Options + +`GuzzleHttp\Command\ServiceClient` reserves the `@http` command parameter for +per-command Guzzle request options. When a command is executed, the service +client reads `$command['@http']`, removes it from the command, transforms the +remaining command data into a PSR-7 request, and passes the `@http` array to the +underlying Guzzle HTTP client. + +This is intended for trusted application code that needs to adjust transport +behavior for a single command, such as setting a shorter timeout. Treat `@http` +as a reserved control key, not as an operation parameter. Do not pass untrusted +input directly into command arguments without filtering it first. If external +input can include `@http`, that input may be able to influence the underlying +HTTP request or transfer depending on the configured Guzzle client and handler. +The `@http` value must be an array of [Guzzle request +options](https://github.com/guzzle/guzzle/blob/8.0/docs/request-options.md). Be +especially careful with options that affect the target URI, proxy, TLS +verification, headers, body, response sink, redirects, or timeouts. + +Build command arguments from an allowlist of expected operation parameters, or +explicitly reject reserved keys such as `@http` before creating commands: + +```php +if (array_key_exists('@http', $input)) { + throw new InvalidArgumentException('"@http" is reserved.'); +} + +$command = $client->getCommand('foo', [ + 'baz' => (string) $input['baz'], +]); +``` + +When setting per-command HTTP options intentionally, only expose and validate the +specific options your application needs: + +```php +use GuzzleHttp\RequestOptions; + +$command = $client->getCommand('foo', [ + 'baz' => 'bar', + '@http' => [ + RequestOptions::CONNECT_TIMEOUT => 1.0, + RequestOptions::TIMEOUT => 2.0, + ], +]); + +$result = $client->execute($command); +``` + +Because `@http` is removed during execution, create a new command if you need to +execute the same operation again with the same per-command HTTP options. + +## Related + +- [Service Clients](service-clients.md) +- [Async and Concurrency](async-and-concurrency.md) +- [Middleware: Extending the Client](middleware-extending-the-client.md) diff --git a/docs/middleware-extending-the-client.md b/docs/middleware-extending-the-client.md new file mode 100644 index 0000000..3e6877a --- /dev/null +++ b/docs/middleware-extending-the-client.md @@ -0,0 +1,60 @@ +# Middleware: Extending the Client + +Middleware can be added to the service client or underlying HTTP client to +implement additional behavior and customize the `Command`-to-`Result` and +`Request`-to-`Response` lifecycles, respectively. + +Command middleware is added to the service client's handler stack and wraps +commands before they are transformed into HTTP requests. Command handlers use the +shape `callable(GuzzleHttp\Command\CommandInterface): GuzzleHttp\Promise\PromiseInterface`. HTTP middleware should be configured on the underlying Guzzle HTTP client instead. + +The service client's command stack is separate from the underlying Guzzle HTTP +client stack: + +- Command middleware receives commands and resolves to results. +- HTTP middleware receives PSR-7 requests and resolves to PSR-7 responses. +- Use [Guzzle HTTP middleware](https://github.com/guzzle/guzzle/blob/8.0/docs/handlers-and-middleware.md#middleware) for transport behavior such as retries, signing, logging, and request/response inspection. + +## Adding Command Middleware + +```php +use GuzzleHttp\Command\CommandInterface; +use GuzzleHttp\RequestOptions; + +$client->getHandlerStack()->push(function (callable $handler) { + return function (CommandInterface $command) use ($handler) { + $http = $command['@http'] ?: []; + $http[RequestOptions::TIMEOUT] = 2.0; + $command['@http'] = $http; + + return $handler($command); + }; +}); +``` + +## Command Stack Lifecycle + +`ServiceClient::getCommand()` clones the service client's current command +handler stack into the returned command. Middleware added to the service client +after a command has been created does not affect that existing command. + +```php +$first = $client->getCommand('foo'); + +$client->getHandlerStack()->push($middleware); + +$second = $client->getCommand('foo'); + +// $first uses the stack captured before $middleware was added. +// $second uses the stack that includes $middleware. +``` + +When `executeAsync()` runs, it resolves the command's handler stack. If a custom +`CommandInterface` returns `null` from `getHandlerStack()`, `executeAsync()` +falls back to the service client's current command handler stack. + +## Related + +- [Service Clients](service-clients.md) +- [Executing Commands](executing-commands.md) +- [Async and Concurrency](async-and-concurrency.md) diff --git a/docs/service-clients.md b/docs/service-clients.md new file mode 100644 index 0000000..a36826c --- /dev/null +++ b/docs/service-clients.md @@ -0,0 +1,163 @@ +# Service Clients + +Guzzle Command helps create web service clients by mapping high-level commands +to Guzzle HTTP requests and mapping HTTP responses back to command results. It +is useful when an application should call named service operations instead of +constructing every HTTP request directly. + +Command middleware can customize the command-to-result lifecycle. This is +separate from Guzzle HTTP middleware, which customizes the request-to-response +lifecycle on the underlying HTTP client. + +## Core Concepts + +A *service* is the remote API your client calls. In Guzzle Command, a service is +represented by a service client that knows how to turn operation names and +parameters into HTTP requests. + +A *command* is an object that represents one service operation. It has an +operation name, such as `createUser`, and a set of parameters for that +operation. + +A *result* is an object that represents the processed response from executing a +command. Results usually contain decoded response data rather than raw PSR-7 +responses. + +## Commands + +Commands are key-value pair objects representing a single operation of a web +service. Commands have a name and a set of parameters. + +## Results + +Results are key-value pair objects representing the processed result of +executing an operation of a web service. + +## Service Clients + +Service Clients are web service clients that implement the +`GuzzleHttp\Command\ServiceClientInterface` and use an underlying Guzzle HTTP +client (`GuzzleHttp\ClientInterface`) to communicate with the service. Service +clients create and execute commands (`GuzzleHttp\Command\CommandInterface`), +which encapsulate operations within the web service, including the operation +name and parameters. This library provides a generic implementation of a service +client: the `GuzzleHttp\Command\ServiceClient` class. + +## Instantiating a Service Client + +The provided service client implementation (`GuzzleHttp\Command\ServiceClient`) +can be instantiated by providing the following arguments: + +1. A fully-configured Guzzle HTTP client that will be used to perform the + underlying HTTP requests. That is, an instance of an object implementing + `GuzzleHttp\ClientInterface` such as `new GuzzleHttp\Client()`. +1. A callable that transforms a command into a request. The callable is invoked + as `callable(GuzzleHttp\Command\CommandInterface): Psr\Http\Message\RequestInterface`. +1. A callable that transforms a response into a result. The callable is invoked + as `callable(Psr\Http\Message\ResponseInterface, Psr\Http\Message\RequestInterface, GuzzleHttp\Command\CommandInterface): GuzzleHttp\Command\ResultInterface`. +1. Optionally, a Guzzle HandlerStack (`GuzzleHttp\HandlerStack`), which can be + used to add command-level middleware to the service client. + +Below is an example configured to send and receive JSON payloads: + +```php +use GuzzleHttp\Client as HttpClient; +use GuzzleHttp\Command\CommandInterface; +use GuzzleHttp\Command\Result; +use GuzzleHttp\Command\ResultInterface; +use GuzzleHttp\Command\ServiceClient; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Utils; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +$client = new ServiceClient( + new HttpClient(['base_uri' => 'https://api.example.com']), + function (CommandInterface $command): RequestInterface { + return new Request( + 'POST', + '/' . rawurlencode($command->getName()), + ['Accept' => 'application/json', 'Content-Type' => 'application/json'], + Utils::jsonEncode($command->toArray()) + ); + }, + function ( + ResponseInterface $response, + RequestInterface $request, + CommandInterface $command + ): ResultInterface { + return new Result( + Utils::jsonDecode((string) $response->getBody(), true) + ); + } +); +``` + +## Transformers + +The command-to-request transformer adapts your service operation model to HTTP. +It can choose the HTTP method, URI, headers, and body using the command name and +parameters: + +```php +use GuzzleHttp\Command\CommandInterface; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Utils; +use Psr\Http\Message\RequestInterface; + +$commandToRequest = function (CommandInterface $command): RequestInterface { + $body = Utils::jsonEncode($command->toArray()); + $path = '/commands/' . rawurlencode($command->getName()); + + return new Request( + 'POST', + $path, + ['Content-Type' => 'application/json'], + $body + ); +}; +``` + +The response-to-result transformer adapts the HTTP response to your SDK result +shape. It receives the response, request, and original command: + +```php +use GuzzleHttp\Command\CommandInterface; +use GuzzleHttp\Command\Result; +use GuzzleHttp\Command\ResultInterface; +use GuzzleHttp\Utils; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +$responseToResult = function ( + ResponseInterface $response, + RequestInterface $request, + CommandInterface $command +): ResultInterface { + return new Result([ + 'operation' => $command->getName(), + 'statusCode' => $response->getStatusCode(), + 'data' => Utils::jsonDecode((string) $response->getBody(), true), + ]); +}; +``` + +## Command Middleware and HTTP Middleware + +Command middleware is added to the service client's command handler stack. It +receives a command, may inspect or modify command parameters, and returns a +promise that resolves to a `ResultInterface`. + +HTTP middleware is added to the underlying Guzzle HTTP client's handler stack. +It receives PSR-7 requests after a command has been transformed and before the +request is sent. + +Use command middleware for operation-level concerns, such as adding command +defaults or inspecting results. Use HTTP middleware for transport-level concerns, +such as request signing, retries, or logging raw HTTP messages. + +## Related + +- [Executing Commands](executing-commands.md) +- [Async and Concurrency](async-and-concurrency.md) +- [Middleware: Extending the Client](middleware-extending-the-client.md)