Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin documentation #30

Merged
merged 1 commit into from Nov 12, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
117 changes: 115 additions & 2 deletions 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we not say the client always throws the exception in the sync mode? and it can't throw exceptions in async. i am not sure this plugin would make sense.

i just looked at HttpClient.php and we just say @throws Exception without further specifying when the exception would happen. but BatchClient for example seems to assume exceptions being thrown.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception in this case is more when connection or transfer, parsing failed. This plugin transforms response who was correctly received but has a status code between 400 and 599 (the status code list where it throws exception can be changed by configuring the plugin)

it correspond to this plugin in the plugin list : " Convert error responses to exceptions (throw exception when status is 4xx / 5xx)"

And i didn't talk about the throw part as it's depend if your are in sync or async mode.

- [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.
3 changes: 3 additions & 0 deletions docs/plugins/authentication.md
@@ -0,0 +1,3 @@
# Authentication Plugin

TODO: explain the authentication plugin
3 changes: 3 additions & 0 deletions docs/plugins/cookie.md
@@ -0,0 +1,3 @@
# Cookie Plugin

TODO: explain the cookie plugin
3 changes: 3 additions & 0 deletions docs/plugins/encoding.md
@@ -0,0 +1,3 @@
# Encoding Plugin

TODO: explain the encoding plugin
3 changes: 3 additions & 0 deletions docs/plugins/error.md
@@ -0,0 +1,3 @@
# Error Plugin

TODO: explain the error plugin
79 changes: 79 additions & 0 deletions 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.
3 changes: 3 additions & 0 deletions docs/plugins/redirect.md
@@ -0,0 +1,3 @@
# Redirect Plugin

TODO: explain the redirect plugin
3 changes: 3 additions & 0 deletions docs/plugins/retry.md
@@ -0,0 +1,3 @@
# Retry Plugin

TODO: explain the retry plugin
3 changes: 3 additions & 0 deletions docs/plugins/stopwatch.md
@@ -0,0 +1,3 @@
# Stopwatch Plugin

TODO: explain the stopwatch plugin