diff --git a/clients/curl-client.rst b/clients/curl-client.rst index 08ee3f7..a337340 100644 --- a/clients/curl-client.rst +++ b/clients/curl-client.rst @@ -12,17 +12,16 @@ To install the cURL client, run: $ composer require php-http/curl-client -Usage ------ - -The cURL client needs a :ref:`message ` and a :ref:`stream ` -factory in order to to work:: +.. include:: includes/install-message-factory.inc - use Http\Client\Curl\Client; +.. include:: includes/install-discovery.inc - $client = new Client($messageFactory, $streamFactory); +Usage +----- -Using `php-http/message `_:: +The cURL client needs a :ref:`message factory ` and a +:ref:`stream factory ` in order to to work. You can either specify the factory +explicitly:: use Http\Client\Curl\Client; use Http\Message\MessageFactory\DiactorosMessageFactory; @@ -30,7 +29,7 @@ Using `php-http/message `_:: $client = new Client(new DiactorosMessageFactory(), new DiactorosStreamFactory()); -Using `php-http/discovery `_:: +Or you can use :doc:`../discovery`:: use Http\Client\Curl\Client; use Http\Discovery\MessageFactoryDiscovery; @@ -53,7 +52,8 @@ You can use `cURL options `_ to configure Client:: ]; $client = new Client(MessageFactoryDiscovery::find(), StreamFactoryDiscovery::find(), $options); -These options cannot be used (will be overwritten by Client): +The following options can not be changed in the set up. Most of them are to be provided with the +request instead: * CURLOPT_CUSTOMREQUEST * CURLOPT_FOLLOWLOCATION diff --git a/clients/includes/further-reading.inc b/clients/includes/further-reading.inc index 553442a..243edb2 100644 --- a/clients/includes/further-reading.inc +++ b/clients/includes/further-reading.inc @@ -1,4 +1,4 @@ -* Use :ref:`plugins ` to customize the way HTTP requests are sent and +* Use :doc:`plugins ` to customize the way HTTP requests are sent and responses processed by following redirects, adding Authentication or Cookie headers and more. * Learn how you can decouple your code from any PSR-7 implementation by using a diff --git a/components/client-common.rst b/components/client-common.rst index 58149bb..213b6f8 100644 --- a/components/client-common.rst +++ b/components/client-common.rst @@ -1,5 +1,3 @@ -.. _client-common: - Client Common ============= diff --git a/discovery.rst b/discovery.rst index a57da73..be484c0 100644 --- a/discovery.rst +++ b/discovery.rst @@ -2,12 +2,16 @@ Discovery ========= The discovery service allows to find and use installed resources. + Under the hood it uses `Puli`_ for the discovery logic. All of our packages provide Puli resources. Discovery is simply a convenience wrapper to statically access clients and factories for when Dependency Injection is not an option. Discovery is useful in libraries that want to offer zero-configuration services relying on the virtual packages. If you have Dependency Injection available, using Puli directly is more elegant (see for example the Symfony HttplugBundle). +Consumers of libraries using discovery still need to make sure they install one of the implementations. +Discovery can only find installed code, not fetch code from other sources. + Currently available discovery services: - HTTP Client Discovery @@ -32,7 +36,7 @@ In both cases you have to install the discovery package itself: .. code-block:: bash - $ composer require php-http/discovery + $ composer require php-http/discovery As mentioned above, discovery relies on Puli. In order to use discovery, you need to also set up Puli. The easiest way is installing the composer-plugin which automatically configures all the composer packages to act as diff --git a/httplug/library-developers.rst b/httplug/library-developers.rst index fcfbb02..4dc77a9 100644 --- a/httplug/library-developers.rst +++ b/httplug/library-developers.rst @@ -69,6 +69,13 @@ When you construct HTTP message objects in your library, you should not depend on a concrete PSR-7 message implementation. Instead, use the :ref:`PHP-HTTP message factory `. +Plugins +------- + +If your library relies on specific plugins, the recommended way is to provide a factory method for +your users, so they can create the correct client from a base HttpClient. See +:ref:`plugin-client.libraries` for a concrete example. + User documentation ------------------ diff --git a/httplug/migrating.rst b/httplug/migrating.rst index 40b9f4c..f249431 100644 --- a/httplug/migrating.rst +++ b/httplug/migrating.rst @@ -12,7 +12,7 @@ The monolithic Ivory package has been separated into several smaller, more speci Instead of ``Ivory\HttpAdapter\PsrHttpAdapter``, use ``Http\Client\HttpClient``. The HttpClient simply has a method to send requests. -If you used the ``Ivory\HttpAdapter\HttpAdapter``, have a look at :ref:`client-common` +If you used the ``Ivory\HttpAdapter\HttpAdapter``, have a look at :doc:`../components/client-common` and use the ``Http\Client\Utils\HttpMethodsClient`` which wraps any HttpClient and provides the convenience methods to send requests without creating RequestInterface instances. @@ -26,7 +26,7 @@ Instead of the ``$options`` argument, configure the client appropriately during If you need different settings, create different instances of the client. You can use :doc:`/plugins/index` to further tune your client. -If you used the ``request`` method, have a look at :ref:`client-common` and +If you used the ``request`` method, have a look at :doc:`../components/client-common` and use the ``Http\Client\Utils\HttpMethodsClient`` which wraps any HttpClient and provides the convenience methods to send requests without creating RequestInterface instances. diff --git a/message/message-factory.rst b/message/message-factory.rst index cb90e16..d5d064e 100644 --- a/message/message-factory.rst +++ b/message/message-factory.rst @@ -20,6 +20,8 @@ The ``MessageFactory`` aims to provide an easy way to construct messages. Usage ----- +.. _stream-factory: + This package provides interfaces for PSR-7 factories including: - ``MessageFactory`` diff --git a/plugins/build-your-own.rst b/plugins/build-your-own.rst new file mode 100644 index 0000000..4267826 --- /dev/null +++ b/plugins/build-your-own.rst @@ -0,0 +1,101 @@ +Building Custom 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 must implement the ``Http\Client\Plugin\Plugin`` interface. + +This interface defines the ``handleRequest`` method that allows to modify behavior of the call:: + + /** + * 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); + +The ``$request`` comes from an upstream plugin or Plugin Client itself. +You can replace it and pass a new version downstream if you need. + +.. note:: + + 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. + +For example a simple plugin setting a header would look like this:: + + 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 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:: + + 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); + } + +.. 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. + +The ``$next`` and ``$first`` callables will return a :doc:`/components/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:: + + Contract for the ``Http\Promise\Promise`` is temporary until a + PSR_ is released. Once it is out, we will use this PSR in HTTPlug and + deprecate the old contract. + +To better understand the whole process check existing implementations in the +`plugin repository`_. + +Contributing Your Plugins to PHP-HTTP +------------------------------------- + +We are open to contributions. If the plugin is of general interest and is not too complex, the best +is to do a Pull Request to ``php-http/plugins``. Please see the :doc:`contribution guide <../development/contributing>`. +We don't promise that every plugin gets merged into the core. We need to keep the core as small as +possible with only the most widely used plugins to keep it maintainable. + +The alternative is providing your plugins in your own repository. Please let us know when you do, +we would like to add a list of existing third party plugins to the list of plugins. + +.. _PSR: https://groups.google.com/forum/?fromgroups#!topic/php-fig/wzQWpLvNSjs +.. _plugin repository: https://github.com/php-http/plugins diff --git a/plugins/index.rst b/plugins/index.rst index a047d8f..d42f129 100644 --- a/plugins/index.rst +++ b/plugins/index.rst @@ -9,6 +9,7 @@ request or you can even start a completely new request. This gives you full cont .. toctree:: introduction + build-your-own authentication cache content-length diff --git a/plugins/introduction.rst b/plugins/introduction.rst index a8d3b6b..c704509 100644 --- a/plugins/introduction.rst +++ b/plugins/introduction.rst @@ -10,8 +10,6 @@ Install the plugin client in your project with Composer_: $ composer require php-http/plugins - - How it works ------------ @@ -69,105 +67,61 @@ The recommended way to order plugins is the following: .. 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. - -Implement your own ------------------- + 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 + :doc:`Authentication Plugin ` after the doc:`Logger Plugin `. -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 must implement the ``Http\Client\Plugin\Plugin`` interface. +Configuration Options +--------------------- -This interface defines the ``handleRequest`` method that allows to modify behavior of the call:: +The PluginClient accepts an array of configuration options that can tweak its behavior. - /** - * 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); +.. _plugin-client.max-restarts: -The ``$request`` comes from an upstream plugin or Plugin Client itself. -You can replace it and pass a new version downstream if you need. +``max_restarts``: int (default 10) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. note:: - - Be aware that the request is immutable. +To prevent issues with faulty plugins or endless redirects, the ``PluginClient`` injects a security +check to the start of the plugin chain. If the same request is restarted more than specified by +that value, execution is aborted and an error is raised. -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. +.. _plugin-client.libraries: -For example a simple plugin setting a header would look like this:: +Libraries that Require Plugins +------------------------------ - public function handleRequest(RequestInterface $request, callable $next, callable $first) - { - $newRequest = $request->withHeader('MyHeader', 'MyValue'); +When :doc:`writing a library based on HTTPlug <../httplug/library-developers>`, you might require +specific plugins to be active. The recommended way for doing this is to provide a factory method +for the ``PluginClient`` that your library users should use. This allows them to inject their own +plugins or configure a different client. For example:: - return $next($newRequest); - } + $myApiClient = new My\Api\Client('https://api.example.org', My\Api\HttpClientFactory::create('john', 's3cr3t')); -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:: + use Http\Client\HttpClient; + use Http\Discovery\HttpClientDiscovery; - public function handleRequest(RequestInterface $request, callable $next, callable $first) + class HttpClientFactory { - if ($someCondition) { - $newRequest = new Request(); - $promise = $first($newRequest); - - // Use the promise to do some jobs ... + /** + * Build the HTTP client to talk with the API. + * + * @param string $user Username + * @param string $pass Password + * @param HttpClient $client Base HTTP client + * + * @return HttpClient + */ + public static function create($user, $pass, HttpClient $client = null) + { + if (!$client) { + $client = HttpClientDiscovery::find(); + } + return new PluginClient($client, [ + new Http\Client\Plugin\ErrorPlugin(), + new AuthenticationPlugin( + // This API has it own authentication algorithm + new ApiAuthentication(Client::AUTH_OAUTH_TOKEN, $user, $pass) + ), + ]); } - - return $next($request); - } - -.. 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. - -The ``$next`` and ``$first`` callables will return a :doc:`/components/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:: - - Contract for the ``Http\Promise\Promise`` is temporary until a - PSR_ is released. Once it is out, we will use this PSR in HTTPlug and - deprecate the old contract. - -To better understand the whole process check existing implementations in the -`plugin repository`_. - -Contribution ------------- - -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. - -.. _PSR: https://groups.google.com/forum/?fromgroups#!topic/php-fig/wzQWpLvNSjs -.. _plugin repository: https://github.com/php-http/plugins diff --git a/plugins/redirect.rst b/plugins/redirect.rst index 4126b63..78d5617 100644 --- a/plugins/redirect.rst +++ b/plugins/redirect.rst @@ -1,4 +1,44 @@ Redirect Plugin =============== -TODO: explain the redirect plugin +The ``RedirectPlugin`` automatically follows redirection answers from a server. If the plugin +detects a redirection, it creates a request to the target URL and restarts the plugin chain. + +The plugin attempts to detect circular redirects and will abort when such a redirect is +encountered. Note that a faulty server appending something on each request is not detected. This +situation is caught by the plugin client itself and can be controlled through the +:ref:`plugin-client.max-restarts` setting. + +Initiate the redirect plugin as follows:: + + use Http\Discovery\HttpClientDiscovery; + use Http\Client\Plugin\PluginClient; + use Http\Client\Plugin\RedirectPlugin; + + $redirectPlugin = new RedirectPlugin(); + + $pluginClient = new PluginClient( + HttpClientDiscovery::find(), + [$redirectPlugin] + ); + +.. warning:: + + Following redirects can increase the robustness of your application. But if you build some sort + of API client, you want to at least keep an eye on the log files. Having your application + follow redirects instead of going to the right end point directly makes your application slower + and increases the load on both server and client. + +Options +------- + +``preserve_header``: boolean|string[] (default: true) + +When set to ``true``, all headers are kept for the next request. ``false`` means all headers are +removed. An array of strings is treated as a whitelist of header names to keep from the original +request. + +``use_default_for_multiple``: bool (default: true) + +Whether to follow the default direction on the multiple redirection status code 300. If set to +false, a status of 300 will raise the ``MultipleRedirectionException``. diff --git a/plugins/retry.rst b/plugins/retry.rst index c51aa77..9050d7d 100644 --- a/plugins/retry.rst +++ b/plugins/retry.rst @@ -1,4 +1,34 @@ Retry Plugin ============ -TODO: explain the retry plugin +The ``RetryPlugin`` can automatically attempt to re-send a request that failed, to work around +unreliable connections and servers. It relies on errors to throw exceptions, so you probably want +to place the :doc:`error` later in the plugin chain:: + + use Http\Discovery\HttpClientDiscovery; + use Http\Client\Plugin\PluginClient; + use Http\Client\Plugin\ErrorPlugin; + use Http\Client\Plugin\RetryPlugin; + + $pluginClient = new PluginClient( + HttpClientDiscovery::find(), + [ + new RetryPlugin(), + new ErrorPlugin(), + ] + ); + +.. warning:: + + You should keep an eye on retried requests, as they add overhead. If some request fails due to + a client side mistake, retrying is only a waste of time and resources. + +Contrary to the :doc:`redirect` the retry plugin does not restart the chain but simply try again +from the current position. + +Options +------- + +``retries``: int (default: 1) + +Number of retry attempts to make before giving up. diff --git a/spelling_word_list.txt b/spelling_word_list.txt index 4154e6e..8ca7e1c 100644 --- a/spelling_word_list.txt +++ b/spelling_word_list.txt @@ -23,6 +23,7 @@ sexualized sublicense sync username +whitelist wiki workflow Puli