From b744011840816c7135d641e84d0fc60615eef94e Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 9 Jun 2026 01:22:56 +0100 Subject: [PATCH 1/5] Refresh README documentation --- README.md | 290 ++++---------------------------------------- docs/index.md | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 346 insertions(+), 267 deletions(-) create mode 100644 docs/index.md diff --git a/README.md b/README.md index 58de45f..34fddbf 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,16 @@ # Guzzle Commands -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. +`guzzlehttp/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) instead. -Key-value pair objects representing an operation of a web service. Commands -have a name and a set of parameters. +## Installation -### Results - -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 - -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 +19,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 +28,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 +38,31 @@ $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); +$result = $client->createUser(['name' => 'Ada']); ``` -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. +The service client can also execute commands asynchronously and run many commands with a fixed concurrency limit. -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. +## Documentation -```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; +- [Full documentation](docs/index.md) +- [Service clients](docs/index.md#service-clients) +- [Executing commands](docs/index.md#executing-commands) +- [Asynchronous commands](docs/index.md#asynchronous-commands) +- [Concurrent requests](docs/index.md#concurrent-requests) +- [Middleware](docs/index.md#middleware-extending-the-client) +- [Upgrade guide](UPGRADING.md) -// 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 Requests - -Use `executeAll()` or `executeAllAsync()` to execute multiple commands with a -fixed concurrency limit. Both methods accept an array or iterator that yields -`CommandInterface` objects. - -`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; +## Version Guidance - return $handler($command); - }; -}); -``` +| Version | Status | PHP Version | +|---------|--------------|--------------| +| 2.x | Experimental | >=7.4,<8.6 | +| 1.x | Latest | >=7.2.5,<8.6 | ## Security diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9d1cd7f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,323 @@ +# Guzzle Commands Documentation + +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. + +### Commands + +Key-value pair objects representing an operation of a web service. Commands +have a name and a set of parameters. + +### Results + +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/): + +``` +composer require guzzlehttp/command +``` + +## Version Guidance + +| Version | Status | PHP Version | +|---------|--------------|--------------| +| 2.x | Experimental | >=7.4,<8.6 | +| 1.x | Latest | >=7.2.5,<8.6 | + +See [UPGRADING.md](../UPGRADING.md) for upgrade notes. + +## 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\UriTemplate\UriTemplate; +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', + UriTemplate::expand('/{command}', ['command' => $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) + ); + } +); +``` + +## 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(); +``` + +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 + +Use `executeAll()` or `executeAllAsync()` to execute multiple commands with a +fixed concurrency limit. Both methods accept an array or iterator that yields +`CommandInterface` objects. + +`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); + }; +}); +``` + +## Security + +If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/command/security/policy) for more information. + +## License + +Guzzle is made available under the MIT License (MIT). Please see [License File](../LICENSE) for more information. + +## For Enterprise + +Available as part of the Tidelift Subscription + +The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-command?utm_source=packagist-guzzlehttp-command&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) From c3f73aa99a17452f15dc6e120263ce5b55a8fe37 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 9 Jun 2026 01:29:42 +0100 Subject: [PATCH 2/5] Move quick start below version guidance --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 34fddbf..de8351d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ Use this package when you are building an SDK-style client with named operations composer require guzzlehttp/command ``` +## Version Guidance + +| Version | Status | PHP Version | +|---------|--------------|--------------| +| 2.x | Experimental | >=7.4,<8.6 | +| 1.x | Latest | >=7.2.5,<8.6 | + ## Quick Start ```php @@ -57,13 +64,6 @@ The service client can also execute commands asynchronously and run many command - [Middleware](docs/index.md#middleware-extending-the-client) - [Upgrade guide](UPGRADING.md) -## Version Guidance - -| Version | Status | PHP Version | -|---------|--------------|--------------| -| 2.x | Experimental | >=7.4,<8.6 | -| 1.x | Latest | >=7.2.5,<8.6 | - ## Security If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/command/security/policy) for more information. From 1eafc0c32fd8b2ba45f76a5ed1b3ad5e625bb91c Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 9 Jun 2026 01:42:42 +0100 Subject: [PATCH 3/5] Refine focused documentation pages --- README.md | 12 +- docs/async-and-concurrency.md | 46 +++++ docs/executing-commands.md | 45 +++++ docs/index.md | 323 ---------------------------------- docs/middleware.md | 30 ++++ docs/service-clients.md | 52 ++++++ 6 files changed, 178 insertions(+), 330 deletions(-) create mode 100644 docs/async-and-concurrency.md create mode 100644 docs/executing-commands.md delete mode 100644 docs/index.md create mode 100644 docs/middleware.md create mode 100644 docs/service-clients.md diff --git a/README.md b/README.md index de8351d..b077594 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,11 @@ The service client can also execute commands asynchronously and run many command ## Documentation -- [Full documentation](docs/index.md) -- [Service clients](docs/index.md#service-clients) -- [Executing commands](docs/index.md#executing-commands) -- [Asynchronous commands](docs/index.md#asynchronous-commands) -- [Concurrent requests](docs/index.md#concurrent-requests) -- [Middleware](docs/index.md#middleware-extending-the-client) -- [Upgrade guide](UPGRADING.md) +- [Service Clients](docs/service-clients.md) +- [Executing Commands](docs/executing-commands.md) +- [Async and Concurrency](docs/async-and-concurrency.md) +- [Middleware](docs/middleware.md) +- [Upgrade Guide](UPGRADING.md) ## Security diff --git a/docs/async-and-concurrency.md b/docs/async-and-concurrency.md new file mode 100644 index 0000000..35413ae --- /dev/null +++ b/docs/async-and-concurrency.md @@ -0,0 +1,46 @@ +# Async and Concurrency + +Commands can be executed asynchronously and in batches with a fixed concurrency limit. + +## Asynchronous Commands + +Use `executeAsync()` to execute a command asynchronously. It returns a `GuzzleHttp\Promise\PromiseInterface`. + +```php +$command = $client->getCommand('createUser', ['name' => 'Ada']); +$promise = $client->executeAsync($command); + +$result = $promise->wait(); +``` + +Magic methods may also be used asynchronously by appending `Async` to the operation name. + +```php +$promise = $client->createUserAsync(['name' => 'Ada']); +$result = $promise->wait(); +``` + +If built-in execution fails, the promise is typically rejected with a `GuzzleHttp\Command\Exception\CommandException`. Custom middleware and handlers may reject with other values. + +## Concurrent Requests + +Use `executeAll()` or `executeAllAsync()` to execute multiple commands with a fixed concurrency limit. + +```php +$commands = [ + 'first' => $client->getCommand('createUser', ['name' => 'Ada']), + 'second' => $client->getCommand('createUser', ['name' => 'Grace']), +]; + +$results = $client->executeAll($commands, [ + 'concurrency' => 10, +]); +``` + +The supported options are: + +- `concurrency`: maximum number of commands to execute at the same time. The default is `25`. +- `fulfilled`: callable invoked when an individual command succeeds. +- `rejected`: callable invoked when an individual command fails. + +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. diff --git a/docs/executing-commands.md b/docs/executing-commands.md new file mode 100644 index 0000000..b8ec308 --- /dev/null +++ b/docs/executing-commands.md @@ -0,0 +1,45 @@ +# Executing Commands + +Service clients create command objects with `getCommand()` and execute them with `execute()`. + +```php +$command = $client->getCommand('createUser', ['name' => 'Ada']); +$result = $client->execute($command); +``` + +Result objects implement `GuzzleHttp\Command\ResultInterface`, are `ArrayAccess`-ible, and contain data parsed from the HTTP response. + +## Magic Methods + +Service clients have magic methods that create and execute commands by name. + +```php +$result = $client->createUser(['name' => 'Ada']); +``` + +## Per-Command HTTP Options + +`GuzzleHttp\Command\ServiceClient` reserves the `@http` command parameter for per-command Guzzle request options. During execution, the service client removes `@http` from the command and passes the array to the underlying Guzzle HTTP client. + +```php +use GuzzleHttp\RequestOptions; + +$command = $client->getCommand('createUser', [ + 'name' => 'Ada', + '@http' => [ + RequestOptions::TIMEOUT => 2.0, + ], +]); + +$result = $client->execute($command); +``` + +Treat `@http` as a reserved control key, not as an operation parameter. Do not pass untrusted input directly into command arguments without filtering reserved keys first. + +```php +if (array_key_exists('@http', $input)) { + throw new InvalidArgumentException('"@http" is reserved.'); +} +``` + +Be especially careful with request options that affect the target URI, proxy, TLS verification, headers, body, response sink, redirects, or timeouts. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 9d1cd7f..0000000 --- a/docs/index.md +++ /dev/null @@ -1,323 +0,0 @@ -# Guzzle Commands Documentation - -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. - -### Commands - -Key-value pair objects representing an operation of a web service. Commands -have a name and a set of parameters. - -### Results - -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/): - -``` -composer require guzzlehttp/command -``` - -## Version Guidance - -| Version | Status | PHP Version | -|---------|--------------|--------------| -| 2.x | Experimental | >=7.4,<8.6 | -| 1.x | Latest | >=7.2.5,<8.6 | - -See [UPGRADING.md](../UPGRADING.md) for upgrade notes. - -## 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\UriTemplate\UriTemplate; -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', - UriTemplate::expand('/{command}', ['command' => $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) - ); - } -); -``` - -## 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(); -``` - -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 - -Use `executeAll()` or `executeAllAsync()` to execute multiple commands with a -fixed concurrency limit. Both methods accept an array or iterator that yields -`CommandInterface` objects. - -`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); - }; -}); -``` - -## Security - -If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/command/security/policy) for more information. - -## License - -Guzzle is made available under the MIT License (MIT). Please see [License File](../LICENSE) for more information. - -## For Enterprise - -Available as part of the Tidelift Subscription - -The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-command?utm_source=packagist-guzzlehttp-command&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) diff --git a/docs/middleware.md b/docs/middleware.md new file mode 100644 index 0000000..d7c7b1f --- /dev/null +++ b/docs/middleware.md @@ -0,0 +1,30 @@ +# Middleware + +Middleware can be added to the service client or underlying HTTP client to customize different parts of the lifecycle. + +Command middleware wraps commands before they are transformed into HTTP requests. HTTP middleware should be configured on the underlying Guzzle HTTP client instead. + +Command handlers use this shape: + +```php +callable(GuzzleHttp\Command\CommandInterface): GuzzleHttp\Promise\PromiseInterface +``` + +## 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); + }; +}); +``` + +Use command middleware for behavior tied to service operations. Use Guzzle HTTP middleware for behavior tied to PSR-7 requests and responses. diff --git a/docs/service-clients.md b/docs/service-clients.md new file mode 100644 index 0000000..d09f0de --- /dev/null +++ b/docs/service-clients.md @@ -0,0 +1,52 @@ +# Service Clients + +Service clients are web service clients that implement `GuzzleHttp\Command\ServiceClientInterface` and use an underlying Guzzle HTTP client to communicate with a service. + +A service client turns commands into PSR-7 requests, sends them through Guzzle, and turns responses into result objects. + +## Concepts + +- A command is a key-value object representing one service operation. +- A result is a key-value object representing the processed response from an operation. +- Command middleware wraps commands before they are converted into HTTP requests. +- HTTP middleware belongs on the underlying Guzzle HTTP client. + +## Creating a Service Client + +`GuzzleHttp\Command\ServiceClient` accepts: + +- a configured `GuzzleHttp\ClientInterface` +- a callable that converts a command into a PSR-7 request +- a callable that converts a response into a result +- optionally, a command handler stack + +```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()), + ['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)); + } +); +``` From 150d79e34c85667f3d86b3fbbe6e8236f5e4a8c9 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 9 Jun 2026 01:49:07 +0100 Subject: [PATCH 4/5] Restore detailed focused docs --- docs/async-and-concurrency.md | 91 +++++++++++++++++++++++++++++------ docs/executing-commands.md | 72 +++++++++++++++++++-------- docs/middleware.md | 20 +++----- docs/service-clients.md | 59 +++++++++++++++++------ 4 files changed, 180 insertions(+), 62 deletions(-) diff --git a/docs/async-and-concurrency.md b/docs/async-and-concurrency.md index 35413ae..168a5c2 100644 --- a/docs/async-and-concurrency.md +++ b/docs/async-and-concurrency.md @@ -1,46 +1,109 @@ # Async and Concurrency -Commands can be executed asynchronously and in batches with a fixed concurrency limit. - ## Asynchronous Commands -Use `executeAsync()` to execute a command asynchronously. It returns a `GuzzleHttp\Promise\PromiseInterface`. +Commands can be executed asynchronously using `executeAsync()`. This method +returns a `GuzzleHttp\Promise\PromiseInterface`. ```php -$command = $client->getCommand('createUser', ['name' => 'Ada']); +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. +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->createUserAsync(['name' => 'Ada']); +$promise = $client->fooAsync(['baz' => 'bar']); $result = $promise->wait(); ``` -If built-in execution fails, the promise is typically rejected with a `GuzzleHttp\Command\Exception\CommandException`. Custom middleware and handlers may reject with other values. +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 -Use `executeAll()` or `executeAllAsync()` to execute multiple commands with a fixed 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. + +`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('createUser', ['name' => 'Ada']), - 'second' => $client->getCommand('createUser', ['name' => 'Grace']), + '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 when an individual command succeeds. -- `rejected`: callable invoked when an individual command fails. +* `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. +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. diff --git a/docs/executing-commands.md b/docs/executing-commands.md index b8ec308..395dd9d 100644 --- a/docs/executing-commands.md +++ b/docs/executing-commands.md @@ -1,32 +1,73 @@ # Executing Commands -Service clients create command objects with `getCommand()` and execute them with `execute()`. +Service clients create command objects using the ``getCommand()`` method. ```php -$command = $client->getCommand('createUser', ['name' => 'Ada']); -$result = $client->execute($command); +$commandName = 'foo'; +$arguments = ['baz' => 'bar']; +$command = $client->getCommand($commandName, $arguments); ``` -Result objects implement `GuzzleHttp\Command\ResultInterface`, are `ArrayAccess`-ible, and contain data parsed from the HTTP response. +After creating a command, you may execute the command using the `execute()` +method of the client. + +```php +$result = $client->execute($command); +``` -## Magic Methods +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 create and execute commands by name. +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->createUser(['name' => 'Ada']); +$result = $client->foo(['baz' => 'bar']); ``` ## Per-Command HTTP Options -`GuzzleHttp\Command\ServiceClient` reserves the `@http` command parameter for per-command Guzzle request options. During execution, the service client removes `@http` from the command and passes the array to the underlying Guzzle HTTP client. +`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('createUser', [ - 'name' => 'Ada', +$command = $client->getCommand('foo', [ + 'baz' => 'bar', '@http' => [ + RequestOptions::CONNECT_TIMEOUT => 1.0, RequestOptions::TIMEOUT => 2.0, ], ]); @@ -34,12 +75,5 @@ $command = $client->getCommand('createUser', [ $result = $client->execute($command); ``` -Treat `@http` as a reserved control key, not as an operation parameter. Do not pass untrusted input directly into command arguments without filtering reserved keys first. - -```php -if (array_key_exists('@http', $input)) { - throw new InvalidArgumentException('"@http" is reserved.'); -} -``` - -Be especially careful with request options that affect the target URI, proxy, TLS verification, headers, body, response sink, redirects, or timeouts. +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. diff --git a/docs/middleware.md b/docs/middleware.md index d7c7b1f..838302a 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -1,16 +1,12 @@ -# Middleware +# Middleware: Extending the Client -Middleware can be added to the service client or underlying HTTP client to customize different parts of the lifecycle. +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 wraps commands before they are transformed into HTTP requests. HTTP middleware should be configured on the underlying Guzzle HTTP client instead. - -Command handlers use this shape: - -```php -callable(GuzzleHttp\Command\CommandInterface): GuzzleHttp\Promise\PromiseInterface -``` - -## Adding Command Middleware +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; @@ -26,5 +22,3 @@ $client->getHandlerStack()->push(function (callable $handler) { }; }); ``` - -Use command middleware for behavior tied to service operations. Use Guzzle HTTP middleware for behavior tied to PSR-7 requests and responses. diff --git a/docs/service-clients.md b/docs/service-clients.md index d09f0de..1202467 100644 --- a/docs/service-clients.md +++ b/docs/service-clients.md @@ -1,24 +1,48 @@ # Service Clients -Service clients are web service clients that implement `GuzzleHttp\Command\ServiceClientInterface` and use an underlying Guzzle HTTP client to communicate with a service. +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. -A service client turns commands into PSR-7 requests, sends them through Guzzle, and turns responses into result objects. +## Commands -## Concepts +Key-value pair objects representing an operation of a web service. Commands +have a name and a set of parameters. -- A command is a key-value object representing one service operation. -- A result is a key-value object representing the processed response from an operation. -- Command middleware wraps commands before they are converted into HTTP requests. -- HTTP middleware belongs on the underlying Guzzle HTTP client. +## Results -## Creating a Service Client +Key-value pair objects representing the processed result of executing an +operation of a web service. -`GuzzleHttp\Command\ServiceClient` accepts: +## Service Clients -- a configured `GuzzleHttp\ClientInterface` -- a callable that converts a command into a PSR-7 request -- a callable that converts a response into a result -- optionally, a command handler stack +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; @@ -27,6 +51,7 @@ 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; @@ -36,8 +61,8 @@ $client = new ServiceClient( function (CommandInterface $command): RequestInterface { return new Request( 'POST', - '/' . rawurlencode($command->getName()), - ['Content-Type' => 'application/json'], + UriTemplate::expand('/{command}', ['command' => $command->getName()]), + ['Accept' => 'application/json', 'Content-Type' => 'application/json'], Utils::jsonEncode($command->toArray()) ); }, @@ -46,7 +71,9 @@ $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) + ); } ); ``` From 22e38a8543c1749896cf154c0969e27c4e5bd26b Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 9 Jun 2026 11:50:13 +0100 Subject: [PATCH 5/5] Refine documentation navigation --- README.md | 15 ++-- docs/async-and-concurrency.md | 46 +++++++--- docs/executing-commands.md | 72 +++++++++++++-- docs/middleware-extending-the-client.md | 60 +++++++++++++ docs/middleware.md | 24 ----- docs/service-clients.md | 114 ++++++++++++++++++++---- 6 files changed, 264 insertions(+), 67 deletions(-) create mode 100644 docs/middleware-extending-the-client.md delete mode 100644 docs/middleware.md diff --git a/README.md b/README.md index b077594..ba23559 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -# Guzzle Commands +# Guzzle Command -`guzzlehttp/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. +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. -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) instead. +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. + +For declarative service descriptions that define operations from API metadata, see [Guzzle Services](https://github.com/guzzle/guzzle-services/blob/2.0/README.md). ## Installation @@ -52,15 +54,16 @@ $client = new ServiceClient( $result = $client->createUser(['name' => 'Ada']); ``` -The service client can also execute commands asynchronously and run many commands with a fixed concurrency limit. +The service client can also execute commands asynchronously and run many commands with a configurable concurrency limit. ## Documentation - [Service Clients](docs/service-clients.md) - [Executing Commands](docs/executing-commands.md) - [Async and Concurrency](docs/async-and-concurrency.md) -- [Middleware](docs/middleware.md) +- [Middleware: Extending the Client](docs/middleware-extending-the-client.md) - [Upgrade Guide](UPGRADING.md) +- [Changelog](CHANGELOG.md) ## Security @@ -68,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 index 168a5c2..1da811b 100644 --- a/docs/async-and-concurrency.md +++ b/docs/async-and-concurrency.md @@ -1,9 +1,13 @@ # 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; @@ -40,18 +44,21 @@ If built-in execution fails, the promise is typically rejected with a `CommandServerException`, respectively, when the underlying Guzzle exception contains a response. Custom middleware and handlers may reject with other values. -## Concurrent Requests +## Concurrent Commands Use `executeAll()` or `executeAllAsync()` to execute multiple commands with a -fixed concurrency limit. Both methods accept an array or iterator that yields -`CommandInterface` objects. +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 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. +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; @@ -73,8 +80,11 @@ $results = $client->executeAll($commands, [ ``` `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: +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; @@ -95,15 +105,23 @@ $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()` +- `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()` +- `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 index 395dd9d..9638631 100644 --- a/docs/executing-commands.md +++ b/docs/executing-commands.md @@ -1,6 +1,10 @@ # Executing Commands -Service clients create command objects using the ``getCommand()`` method. +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'; @@ -16,17 +20,62 @@ $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. +`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. +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 @@ -41,9 +90,10 @@ 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. +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: @@ -77,3 +127,9 @@ $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/middleware.md b/docs/middleware.md deleted file mode 100644 index 838302a..0000000 --- a/docs/middleware.md +++ /dev/null @@ -1,24 +0,0 @@ -# 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); - }; -}); -``` diff --git a/docs/service-clients.md b/docs/service-clients.md index 1202467..a36826c 100644 --- a/docs/service-clients.md +++ b/docs/service-clients.md @@ -1,28 +1,44 @@ # Service Clients -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 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 -Key-value pair objects representing an operation of a web service. Commands -have a name and a set of parameters. +Commands are key-value pair objects representing a single operation of a web +service. Commands have a name and a set of parameters. ## Results -Key-value pair objects representing the processed result of executing an -operation of a web service. +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`), +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. @@ -35,9 +51,9 @@ 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 +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 +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. @@ -51,7 +67,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; @@ -61,7 +76,7 @@ $client = new ServiceClient( function (CommandInterface $command): RequestInterface { return new Request( 'POST', - UriTemplate::expand('/{command}', ['command' => $command->getName()]), + '/' . rawurlencode($command->getName()), ['Accept' => 'application/json', 'Content-Type' => 'application/json'], Utils::jsonEncode($command->toArray()) ); @@ -77,3 +92,72 @@ $client = new ServiceClient( } ); ``` + +## 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)