Skip to content

Commit

Permalink
Merge pull request #51 from php-http/plugins
Browse files Browse the repository at this point in the history
Update plugin documentation
  • Loading branch information
sagikazarmark committed Dec 29, 2015
2 parents 20259ae + 56b7e8a commit 8bfe7a5
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 157 deletions.
203 changes: 131 additions & 72 deletions docs/httplug/plugins.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,184 @@
# Plugin System
# HTTPlug Plugins

The plugin system allows to look at requests and responses and replace them if needed, inside an `HttpClient`.

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.
The plugin system allows to wrap a Client and add some processing logic prior to and/or after sending the actual
request or you can even start a completely new request. This gives you full control over what happens in your workflow.


## Install

Install the plugin client in your project with composer:
Install the plugin client in your project with [Composer](https://getcomposer.org/):

``` bash
composer require "php-http/plugins:^1.0"
$ composer require php-http/plugins
```


## Usage
## How it works

In the plugin package, you can find the following content:

- the Plugin Client itself which acts as a wrapper around any kind of HTTP Client (sync/async)
- a Plugin interface
- a set of core plugins (see the full list in the left side navigation)

The Plugin Client accepts an HTTP Client implementation and an array of plugins.

First you need to have some plugins:
Let's see an example:

``` 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,
]
);
```

Then you can create a `PluginClient`:
The Plugin Client accepts and implements both `Http\Client\HttpClient` and `Http\Client\HttpAsyncClient`, so you can use
both ways to send a request. In case the passed client implements only one of these interfaces, the Plugin Client
"emulates" the other behavior as a fallback.

``` php
use Http\Discovery\HttpClientDiscovery;
use Http\Client\Plugin\PluginClient;
It is important, that the order of plugins matters. During the request, plugins are called in the order they have
been added, from first to last. Once a response has been received, they are called again in reversed order,
from last to first.

...
In case of our previous example, the execution chain will look like this:

$pluginClient = new PluginClient(HttpClientDiscovery::find(), [
$retryPlugin,
$redirectPlugin
]);
```
Request ---> PluginClient ---> RetryPlugin ---> RedirectPlugin ---> HttpClient ----
| (processing call)
Response <--- PluginClient <--- RetryPlugin <--- RedirectPlugin <--- HttpClient <---
```

You can use the plugin client like a classic `Http\Client\HttpClient` or `Http\Client\HttpAsyncClient` one:
In order to have correct behavior over the global process, you need to understand well how each plugin is used,
and manage a correct order when passing the array to the Plugin Client.

``` php
// Send a request
$response = $pluginClient->sendRequest($request);
Retry Plugin 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.

// Send an asynchronous request
$promise = $pluginClient->sendAsyncRequest($request);
```
The recommended way to order plugins is the following:

1. Plugins that modify the request should be at the beginning (like Authentication or Cookie Plugin)
2. Plugins which intervene in the workflow should be in the "middle" (like Retry or Redirect Plugin)
3. Plugins which log information should be last (like Logger or History Plugin)

!!! note "Note:"
There can be exceptions to these rules. For example,
for security reasons you might not want to log the authentication information (like `Authorization` header)
and choose to put the Authentication Plugin after the Logger Plugin.

Go to the [tutorial](tutorial.md) to read more about using `HttpClient` and `HttpAsyncClient`

## Implement your own

## Available plugins
When writing your own Plugin, you need to be aware that the Plugin Client is async first.
This means that every plugin must be written with Promises. More about this later.

Each plugin has its own configuration and dependencies, check the documentation for each of the available plugins:
Each plugin must implement the `Http\Client\Plugin\Plugin` interface.

- [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)
This interface defines the `handleRequest` method that allows to modify behavior of the call:

```php
/**
* Handles the request and returns 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);
```

## Order of plugins
The `$request` comes from an upstream plugin or Plugin Client itself.
You can replace it and pass a new version downstream if you need.

When you inject an array of plugins into the `PluginClient`, the order of the plugins matters.
!!! note "Note:"
Be aware that the request is immutable.

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:
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.

```php
use Http\Discovery\HttpClientDiscovery;
use Http\Client\Plugin\PluginClient;
use Http\Client\Plugin\RetryPlugin;
use Http\Client\Plugin\RedirectPlugin;
For example a simple plugin setting a header would look like this:

$retryPlugin = new RetryPlugin();
$redirectPlugin = new RedirectPlugin();
``` php
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$newRequest = $request->withHeader('MyHeader', 'MyValue');

$pluginClient = new PluginClient(HttpClientDiscovery::find(), [
$retryPlugin,
$redirectPlugin
]);
return $next($newRequest);
}
```

The execution chain will look like this:
The `$first` callable is the first plugin in the chain. It allows you to completely reboot the execution chain, or send
another request if needed, while still going through all the defined plugins.
Like in case of the `$next` callable, you must pass the `$request` as the first argument.

```
Request ---> PluginClient ---> RetryPlugin ---> RedirectPlugin ---> HttpClient ----
| (processing call)
Response <--- PluginClient <--- RetryPlugin <--- RedirectPlugin <--- HttpClient <---
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
if ($someCondition) {
$newRequest = new Request();
$promise = $first($newRequest);
// Use the promise to do some jobs ...
}
return $next($request);
}
```

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`
!!! warning "Warning:"
In this example the condition is not superfluous:
you need to have some way to not call the `$first` callable each time
or you will end up in an infinite execution loop.

`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 `$next` and `$first` callables will return a Promise (defined in `php-http/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;
});
}
```

!!! warning "Warning:"
Contract for the `Http\Promise\Promise` is temporary until a
[PSR is released](https://groups.google.com/forum/?fromgroups#!topic/php-fig/wzQWpLvNSjs).
Once it is out, we will use this PSR in HTTPlug and deprecate the old contract.

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`)
To better understand the whole process check existing implementations in the
[plugin repository](https://github.com/php-http/plugins).

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.

## Contribution

## Implementing your own Plugin
We are always open to contributions. Either in form of Pull Requests to the core package or self-made plugin packages.
We encourage everyone to prefer sending Pull Requests, however we don't promise that every plugin gets
merged into the core. If this is the case, it is not because we think your work is not good enough. We try to keep
the core as small as possible with the most widely used plugin implementations.

Read this [documentation](plugins/plugin-implementation.md) if you want to create your own Plugin.
Even if we think that a plugin is not suitable for the core, we want to help you sharing your work with the community.
You can always open a Pull Request to place a link and a small description of your plugin on the
[Third Party Plugins](plugins/third-party-plugins.md) page. In special cases,
we might offer you to host your package under the PHP HTTP namespace.
18 changes: 12 additions & 6 deletions docs/httplug/plugins/authentication.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
# Authentication Plugin

This plugin uses the [authentication component](/components/authentication) to authenticate requests sent through
the client.
This plugin uses the [authentication component](http://docs.httplug.io/en/latest/components/authentication/)
from `php-http/message` to authenticate requests sent through the client.


## Usage

``` php
use Http\Discovery\HttpClientDiscovery;
use Http\Message\Authentication\BasicAuth;
use Http\Plugins\PluginClient;
use Http\Plugins\AuthenticationPlugin;

$pluginClient = new PluginClient(new HttpClient(), [new AuthenticationPlugin(new AuthenticationMethod()]);
$authentication = new BasicAuth('username', 'password');
$authenticationPlugin = new AuthenticationPlugin($authentication);

$pluginClient = new PluginClient(
HttpClientDiscovery::find(),
[$authenticationPlugin]
);
```

Check the [authentication component documentation](/components/authentication)
Check the [authentication component documentation](http://docs.httplug.io/en/latest/components/authentication/)
for the list of available authentication methods.
79 changes: 0 additions & 79 deletions docs/httplug/plugins/plugin-implementation.md

This file was deleted.

1 change: 1 addition & 0 deletions docs/httplug/plugins/third-party-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Third Party Plugins

0 comments on commit 8bfe7a5

Please sign in to comment.