diff --git a/docs/plugins.md b/docs/plugins.md index d5da2de..1b7f24f 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -1,5 +1,118 @@ # Plugin System -The plugin system will allow to manipulate requests and responses inside a `HttpClient`. +The plugin system allows to look at requests and responses and replace them if needed, inside an `HttpClient`. -TODO: finalize system and document. +Using the `Http\Client\Plugin\PluginClient`, you can inject an `HttpClient`, or an `HttpAsyncClient`, and an array +of plugins implementing the `Http\Client\Plugin\Plugin` interface. + +Each plugin can replace the `RequestInterface` sent or the `ResponseInterface` received. It can also change the behavior of a call, +like retrying the request or emit another one when a redirection response was received. + +## Install + +Install the plugin client in your project with composer: + +``` bash +composer require "php-http/plugins:^1.0" +``` + +## Usage + +First you need to have some plugins: + +```php +use Http\Client\Plugin\RetryPlugin; +use Http\Client\Plugin\RedirectPlugin; + +$retryPlugin = new RetryPlugin(); +$redirectPlugin = new RedirectPlugin(); +``` + +Then you can create a `PluginClient`: + +```php +use Http\Discovery\HttpClientDiscovery; +use Http\Client\Plugin\PluginClient; + +... + +$pluginClient = new PluginClient(HttpClientDiscovery::find(), [ + $retryPlugin, + $redirectPlugin +]); +``` + +You can use the plugin client like a classic `Http\Client\HttpClient` or `Http\Client\HttpAsyncClient` one: + +```php +// Send a request +$response = $pluginClient->sendRequest($request); + +// Send an asynchronous request +$promise = $pluginClient->sendAsyncRequest($request); +``` + +Go to the [tutorial](tutorial.md) to read more about using `HttpClient` and `HttpAsyncClient` + +## Available plugins + +Each plugin has its own configuration and dependencies, check the documentation for each of the available plugins: + + - [Authentication](plugins/authentication.md): Add authentication header on a request + - [Cookie](plugins/cookie.md): Add cookies to request and save them from the response + - [Encoding](plugins/encoding.md): Add support for receiving chunked, deflate or gzip response + - [Error](plugins/error.md): Transform bad response (400 to 599) to exception + - [Redirect](plugins/redirect.md): Follow redirection coming from 3XX responses + - [Retry](plugins/retry.md): Retry a failed call + - [Stopwatch](plugins/stopwatch.md): Log time of a request call by using [the Symfony Stopwatch component](http://symfony.com/doc/current/components/stopwatch.html) + +## Order of plugins + +When you inject an array of plugins into the `PluginClient`, the order of the plugins matters. + +During the request, plugins are called in the order they have in the array, from first to last plugin. Once a response has been received, +they are called again in inverse order, from last to first. + +i.e. with the following code: + +```php +use Http\Discovery\HttpClientDiscovery; +use Http\Client\Plugin\PluginClient; +use Http\Client\Plugin\RetryPlugin; +use Http\Client\Plugin\RedirectPlugin; + +$retryPlugin = new RetryPlugin(); +$redirectPlugin = new RedirectPlugin(); + +$pluginClient = new PluginClient(HttpClientDiscovery::find(), [ + $retryPlugin, + $redirectPlugin +]); +``` + +The execution chain will look like this: + +``` +Request ---> PluginClient ---> RetryPlugin ---> RedirectPlugin ---> HttpClient ---- + | (processing call) +Response <--- PluginClient <--- RetryPlugin <--- RedirectPlugin <--- HttpClient <--- +``` + +In order to have correct behavior over the global process, you need to understand well each plugin used, +and manage a correct order when passing the array to the `PluginClient` + +`RetryPlugin` will be best at the end to optimize the retry process, but it can also be good +to have it as the first plugin, if one of the plugins is inconsistent and may need a retry. + +The recommended way to order plugins is the following rules: + + 1. Plugins that modify the request should be at the beginning (like the `AuthenticationPlugin` or the `CookiePlugin`) + 2. Plugins which intervene in the workflow should be in the "middle" (like the `RetryPlugin` or the `RedirectPlugin`) + 3. Plugins which log information should be last (like the `LoggerPlugin` or the `HistoryPlugin`) + +However, there can be exceptions to these rules. For example, for security reasons you might not want to log the authentication header +and chose to put the AuthenticationPlugin after the LoggerPlugin. + +## Implementing your own Plugin + +Read this [documentation](plugins/plugin-implementation.md) if you want to create your own Plugin. diff --git a/docs/plugins/authentication.md b/docs/plugins/authentication.md new file mode 100644 index 0000000..71de379 --- /dev/null +++ b/docs/plugins/authentication.md @@ -0,0 +1,3 @@ +# Authentication Plugin + +TODO: explain the authentication plugin diff --git a/docs/plugins/cookie.md b/docs/plugins/cookie.md new file mode 100644 index 0000000..27ecd43 --- /dev/null +++ b/docs/plugins/cookie.md @@ -0,0 +1,3 @@ +# Cookie Plugin + +TODO: explain the cookie plugin diff --git a/docs/plugins/encoding.md b/docs/plugins/encoding.md new file mode 100644 index 0000000..ee03b44 --- /dev/null +++ b/docs/plugins/encoding.md @@ -0,0 +1,3 @@ +# Encoding Plugin + +TODO: explain the encoding plugin diff --git a/docs/plugins/error.md b/docs/plugins/error.md new file mode 100644 index 0000000..46bad4f --- /dev/null +++ b/docs/plugins/error.md @@ -0,0 +1,3 @@ +# Error Plugin + +TODO: explain the error plugin diff --git a/docs/plugins/plugin-implementation.md b/docs/plugins/plugin-implementation.md new file mode 100644 index 0000000..e4f238d --- /dev/null +++ b/docs/plugins/plugin-implementation.md @@ -0,0 +1,79 @@ +# Implementing your own Plugin + +When writing your own Plugin, you need to be aware that the `PluginClient` is async first. This means that every plugin must +be written by respecting the `HttpAsyncClient` contract and returns a `Promise`. + +Each plugin must implement the `Http\Client\Plugin\Plugin` interface. + +This interface defines the `handleRequest` method that allows to modify behavior of the call: + +```php +/** + * handle the request and return the response coming from the next callable + * + * @param RequestInterface $request Request to use + * @param callable $next Callback to call to have the request, it muse have the request as it first argument + * @param callable $first First element in the plugin chain, used to to restart a request from the beginning + * + * @return Promise + */ +public function handleRequest(RequestInterface $request, callable $next, callable $first); +``` + +The `$request` comes from upstream. You can replace it and pass a new version downstream if you need. Always be aware that +the request is immutable. + +The `$next` callable is the next plugin in the execution chain. When you need to call it, you must pass the `$request` +as the first argument of this callable. + +``` +public function handleRequest(RequestInterface $request, callable $next, callable $first) +{ + $newRequest = $request->withHeader('MyHeader', 'MyValue'); + + return $next($newRequest); +} +``` + + +The `$first` callable is the first plugin in the execution. It allows you to completely reboot the execution chain, or send +other request if needed, while still going through all the defined plugins. Like the `$next` callable, you must pass the `$request` +as the first argument of this callable. + +``` +public function handleRequest(RequestInterface $request, callable $next, callable $first) +{ + if ($someCondition) { + $newRequest = new Request(); + $promise = $first($newRequest); + + // Use the promise do some jobs ... + } + + return $next($request); +} +``` + +In this example the condition is not superfluous, you need to have some way to not calling the $first callable each time or +you will end up with a infinite execution loop. + +The `$next` and `$first` callable will return a `Promise`. You can manipulate the `ResponseInterface` or the `Exception` +by using the `then` method of the promise. + +``` +public function handleRequest(RequestInterface $request, callable $next, callable $first) +{ + $newRequest = $request->withHeader('MyHeader', 'MyValue'); + + return $next($request)->then(function (ResponseInterface $response) { + return $response->withHeader('MyResponseHeader', 'value'); + }, function (Exception $exception) { + echo $exception->getMessage(); + + throw $exception; + }); +} +``` + +Anyway it is always a good practice to read existing implementation inside the [plugin repository](https://github.com/php-http/plugins) to better understand the whole +process. diff --git a/docs/plugins/redirect.md b/docs/plugins/redirect.md new file mode 100644 index 0000000..6efe20f --- /dev/null +++ b/docs/plugins/redirect.md @@ -0,0 +1,3 @@ +# Redirect Plugin + +TODO: explain the redirect plugin diff --git a/docs/plugins/retry.md b/docs/plugins/retry.md new file mode 100644 index 0000000..2a5edd8 --- /dev/null +++ b/docs/plugins/retry.md @@ -0,0 +1,3 @@ +# Retry Plugin + +TODO: explain the retry plugin diff --git a/docs/plugins/stopwatch.md b/docs/plugins/stopwatch.md new file mode 100644 index 0000000..bc364ad --- /dev/null +++ b/docs/plugins/stopwatch.md @@ -0,0 +1,3 @@ +# Stopwatch Plugin + +TODO: explain the stopwatch plugin