|
1 |
| -# Plugin System |
| 1 | +# HTTPlug Plugins |
2 | 2 |
|
3 |
| -The plugin system allows to look at requests and responses and replace them if needed, inside an `HttpClient`. |
4 |
| - |
5 |
| -Using the `Http\Client\Plugin\PluginClient`, you can inject an `HttpClient`, or an `HttpAsyncClient`, and an array |
6 |
| -of plugins implementing the `Http\Client\Plugin\Plugin` interface. |
7 |
| - |
8 |
| -Each plugin can replace the `RequestInterface` sent or the `ResponseInterface` received. |
9 |
| -It can also change the behavior of a call, |
10 |
| -like retrying the request or emit another one when a redirection response was received. |
| 3 | +The plugin system allows to wrap a Client and add some processing logic prior to and/or after sending the actual |
| 4 | +request or you can even start a completely new request. This gives you full control over what happens in your workflow. |
11 | 5 |
|
12 | 6 |
|
13 | 7 | ## Install
|
14 | 8 |
|
15 |
| -Install the plugin client in your project with composer: |
| 9 | +Install the plugin client in your project with [Composer](https://getcomposer.org/): |
16 | 10 |
|
17 | 11 | ``` bash
|
18 |
| -composer require "php-http/plugins:^1.0" |
| 12 | +$ composer require php-http/plugins |
19 | 13 | ```
|
20 | 14 |
|
21 | 15 |
|
22 |
| -## Usage |
| 16 | +## How it works |
| 17 | + |
| 18 | +In the plugin package, you can find the following content: |
| 19 | + |
| 20 | +- the Plugin Client itself which acts as a wrapper around any kind of HTTP Client (sync/async) |
| 21 | +- a Plugin interface |
| 22 | +- a set of core plugins (see the full list in the left side navigation) |
| 23 | + |
| 24 | +The Plugin Client accepts an HTTP Client implementation and an array of plugins. |
23 | 25 |
|
24 |
| -First you need to have some plugins: |
| 26 | +Let's see an example: |
25 | 27 |
|
26 | 28 | ``` php
|
| 29 | +use Http\Discovery\HttpClientDiscovery; |
| 30 | +use Http\Client\Plugin\PluginClient; |
27 | 31 | use Http\Client\Plugin\RetryPlugin;
|
28 | 32 | use Http\Client\Plugin\RedirectPlugin;
|
29 | 33 |
|
30 | 34 | $retryPlugin = new RetryPlugin();
|
31 | 35 | $redirectPlugin = new RedirectPlugin();
|
| 36 | + |
| 37 | +$pluginClient = new PluginClient( |
| 38 | + HttpClientDiscovery::find(), |
| 39 | + [ |
| 40 | + $retryPlugin, |
| 41 | + $redirectPlugin, |
| 42 | + ] |
| 43 | +); |
32 | 44 | ```
|
33 | 45 |
|
34 |
| -Then you can create a `PluginClient`: |
| 46 | +The Plugin Client accepts and implements both `Http\Client\HttpClient` and `Http\Client\HttpAsyncClient`, so you can use |
| 47 | +both ways to send a request. In case the passed client implements only one of these interfaces, the Plugin Client |
| 48 | +"emulates" the other behavior as a fallback. |
35 | 49 |
|
36 |
| -``` php |
37 |
| -use Http\Discovery\HttpClientDiscovery; |
38 |
| -use Http\Client\Plugin\PluginClient; |
| 50 | +It is important, that the order of plugins matters. During the request, plugins are called in the order they have |
| 51 | +been added, from first to last. Once a response has been received, they are called again in reversed order, |
| 52 | +from last to first. |
39 | 53 |
|
40 |
| -... |
| 54 | +In case of our previous example, the execution chain will look like this: |
41 | 55 |
|
42 |
| -$pluginClient = new PluginClient(HttpClientDiscovery::find(), [ |
43 |
| - $retryPlugin, |
44 |
| - $redirectPlugin |
45 |
| -]); |
| 56 | +``` |
| 57 | +Request ---> PluginClient ---> RetryPlugin ---> RedirectPlugin ---> HttpClient ---- |
| 58 | + | (processing call) |
| 59 | +Response <--- PluginClient <--- RetryPlugin <--- RedirectPlugin <--- HttpClient <--- |
46 | 60 | ```
|
47 | 61 |
|
48 |
| -You can use the plugin client like a classic `Http\Client\HttpClient` or `Http\Client\HttpAsyncClient` one: |
| 62 | +In order to have correct behavior over the global process, you need to understand well how each plugin is used, |
| 63 | +and manage a correct order when passing the array to the Plugin Client. |
49 | 64 |
|
50 |
| -``` php |
51 |
| -// Send a request |
52 |
| -$response = $pluginClient->sendRequest($request); |
| 65 | +Retry Plugin will be best at the end to optimize the retry process, but it can also be good |
| 66 | +to have it as the first plugin, if one of the plugins is inconsistent and may need a retry. |
53 | 67 |
|
54 |
| -// Send an asynchronous request |
55 |
| -$promise = $pluginClient->sendAsyncRequest($request); |
56 |
| -``` |
| 68 | +The recommended way to order plugins is the following: |
| 69 | + |
| 70 | + 1. Plugins that modify the request should be at the beginning (like Authentication or Cookie Plugin) |
| 71 | + 2. Plugins which intervene in the workflow should be in the "middle" (like Retry or Redirect Plugin) |
| 72 | + 3. Plugins which log information should be last (like Logger or History Plugin) |
| 73 | + |
| 74 | +!!! note "Note:" |
| 75 | + There can be exceptions to these rules. For example, |
| 76 | + for security reasons you might not want to log the authentication information (like `Authorization` header) |
| 77 | + and choose to put the Authentication Plugin after the Logger Plugin. |
57 | 78 |
|
58 |
| -Go to the [tutorial](tutorial.md) to read more about using `HttpClient` and `HttpAsyncClient` |
59 | 79 |
|
| 80 | +## Implement your own |
60 | 81 |
|
61 |
| -## Available plugins |
| 82 | +When writing your own Plugin, you need to be aware that the Plugin Client is async first. |
| 83 | +This means that every plugin must be written with Promises. More about this later. |
62 | 84 |
|
63 |
| -Each plugin has its own configuration and dependencies, check the documentation for each of the available plugins: |
| 85 | +Each plugin must implement the `Http\Client\Plugin\Plugin` interface. |
64 | 86 |
|
65 |
| - - [Authentication](plugins/authentication.md): Add authentication header on a request |
66 |
| - - [Cookie](plugins/cookie.md): Add cookies to request and save them from the response |
67 |
| - - [Encoding](plugins/encoding.md): Add support for receiving chunked, deflate or gzip response |
68 |
| - - [Error](plugins/error.md): Transform bad response (400 to 599) to exception |
69 |
| - - [Redirect](plugins/redirect.md): Follow redirection coming from 3XX responses |
70 |
| - - [Retry](plugins/retry.md): Retry a failed call |
71 |
| - - [Stopwatch](plugins/stopwatch.md): Log time of a request call by using [the Symfony Stopwatch component](http://symfony.com/doc/current/components/stopwatch.html) |
| 87 | +This interface defines the `handleRequest` method that allows to modify behavior of the call: |
72 | 88 |
|
| 89 | +```php |
| 90 | +/** |
| 91 | + * Handles the request and returns the response coming from the next callable. |
| 92 | + * |
| 93 | + * @param RequestInterface $request Request to use. |
| 94 | + * @param callable $next Callback to call to have the request, it muse have the request as it first argument. |
| 95 | + * @param callable $first First element in the plugin chain, used to to restart a request from the beginning. |
| 96 | + * |
| 97 | + * @return Promise |
| 98 | + */ |
| 99 | +public function handleRequest(RequestInterface $request, callable $next, callable $first); |
| 100 | +``` |
73 | 101 |
|
74 |
| -## Order of plugins |
| 102 | +The `$request` comes from an upstream plugin or Plugin Client itself. |
| 103 | +You can replace it and pass a new version downstream if you need. |
75 | 104 |
|
76 |
| -When you inject an array of plugins into the `PluginClient`, the order of the plugins matters. |
| 105 | +!!! note "Note:" |
| 106 | + Be aware that the request is immutable. |
77 | 107 |
|
78 |
| -During the request, plugins are called in the order they have in the array, from first to last plugin. |
79 |
| -Once a response has been received, they are called again in inverse order, from last to first. |
80 | 108 |
|
81 |
| -i.e. with the following code: |
| 109 | +The `$next` callable is the next plugin in the execution chain. When you need to call it, you must pass the `$request` |
| 110 | +as the first argument of this callable. |
82 | 111 |
|
83 |
| -```php |
84 |
| -use Http\Discovery\HttpClientDiscovery; |
85 |
| -use Http\Client\Plugin\PluginClient; |
86 |
| -use Http\Client\Plugin\RetryPlugin; |
87 |
| -use Http\Client\Plugin\RedirectPlugin; |
| 112 | +For example a simple plugin setting a header would look like this: |
88 | 113 |
|
89 |
| -$retryPlugin = new RetryPlugin(); |
90 |
| -$redirectPlugin = new RedirectPlugin(); |
| 114 | +``` php |
| 115 | +public function handleRequest(RequestInterface $request, callable $next, callable $first) |
| 116 | +{ |
| 117 | + $newRequest = $request->withHeader('MyHeader', 'MyValue'); |
91 | 118 |
|
92 |
| -$pluginClient = new PluginClient(HttpClientDiscovery::find(), [ |
93 |
| - $retryPlugin, |
94 |
| - $redirectPlugin |
95 |
| -]); |
| 119 | + return $next($newRequest); |
| 120 | +} |
96 | 121 | ```
|
97 | 122 |
|
98 |
| -The execution chain will look like this: |
| 123 | +The `$first` callable is the first plugin in the chain. It allows you to completely reboot the execution chain, or send |
| 124 | +another request if needed, while still going through all the defined plugins. |
| 125 | +Like in case of the `$next` callable, you must pass the `$request` as the first argument. |
99 | 126 |
|
100 | 127 | ```
|
101 |
| -Request ---> PluginClient ---> RetryPlugin ---> RedirectPlugin ---> HttpClient ---- |
102 |
| - | (processing call) |
103 |
| -Response <--- PluginClient <--- RetryPlugin <--- RedirectPlugin <--- HttpClient <--- |
| 128 | +public function handleRequest(RequestInterface $request, callable $next, callable $first) |
| 129 | +{ |
| 130 | + if ($someCondition) { |
| 131 | + $newRequest = new Request(); |
| 132 | + $promise = $first($newRequest); |
| 133 | +
|
| 134 | + // Use the promise to do some jobs ... |
| 135 | + } |
| 136 | +
|
| 137 | + return $next($request); |
| 138 | +} |
104 | 139 | ```
|
105 | 140 |
|
106 |
| -In order to have correct behavior over the global process, you need to understand well each plugin used, |
107 |
| -and manage a correct order when passing the array to the `PluginClient` |
| 141 | +!!! warning "Warning:" |
| 142 | + In this example the condition is not superfluous: |
| 143 | + you need to have some way to not call the `$first` callable each time |
| 144 | + or you will end up in an infinite execution loop. |
108 | 145 |
|
109 |
| -`RetryPlugin` will be best at the end to optimize the retry process, but it can also be good |
110 |
| -to have it as the first plugin, if one of the plugins is inconsistent and may need a retry. |
| 146 | +The `$next` and `$first` callables will return a Promise (defined in `php-http/promise`). |
| 147 | +You can manipulate the `ResponseInterface` or the `Exception` by using the `then` method of the promise. |
| 148 | + |
| 149 | +``` |
| 150 | +public function handleRequest(RequestInterface $request, callable $next, callable $first) |
| 151 | +{ |
| 152 | + $newRequest = $request->withHeader('MyHeader', 'MyValue'); |
| 153 | +
|
| 154 | + return $next($request)->then(function (ResponseInterface $response) { |
| 155 | + return $response->withHeader('MyResponseHeader', 'value'); |
| 156 | + }, function (Exception $exception) { |
| 157 | + echo $exception->getMessage(); |
| 158 | +
|
| 159 | + throw $exception; |
| 160 | + }); |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +!!! warning "Warning:" |
| 165 | + Contract for the `Http\Promise\Promise` is temporary until a |
| 166 | + [PSR is released](https://groups.google.com/forum/?fromgroups#!topic/php-fig/wzQWpLvNSjs). |
| 167 | + Once it is out, we will use this PSR in HTTPlug and deprecate the old contract. |
111 | 168 |
|
112 |
| -The recommended way to order plugins is the following rules: |
113 | 169 |
|
114 |
| - 1. Plugins that modify the request should be at the beginning (like the `AuthenticationPlugin` or the `CookiePlugin`) |
115 |
| - 2. Plugins which intervene in the workflow should be in the "middle" (like the `RetryPlugin` or the `RedirectPlugin`) |
116 |
| - 3. Plugins which log information should be last (like the `LoggerPlugin` or the `HistoryPlugin`) |
| 170 | +To better understand the whole process check existing implementations in the |
| 171 | +[plugin repository](https://github.com/php-http/plugins). |
117 | 172 |
|
118 |
| -However, there can be exceptions to these rules. For example, |
119 |
| -for security reasons you might not want to log the authentication header |
120 |
| -and chose to put the AuthenticationPlugin after the LoggerPlugin. |
121 | 173 |
|
| 174 | +## Contribution |
122 | 175 |
|
123 |
| -## Implementing your own Plugin |
| 176 | +We are always open to contributions. Either in form of Pull Requests to the core package or self-made plugin packages. |
| 177 | +We encourage everyone to prefer sending Pull Requests, however we don't promise that every plugin gets |
| 178 | +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 |
| 179 | +the core as small as possible with the most widely used plugin implementations. |
124 | 180 |
|
125 |
| -Read this [documentation](plugins/plugin-implementation.md) if you want to create your own Plugin. |
| 181 | +Even if we think that a plugin is not suitable for the core, we want to help you sharing your work with the community. |
| 182 | +You can always open a Pull Request to place a link and a small description of your plugin on the |
| 183 | +[Third Party Plugins](plugins/third-party-plugins.md) page. In special cases, |
| 184 | +we might offer you to host your package under the PHP HTTP namespace. |
0 commit comments