├── .gitignore ├── favicon.ico ├── CONTRIBUTING ├── _static ├── logo.png └── custom.css ├── requirements.txt ├── assets └── img │ ├── plugefant.png │ ├── debug-toolbar.png │ ├── dependency-graph.png │ └── symfony-profiler │ ├── dashboard.png │ ├── request-stack.png │ └── error-plugin-failure.png ├── components ├── adapter-integration-tests.rst ├── multipart-stream-builder.rst ├── promise.rst └── client-common.rst ├── clients ├── includes │ ├── further-reading-sync.inc │ ├── further-reading-async.inc │ └── further-reading.inc ├── zend-adapter.rst ├── artax-adapter.rst ├── symfony-client.rst ├── cakephp-adapter.rst ├── guzzle5-adapter.rst ├── curl-client.rst ├── buzz-adapter.rst ├── react-adapter.rst ├── guzzle6-adapter.rst ├── guzzle7-adapter.rst ├── socket-client.rst └── mock-client.rst ├── image-sources └── httplug-dependencies.key ├── development ├── index.rst ├── license.rst ├── documentation.rst ├── code-of-conduct.rst └── contributing.rst ├── watch.sh ├── .editorconfig ├── httplug ├── usage.rst ├── backwards-compatibility.rst ├── migrating.rst ├── exceptions.rst ├── introduction.rst ├── users.rst ├── library-developers.rst └── tutorial.rst ├── .readthedocs.yaml ├── message.rst ├── integrations ├── index.rst └── symfony-full-configuration.rst ├── Pipfile ├── plugins ├── cookie.rst ├── index.rst ├── query.rst ├── history.rst ├── authentication.rst ├── content-type.rst ├── stopwatch.rst ├── decoder.rst ├── logger.rst ├── seekable-body-plugins.rst ├── content-length.rst ├── request-uri-manipulations.rst ├── error.rst ├── retry.rst ├── redirect.rst ├── headers.rst ├── build-your-own.rst ├── vcr.rst ├── introduction.rst └── cache.rst ├── spelling_word_list.txt ├── .github └── workflows │ └── spellcheck.yml ├── LICENSE ├── README.md ├── message ├── message-factory.rst └── authentication.rst ├── index.rst ├── Makefile ├── conf.py ├── clients.rst └── Pipfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/favicon.ico -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Please see http://docs.php-http.org/en/latest/development/contributing.html 2 | -------------------------------------------------------------------------------- /_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/_static/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-rtd-theme 3 | sphinxcontrib-spelling 4 | pyenchant 5 | -------------------------------------------------------------------------------- /assets/img/plugefant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/assets/img/plugefant.png -------------------------------------------------------------------------------- /assets/img/debug-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/assets/img/debug-toolbar.png -------------------------------------------------------------------------------- /components/adapter-integration-tests.rst: -------------------------------------------------------------------------------- 1 | Adapter Integration Tests 2 | ========================= 3 | 4 | TODO 5 | -------------------------------------------------------------------------------- /assets/img/dependency-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/assets/img/dependency-graph.png -------------------------------------------------------------------------------- /clients/includes/further-reading-sync.inc: -------------------------------------------------------------------------------- 1 | Further reading 2 | --------------- 3 | 4 | .. include:: includes/further-reading.inc 5 | -------------------------------------------------------------------------------- /image-sources/httplug-dependencies.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/image-sources/httplug-dependencies.key -------------------------------------------------------------------------------- /assets/img/symfony-profiler/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/assets/img/symfony-profiler/dashboard.png -------------------------------------------------------------------------------- /assets/img/symfony-profiler/request-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/assets/img/symfony-profiler/request-stack.png -------------------------------------------------------------------------------- /assets/img/symfony-profiler/error-plugin-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-http/documentation/HEAD/assets/img/symfony-profiler/error-plugin-failure.png -------------------------------------------------------------------------------- /development/index.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | .. toctree:: 5 | 6 | code-of-conduct 7 | contributing 8 | documentation 9 | license 10 | -------------------------------------------------------------------------------- /watch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Note: --include is not available in all versions of inotifywait 4 | while inotifywait -e modify -r --include ".+?\.rst" .; do 5 | make html 6 | done 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /clients/includes/further-reading-async.inc: -------------------------------------------------------------------------------- 1 | Further reading 2 | --------------- 3 | 4 | .. include:: includes/further-reading.inc 5 | 6 | * Read more about :doc:`promises ` when using asynchronous 7 | requests. 8 | -------------------------------------------------------------------------------- /httplug/usage.rst: -------------------------------------------------------------------------------- 1 | HTTPlug usage 2 | ============= 3 | 4 | HTTPlug is relevant for three groups of users: 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | Library Users 10 | Application Developers 11 | Library Developers 12 | 13 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: conf.py 10 | 11 | # additional formats 12 | formats: 13 | - pdf 14 | 15 | python: 16 | install: 17 | - requirements: requirements.txt 18 | -------------------------------------------------------------------------------- /clients/includes/further-reading.inc: -------------------------------------------------------------------------------- 1 | * Use :doc:`plugins ` to customize the way HTTP requests are sent and 2 | responses processed by following redirects, adding Authentication or Cookie 3 | headers and more. 4 | * Learn how you can decouple your code from any PSR-7 implementation by using 5 | the :ref:`HTTP factories `. 6 | -------------------------------------------------------------------------------- /message.rst: -------------------------------------------------------------------------------- 1 | Message 2 | ======= 3 | 4 | This package contains various PSR-7 tools which might be useful in an HTTP workflow: 5 | 6 | .. toctree:: 7 | 8 | message/authentication 9 | message/message-factory 10 | 11 | * Authentication method implementations 12 | * Various Stream encoding tools 13 | * Message decorators 14 | * Cookie implementation 15 | -------------------------------------------------------------------------------- /integrations/index.rst: -------------------------------------------------------------------------------- 1 | Framework Integrations 2 | ====================== 3 | 4 | HTTPlug provides the following framework integrations: 5 | 6 | .. toctree:: 7 | 8 | symfony-bundle 9 | symfony-full-configuration 10 | 11 | * `Nette Framework Integration`_ (external Documentation) 12 | 13 | .. _`Nette Framework Integration`: https://github.com/FreezyBee/Httplug 14 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | sphinx-php = {git = "https://github.com/fabpot/sphinx-php.git"} 10 | sphinx-rtd-theme = "==1.0.0" 11 | sphinxcontrib-spelling = "~=7.2.0" 12 | pyenchant = "~=3.2.1" 13 | docutils = "==0.17" 14 | Sphinx = "~=4.2.0" 15 | 16 | [requires] 17 | python_version = "3.7" 18 | -------------------------------------------------------------------------------- /plugins/cookie.rst: -------------------------------------------------------------------------------- 1 | Cookie Plugin 2 | ============= 3 | 4 | The ``CookiePlugin`` allow you to store cookies in a ``CookieJar`` and reuse them on consequent requests according 5 | to :rfc:`6265#section-4` specification:: 6 | 7 | use Http\Discovery\HttpClientDiscovery; 8 | use Http\Message\CookieJar; 9 | use Http\Client\Common\PluginClient; 10 | use Http\Client\Common\Plugin\CookiePlugin; 11 | 12 | $cookiePlugin = new CookiePlugin(new CookieJar()); 13 | 14 | $pluginClient = new PluginClient( 15 | HttpClientDiscovery::find(), 16 | [$cookiePlugin] 17 | ); 18 | -------------------------------------------------------------------------------- /plugins/index.rst: -------------------------------------------------------------------------------- 1 | .. _plugins: 2 | 3 | Plugins 4 | ======= 5 | 6 | The plugin system allows to wrap a Client and add some processing logic prior to and/or after sending the actual 7 | request or you can even start a completely new request. This gives you full control over what happens in your workflow. 8 | 9 | .. toctree:: 10 | 11 | introduction 12 | build-your-own 13 | seekable-body-plugins 14 | authentication 15 | cache 16 | content-length 17 | content-type 18 | cookie 19 | decoder 20 | error 21 | headers 22 | history 23 | logger 24 | query 25 | redirect 26 | request-uri-manipulations 27 | retry 28 | stopwatch 29 | vcr 30 | -------------------------------------------------------------------------------- /_static/custom.css: -------------------------------------------------------------------------------- 1 | .row-even .line-block, .row-odd .line-block { 2 | margin-left: 0; 3 | } 4 | 5 | .versionmodified { 6 | font-weight: bold; 7 | } 8 | 9 | .wy-nav-side { 10 | background: #0190c9; 11 | background-image: linear-gradient(#64caf6, #012038); 12 | } 13 | 14 | .wy-side-nav-search { 15 | background: #d9e1f2; 16 | } 17 | .wy-side-nav-search > a, .wy-side-nav-search > div.version { 18 | color: #404040; 19 | } 20 | 21 | .wy-menu-vertical a:hover { 22 | color: #122437; 23 | } 24 | .wy-menu-vertical p.caption { 25 | color: #b3b3b3; 26 | margin-top: 16px; 27 | margin-bottom: 0; 28 | } 29 | .wy-menu-vertical p.caption .caption-text { 30 | font-size: 120%; 31 | } 32 | -------------------------------------------------------------------------------- /clients/zend-adapter.rst: -------------------------------------------------------------------------------- 1 | Zend Adapter (deprecated) 2 | ========================= 3 | 4 | An HTTPlug adapter for the Zend HTTP client. 5 | 6 | Zend framework meanwhile has been renamed to Laminas, and the client is no 7 | longer maintained. 8 | 9 | This adapter only implements the PHP-HTTP synchronous interface. This interface 10 | has been superseded by PSR-18, which the `Laminas Diactoros`_ implements 11 | directly. 12 | 13 | Installation 14 | ------------ 15 | 16 | To install the Zend adapter, which will also install Zend itself (if it was 17 | not yet included in your project), run: 18 | 19 | .. code-block:: bash 20 | 21 | $ composer require php-http/zend-adapter 22 | 23 | .. _Laminas Diactoros: https://docs.laminas.dev/laminas-diactoros/ 24 | -------------------------------------------------------------------------------- /spelling_word_list.txt: -------------------------------------------------------------------------------- 1 | Artax 2 | async 3 | auth 4 | autowiring 5 | backoff 6 | backtrace 7 | boolean 8 | callables 9 | chunked 10 | cURL 11 | Diactoros 12 | discoverable 13 | Elasticsearch 14 | fallback 15 | GitHub 16 | hotfix 17 | Laminas 18 | Liskov 19 | Symfony 20 | HTTPlug 21 | Ivory 22 | instantiation 23 | integrations 24 | interoperability 25 | namespace 26 | plugin 27 | plugins 28 | matchers 29 | matcher 30 | Monolog 31 | multipart 32 | natively 33 | param 34 | params 35 | profiler 36 | PHP 37 | phpdoc 38 | rebase 39 | redirections 40 | Semver 41 | Seekable 42 | seekable 43 | sexualized 44 | sublicense 45 | superglobals 46 | sync 47 | toolbar 48 | typehint 49 | username 50 | versa 51 | versionadded 52 | whitelist 53 | wiki 54 | Wikipedia 55 | workflow 56 | Zend 57 | -------------------------------------------------------------------------------- /plugins/query.rst: -------------------------------------------------------------------------------- 1 | Query plugin 2 | ============ 3 | 4 | Default Query parameters 5 | ------------------------ 6 | 7 | The plugin ``QueryDefaultsPlugin`` allows you to define default values for 8 | query parameters. If a query parameter is not set, it will be added. However, if the query parameter 9 | is already present, the request is left unchanged. Names and values must not be URL encoded as this 10 | plugin will encode them:: 11 | 12 | use Http\Discovery\HttpClientDiscovery; 13 | use Http\Client\Common\PluginClient; 14 | use Http\Client\Common\Plugin\QueryDefaultsPlugin; 15 | 16 | $queryDefaultsPlugin = new QueryDefaultsPlugin([ 17 | 'locale' => 'en' 18 | ]); 19 | 20 | $pluginClient = new PluginClient( 21 | HttpClientDiscovery::find(), 22 | [$queryDefaultsPlugin] 23 | ); 24 | 25 | -------------------------------------------------------------------------------- /plugins/history.rst: -------------------------------------------------------------------------------- 1 | History Plugin 2 | ============== 3 | 4 | The ``HistoryPlugin`` notifies a ``Http\Client\Common\Plugin\Journal`` of all 5 | successful and failed calls:: 6 | 7 | use Http\Discovery\HttpClientDiscovery; 8 | use Http\Client\Common\PluginClient; 9 | use Http\Client\Common\Plugin\HistoryPlugin; 10 | 11 | $historyPlugin = new HistoryPlugin(new \My\Journal\Implementation()); 12 | 13 | $pluginClient = new PluginClient( 14 | HttpClientDiscovery::find(), 15 | [$historyPlugin] 16 | ); 17 | 18 | 19 | As an example, HttplugBundle uses this plugin to collect responses and exceptions associated with 20 | requests for the debug toolbar. 21 | 22 | This plugin only collects data after resolution. For logging purposes, it is recommended to use 23 | the :doc:`LoggerPlugin ` instead, which logs events as they occur. 24 | -------------------------------------------------------------------------------- /plugins/authentication.rst: -------------------------------------------------------------------------------- 1 | Authentication Plugin 2 | ===================== 3 | 4 | This plugin uses the :doc:`authentication component ` 5 | from ``php-http/message`` to authenticate requests sent through the client:: 6 | 7 | use Http\Discovery\HttpClientDiscovery; 8 | use Http\Message\Authentication\BasicAuth; 9 | use Http\Client\Common\PluginClient; 10 | use Http\Client\Common\Plugin\AuthenticationPlugin; 11 | 12 | $authentication = new BasicAuth('username', 'password'); 13 | $authenticationPlugin = new AuthenticationPlugin($authentication); 14 | 15 | $pluginClient = new PluginClient( 16 | HttpClientDiscovery::find(), 17 | [$authenticationPlugin] 18 | ); 19 | 20 | Check the :doc:`authentication component documentation ` 21 | for the list of available authentication methods. 22 | -------------------------------------------------------------------------------- /clients/artax-adapter.rst: -------------------------------------------------------------------------------- 1 | Artax Adapter 2 | ============= 3 | 4 | An HTTPlug adapter for the `Artax HTTP client`_. 5 | 6 | Installation 7 | ------------ 8 | 9 | To install the Artax adapter, which will also install Artax itself (if it was 10 | not yet included in your project), run: 11 | 12 | .. code-block:: bash 13 | 14 | $ composer require php-http/artax-adapter 15 | 16 | Usage 17 | ----- 18 | 19 | Begin by creating a Artax adapter:: 20 | 21 | use Amp\Artax\DefaultClient; 22 | use Http\Adapter\Artax\Client as ArtaxAdapter; 23 | use Http\Message\MessageFactory\GuzzleMessageFactory; 24 | 25 | $adapter = new ArtaxAdapter(new DefaultClient(), new GuzzleMessageFactory()); 26 | 27 | Or relying on :doc:`discovery `:: 28 | 29 | use Http\Adapter\Artax\Client as ArtaxAdapter; 30 | 31 | $adapter = new ArtaxAdapter(); 32 | 33 | .. include:: includes/further-reading-async.inc 34 | 35 | .. _Artax HTTP client: https://github.com/amphp/artax 36 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yml: -------------------------------------------------------------------------------- 1 | name: spellcheck 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | - name: Install dependencies 17 | run: | 18 | sudo apt update && sudo apt install -y enchant-2 19 | python -m pip install --upgrade setuptools 20 | python -m pip install --upgrade pyenchant sphinx-rtd-theme sphinxcontrib-spelling 21 | - name: Check spelling 22 | # show list of misspelled words 23 | run: | 24 | make spelling 25 | if [[ -s "_build/spelling/output.txt" ]]; then cat "_build/spelling/output.txt"; fi 26 | if [[ -s "_build/spelling/output.txt" ]]; then false; fi 27 | -------------------------------------------------------------------------------- /plugins/content-type.rst: -------------------------------------------------------------------------------- 1 | Content-Type Plugin 2 | =================== 3 | 4 | The ``ContentTypePlugin`` sets the correct ``Content-Type`` header value based on the content of the body stream of the 5 | request. This helps HTTP servers to handle the request:: 6 | 7 | use Http\Discovery\HttpClientDiscovery; 8 | use Http\Client\Common\PluginClient; 9 | use Http\Client\Common\Plugin\ContentTypePlugin; 10 | 11 | $contentTypePlugin = new ContentTypePlugin(); 12 | 13 | $pluginClient = new PluginClient( 14 | HttpClientDiscovery::find(), 15 | [$contentTypePlugin] 16 | ); 17 | 18 | For now, the plugin can only detect JSON or XML content. If the content of the stream can not be determined, the plugin does nothing. 19 | 20 | Options 21 | ------- 22 | 23 | ``skip_detection``: boolean (default: false) 24 | 25 | When set to ``true``, content type detection will be performed only if the body request content size is under the 26 | size_limit parameter value. 27 | 28 | ``size_limit``: int (default: a little bit over 15Mb) 29 | 30 | Determine the size stream limit for which the detection as to be skipped if ``skip_detection`` is ``true``. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Eric GELOEN 2 | Copyright (c) 2015 PHP HTTP Team 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP-HTTP Documentation 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 4 | 5 | 6 | **This is the documentation repository of the PHP-HTTP software components** 7 | 8 | Browse the documentation on [Read the Docs](http://php-http.readthedocs.org/). 9 | 10 | 11 | ## Contributing 12 | 13 | Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). 14 | 15 | #### Add your package under "They use us" 16 | 17 | We want to keep this list of packages to a small number. If your package should be considered for be listed here 18 | it must be a well known packages with a significant number of downloads (a million or more). 19 | 20 | However, we are much less strict with the packages on [httplug.io](http://httplug.io/). On [that repository](https://github.com/php-http/httplug.github.io) 21 | we generally accept any stable and documented package using httplug. 22 | 23 | ## Security 24 | 25 | If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). 26 | 27 | 28 | ## License 29 | 30 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 31 | -------------------------------------------------------------------------------- /development/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | Copyright (c) 2014–2015 Eric GELOEN 5 | 6 | Copyright (c) 2015–2016 PHP HTTP Team 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is furnished 13 | to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /plugins/stopwatch.rst: -------------------------------------------------------------------------------- 1 | Stopwatch Plugin 2 | ================ 3 | 4 | Install 5 | ------- 6 | 7 | .. code-block:: bash 8 | 9 | $ composer require php-http/stopwatch-plugin 10 | 11 | Usage 12 | ----- 13 | 14 | The ``StopwatchPlugin`` records the duration of HTTP requests with a 15 | ``Symfony\Component\Stopwatch\Stopwatch`` instance:: 16 | 17 | use Http\Discovery\HttpClientDiscovery; 18 | use Http\Client\Common\PluginClient; 19 | use Http\Client\Common\Plugin\StopwatchPlugin; 20 | use Symfony\Component\Stopwatch\Stopwatch; 21 | 22 | $stopwatch = new Stopwatch(); 23 | 24 | $stopwatchPlugin = new StopwatchPlugin($stopwatch); 25 | 26 | $pluginClient = new PluginClient( 27 | HttpClientDiscovery::find(), 28 | [$stopwatchPlugin] 29 | ); 30 | 31 | // ... 32 | 33 | foreach ($stopwatch->getSections() as $section) { 34 | foreach ($section->getEvents() as $name => $event) { 35 | echo sprintf('Request %s took %s ms and used %s bytes of memory', $name, $event->getDuration(), $event->getMemory()); 36 | } 37 | } 38 | 39 | .. warning:: 40 | 41 | The results of the stop watch will be unreliable when using an asynchronous client. Execution 42 | time can be longer than it really was, depending on when the status was checked again, and 43 | memory consumption will be mixed up with other code that was executed while waiting for the 44 | response. 45 | -------------------------------------------------------------------------------- /clients/symfony-client.rst: -------------------------------------------------------------------------------- 1 | Symfony Client 2 | ============== 3 | 4 | The Symfony HTTP client provides a ``HttplugClient`` class that implements the ``Http\Client\HttpAsyncClient``. 5 | Until Symfony 5.4, it also implemented the ``Http\Client\HttpClient``, newer versions implement the PSR-18 6 | ``HttpClientInterface`` instead. 7 | 8 | Installation 9 | ------------ 10 | 11 | The Symfony client does not depend on HTTPlug, but the ``HttplugClient`` does. To use the Symfony client with HTTPlug, 12 | you need to install both the client and HTTPlug with: 13 | 14 | .. code-block:: bash 15 | 16 | $ composer require symfony/http-client php-http/httplug 17 | 18 | This client does not come with a PSR-7 implementation out of the box. If you do 19 | not require one, `discovery <../discovery>` will install `Nyholm PSR-7`_. If 20 | you do not allow the composer plugin of the ``php-http/discovery`` component, 21 | you need to install a PSR-7 implementation manually: 22 | 23 | .. code-block:: bash 24 | 25 | $ composer require nyholm/psr7 26 | 27 | Usage 28 | ----- 29 | .. code-block:: php 30 | 31 | use Symfony\Component\HttpClient\HttplugClient; 32 | 33 | $symfonyClient = new HttplugClient(); 34 | 35 | .. note:: 36 | 37 | Check the official `Symfony HttpClient`_ documentation for more details. 38 | 39 | .. _Symfony HttpClient: https://symfony.com/doc/current/components/http_client.html#httplug 40 | 41 | .. _Nyholm PSR-7: https://github.com/Nyholm/psr7/ 42 | -------------------------------------------------------------------------------- /httplug/backwards-compatibility.rst: -------------------------------------------------------------------------------- 1 | Backwards compatibility 2 | ======================= 3 | 4 | Backwards compatibility is an important topic for us, as it should be in every open source project. We follow 5 | Semver_ which allows us to only break backwards compatibility between major versions. We use 6 | deprecation notices to inform you about the changes made before they are removed. 7 | 8 | Our backwards compatibility promise does not include classes or functions with the ``@internal`` annotation. 9 | 10 | Symfony Bundle 11 | -------------- 12 | 13 | The HttplugBundle is just a Symfony integration for HTTPlug and it does not have any classes which falls under the BC 14 | promise. The backwards compatibility of the bundle is only the configuration and its values (and of course the behavior 15 | of those values). 16 | 17 | Discovery 18 | --------- 19 | 20 | The order of the strategies is not part of our BC promise. The strategies themselves are marked 21 | as ``@internal`` so they are also not part of our BC promise. 22 | However, we do promise that we will not remove a strategy neither will we remove classes from the 23 | ``CommonClassesStrategy``. 24 | 25 | The consequences of the BC promise for the discovery library is that you can not rely on the *same* client to be 26 | returned in the future. However, if discovery does find a client now, you can be sure that after future updates it will still discover a client. 27 | 28 | .. _Semver: http://semver.org/ 29 | -------------------------------------------------------------------------------- /plugins/decoder.rst: -------------------------------------------------------------------------------- 1 | Decoder Plugin 2 | ============== 3 | 4 | The ``DecoderPlugin`` decodes the body of the response with filters coming from the ``Transfer-Encoding`` 5 | or ``Content-Encoding`` headers:: 6 | 7 | use Http\Discovery\HttpClientDiscovery; 8 | use Http\Client\Common\PluginClient; 9 | use Http\Client\Common\Plugin\DecoderPlugin; 10 | 11 | $decoderPlugin = new DecoderPlugin(); 12 | 13 | $pluginClient = new PluginClient( 14 | HttpClientDiscovery::find(), 15 | [$decoderPlugin] 16 | ); 17 | 18 | The plugin can handle the following encodings: 19 | 20 | * ``chunked``: Decode a stream with a ``chunked`` encoding (no size in the ``Content-Length`` header of the response) 21 | * ``compress``: Decode a stream encoded with the ``compress`` encoding according to :rfc:`1950` 22 | * ``deflate``: Decode a stream encoded with the ``inflate`` encoding according to :rfc:`1951` 23 | * ``gzip``: Decode a stream encoded with the ``gzip`` encoding according to :rfc:`1952` 24 | 25 | You can also use the decoder plugin to decode only the ``Transfer-Encoding`` header and not the ``Content-Encoding`` one 26 | by setting the ``use_content_encoding`` configuration option to false:: 27 | 28 | $decoderPlugin = new DecoderPlugin(['use_content_encoding' => false]); 29 | 30 | Not decoding content is useful when you don't want to get the encoded response body, or acting as a proxy but still 31 | be able to decode message from the ``Transfer-Encoding`` header value. 32 | -------------------------------------------------------------------------------- /plugins/logger.rst: -------------------------------------------------------------------------------- 1 | Logger Plugin 2 | ============= 3 | 4 | Install 5 | ------- 6 | 7 | .. code-block:: bash 8 | 9 | $ composer require php-http/logger-plugin 10 | 11 | Usage 12 | ----- 13 | 14 | The ``LoggerPlugin`` converts requests, responses and exceptions to strings and logs them with a PSR3_ 15 | compliant logger:: 16 | 17 | use Http\Discovery\HttpClientDiscovery; 18 | use Http\Client\Common\PluginClient; 19 | use Http\Client\Common\Plugin\LoggerPlugin; 20 | use Monolog\Logger; 21 | 22 | $loggerPlugin = new LoggerPlugin(new Logger('http')); 23 | 24 | $pluginClient = new PluginClient( 25 | HttpClientDiscovery::find(), 26 | [$loggerPlugin] 27 | ); 28 | 29 | The log level for exceptions is ``error``, the request and responses without exceptions are logged at level ``info``. 30 | Request and response/errors can be correlated by looking at the ``uid`` of the log context. 31 | If you don't want to normally log requests, you can set the logger to normally only log ``error`` but use the 32 | ``Fingerscrossed`` logger of Monolog to also log the request in case an exception is encountered. 33 | 34 | By default it uses ``Http\Message\Formatter\SimpleFormatter`` to format the request or the response into a string. 35 | You can use any formatter implementing the ``Http\Message\Formatter`` interface:: 36 | 37 | $formatter = new \My\Formatter\Implementation(); 38 | 39 | $loggerPlugin = new LoggerPlugin(new Logger('http'), $formatter); 40 | 41 | .. _PSR3: http://www.php-fig.org/psr/psr-3/ 42 | -------------------------------------------------------------------------------- /httplug/migrating.rst: -------------------------------------------------------------------------------- 1 | Migrating to HTTPlug 2 | ==================== 3 | 4 | If you currently use a concrete HTTP client implementation or the Ivory HTTP Adapter, 5 | migrating to HTTPlug should not be very hard. 6 | 7 | Migrating from Ivory HTTP Adapter 8 | --------------------------------- 9 | 10 | The monolithic Ivory package has been separated into several smaller, more specific packages. 11 | 12 | Instead of ``Ivory\HttpAdapter\PsrHttpAdapter``, use ``Http\Client\HttpClient``. 13 | The HttpClient simply has a method to send requests. 14 | 15 | If you used the ``Ivory\HttpAdapter\HttpAdapter``, have a look at :doc:`../components/client-common` 16 | and use the ``Http\Client\Utils\HttpMethodsClient`` which wraps any HttpClient 17 | and provides the convenience methods to send requests without creating 18 | RequestInterface instances. 19 | 20 | Migrating from Guzzle 21 | --------------------- 22 | 23 | Replace usage of ``GuzzleHttp\ClientInterface`` with ``Http\Client\HttpClient``. 24 | The ``send`` method is called ``sendRequest``. 25 | Instead of the ``$options`` argument, configure the client appropriately during set up. 26 | If you need different settings, create different instances of the client. 27 | You can use :doc:`/plugins/index` to further tune your client. 28 | 29 | If you used the ``request`` method, have a look at :doc:`../components/client-common` and 30 | use the ``Http\Client\Utils\HttpMethodsClient`` which wraps any HttpClient and 31 | provides the convenience methods to send requests without creating 32 | RequestInterface instances. 33 | -------------------------------------------------------------------------------- /plugins/seekable-body-plugins.rst: -------------------------------------------------------------------------------- 1 | Seekable Body Plugins 2 | ===================== 3 | 4 | ``RequestSeekableBodyPlugin`` and ``ResponseSeekableBodyPlugin`` ensure that body used in request and response is always seekable. 5 | Use this plugin if you want plugins to read the stream and then be able to rewind it:: 6 | 7 | use Http\Discovery\HttpClientDiscovery; 8 | use Http\Client\Common\PluginClient; 9 | use Http\Client\Common\Plugin\RequestSeekableBodyPlugin; 10 | use Http\Client\Common\Plugin\ResponseSeekableBodyPlugin; 11 | 12 | $options = [ 13 | 'use_file_buffer' => true, 14 | 'memory_buffer_size' => 2097152, 15 | ]; 16 | $requestSeekableBodyPlugin = new RequestSeekableBodyPlugin($options); 17 | $responseSeekableBodyPlugin = new ResponseSeekableBodyPlugin($options); 18 | 19 | $pluginClient = new PluginClient( 20 | HttpClientDiscovery::find(), 21 | [$requestSeekableBodyPlugin, $responseSeekableBodyPlugin] 22 | ); 23 | 24 | Those plugins support the following options (which are passed to the ``BufferedStream`` class): 25 | 26 | * ``use_file_buffer``: Whether it should use a temporary file to buffer the body of a stream if it's too big 27 | * ``memory_buffer_size``: Maximum memory to use for buffering the stream before it switch to a file 28 | 29 | ``RequestSeekableBodyPlugin`` should be the first of your plugins, then the following plugins can seek in the request body (i.e. for logging purpose). 30 | ``ResponseSeekableBodyPlugin`` should be the last plugin, then previous plugins can seek response body. 31 | -------------------------------------------------------------------------------- /clients/cakephp-adapter.rst: -------------------------------------------------------------------------------- 1 | CakePHP Adapter (deprecated) 2 | ============================ 3 | 4 | This adapter only implements the PHP-HTTP synchronous interface. This interface 5 | has been superseded by PSR-18, which the `CakePHP HTTP client`_ implements 6 | directly. 7 | 8 | Installation 9 | ------------ 10 | 11 | To install the CakePHP adapter, which will also install CakePHP itself (if it was 12 | not yet included in your project), run: 13 | 14 | .. code-block:: bash 15 | 16 | $ composer require php-http/cakephp-adapter 17 | 18 | Usage 19 | ----- 20 | 21 | Begin by creating a CakePHP HTTP client, passing any configuration parameters you 22 | like:: 23 | 24 | use Cake\Http\Client as CakeClient; 25 | 26 | $config = [ 27 | // Config params 28 | ]; 29 | $cakeClient = new CakeClient($config); 30 | 31 | Then create the adapter:: 32 | 33 | use Http\Adapter\Cake\Client as CakeAdapter; 34 | 35 | $adapter = new CakeAdapter($cakeClient); 36 | 37 | .. note:: 38 | 39 | The client parameter is optional; if you do not supply it (or set it to 40 | ``null``) the adapter will create a default CakePHP HTTP client without any options. 41 | 42 | 43 | Or if you installed the :doc:`discovery ` layer:: 44 | 45 | use Http\Adapter\Cake\Client as CakeAdapter; 46 | 47 | $adapter = new CakeAdapter($cakeClient); 48 | 49 | .. warning:: 50 | 51 | The message factory parameter is mandatory if the discovery layer is not installed. 52 | 53 | .. include:: includes/further-reading-async.inc 54 | 55 | .. _CakePHP HTTP client: https://book.cakephp.org/5.0/en/core-libraries/httpclient.html 56 | -------------------------------------------------------------------------------- /clients/guzzle5-adapter.rst: -------------------------------------------------------------------------------- 1 | Guzzle5 Adapter (deprecated) 2 | ============================ 3 | 4 | An HTTPlug adapter for the `Guzzle 5 HTTP client`_. 5 | 6 | This adapter only implements the PHP-HTTP synchronous interface. This interface 7 | has been superseded by PSR-18. 8 | 9 | Guzzle 5 is very old and `not maintained anymore`_. We recommend to upgrade to 10 | Guzzle version 7. 11 | 12 | Installation 13 | ------------ 14 | 15 | To install the Guzzle adapter, which will also install Guzzle itself (if it was 16 | not yet included in your project), run: 17 | 18 | .. code-block:: bash 19 | 20 | $ composer require php-http/guzzle5-adapter 21 | 22 | Usage 23 | ----- 24 | 25 | Begin by creating a Guzzle client, passing any configuration parameters you 26 | like:: 27 | 28 | use GuzzleHttp\Client as GuzzleClient; 29 | 30 | $config = [ 31 | // Config params 32 | ]; 33 | $guzzle = new GuzzleClient($config); 34 | 35 | Then create the adapter:: 36 | 37 | use Http\Adapter\Guzzle5\Client as GuzzleAdapter; 38 | use Http\Message\MessageFactory\GuzzleMessageFactory; 39 | 40 | $adapter = new GuzzleAdapter($guzzle, new GuzzleMessageFactory()); 41 | 42 | Or if you installed the :doc:`discovery ` layer:: 43 | 44 | use Http\Adapter\Guzzle5\Client as GuzzleAdapter; 45 | 46 | $adapter = new GuzzleAdapter($guzzle); 47 | 48 | .. warning:: 49 | 50 | The message factory parameter is mandatory if the discovery layer is not installed. 51 | 52 | .. include:: includes/further-reading-sync.inc 53 | 54 | .. _Guzzle 5 HTTP client: http://docs.guzzlephp.org/en/5.3/ 55 | .. _not maintained anymore: https://github.com/guzzle/guzzle#version-guidance 56 | -------------------------------------------------------------------------------- /plugins/content-length.rst: -------------------------------------------------------------------------------- 1 | Content-Length Plugin 2 | ===================== 3 | 4 | The ``ContentLengthPlugin`` sets the correct ``Content-Length`` header value based on the size of the body stream of the 5 | request. This helps HTTP servers to handle the request:: 6 | 7 | use Http\Discovery\HttpClientDiscovery; 8 | use Http\Client\Common\PluginClient; 9 | use Http\Client\Common\Plugin\ContentLengthPlugin; 10 | 11 | $contentLengthPlugin = new ContentLengthPlugin(); 12 | 13 | $pluginClient = new PluginClient( 14 | HttpClientDiscovery::find(), 15 | [$contentLengthPlugin] 16 | ); 17 | 18 | If the size of the stream can not be determined, the plugin sets the Encoding header to ``chunked``, as defined in 19 | :rfc:`7230#section-4.1` 20 | 21 | This is useful when you want to transfer data of unknown size to an HTTP application without consuming memory. 22 | 23 | As an example, let's say you want to send a tar archive of the current directory to an API. Normally you would 24 | end up doing this in 2 steps, first saving the result of the tar archive into a file or into the memory of 25 | PHP with a variable, then sending this content with an HTTP Request. 26 | 27 | With this plugin you can achieve this behavior without doing the first step:: 28 | 29 | proc_open("/usr/bin/env tar c .", [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]], $pipes, "/path/to/directory"); 30 | $tarResource = $pipes[1]; 31 | 32 | $request = MessageFactoryDiscovery::find()->createRequest('POST', '/url/to/api/endpoint', [], $tarResource); 33 | $response = $pluginClient->sendRequest($request); 34 | 35 | In this case the tar output is directly streamed to the server without using memory on the PHP side. 36 | -------------------------------------------------------------------------------- /httplug/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | HTTPlug defines a common interface for all exceptions thrown by HTTPlug implementations. 5 | Every exception thrown by a HTTP client must implement ``Http\Client\Exception``. 6 | 7 | ``HttpClient::sendRequest()`` can throw one of the following exceptions. 8 | 9 | ================================== ============================= =================== 10 | Exception Thrown when Methods available 11 | ================================== ============================= =================== 12 | TransferException something unexpected happened \- 13 | └ RequestException the request is invalid ``getRequest()`` 14 | |nbsp| |nbsp| └ NetworkException no response received 15 | due to network issues ``getRequest()`` 16 | |nbsp| |nbsp| └ HttpException error response ``getRequest()`` 17 | ``getResponse()`` 18 | ================================== ============================= =================== 19 | 20 | .. note:: 21 | By default clients will always return a PSR-7 response instead of throwing a ``HttpException``. Write your 22 | application to check the response status or use the :doc:`/plugins/error` to make sure 23 | the ``HttpException`` is thrown. 24 | 25 | .. note:: 26 | 27 | The ``sendAsyncRequest`` should never throw an exception but always return a 28 | :doc:`../components/promise`. The exception classes used in ``Promise::wait`` and the ``then`` 29 | callback are however the same as explained here. 30 | 31 | .. |nbsp| unicode:: U+00A0 .. non-breaking space 32 | -------------------------------------------------------------------------------- /plugins/request-uri-manipulations.rst: -------------------------------------------------------------------------------- 1 | Request URI Manipulations 2 | ========================= 3 | 4 | Request URI manipulations can be done thanks to several plugins: 5 | 6 | * ``AddHostPlugin``: Set host, scheme and port. Depending on configuration, 7 | the host is overwritten in every request or only set if not yet defined in the request. 8 | * ``AddPathPlugin``: Prefix the request path with a path, leaving the host information untouched. 9 | * ``BaseUriPlugin``: It's a combination of ``AddHostPlugin`` and ``AddPathPlugin``. 10 | 11 | Each plugin uses the ``UriInterface`` to build the base request:: 12 | 13 | use Http\Discovery\HttpClientDiscovery; 14 | use Http\Discovery\UriFactoryDiscovery; 15 | use Http\Client\Common\PluginClient; 16 | use Http\Client\Common\Plugin\BaseUriPlugin; 17 | 18 | $plugin = new BaseUriPlugin(UriFactoryDiscovery::find()->createUri('https://domain.com:8000/api'), [ 19 | // Always replace the host, even if this one is provided on the sent request. Available for AddHostPlugin. 20 | 'replace' => true, 21 | ])); 22 | 23 | $pluginClient = new PluginClient( 24 | HttpClientDiscovery::find(), 25 | [$plugin] 26 | ); 27 | 28 | The ``AddPathPlugin`` will check if the path prefix is already present on the 29 | URI. This will break for the edge case when the prefix is repeated. For example, 30 | if ``https://example.com/api/api/foo`` is a valid URI on the server and the 31 | configured prefix is ``/api``, the request to ``/api/foo`` is not rewritten. 32 | 33 | For further details, please see the phpdoc on the ``AddPathPlugin`` source code. 34 | 35 | No solution fits all use cases. This implementation works fine for the common 36 | use cases. If you have a specific situation where this is not the right thing, 37 | you can build a custom plugin that does exactly what you need. 38 | -------------------------------------------------------------------------------- /development/documentation.rst: -------------------------------------------------------------------------------- 1 | Building the Documentation 2 | ========================== 3 | 4 | We build the documentation with Sphinx. You could install it on your system or use Docker. 5 | 6 | 7 | Install Sphinx 8 | -------------- 9 | 10 | 11 | Install on Local Machine 12 | ~~~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | The installation for Sphinx differs between system. See `Sphinx installation page`_ for details. When Sphinx is 15 | installed you need to `install enchant`_ (e.g. ``sudo apt-get install enchant``). 16 | 17 | 18 | Using Docker 19 | ~~~~~~~~~~~~ 20 | 21 | If you are using docker. Run the following commands from the repository root. 22 | 23 | .. code-block:: bash 24 | 25 | $ docker run --rm -t -v "$PWD":/doc webplates/readthedocs build 26 | $ docker run --rm -t -v "$PWD":/doc webplates/readthedocs check 27 | 28 | Alternatively you can run the make commands as well: 29 | 30 | .. code-block:: bash 31 | 32 | $ docker run --rm -t -v "$PWD":/doc webplates/readthedocs make html 33 | $ docker run --rm -t -v "$PWD":/doc webplates/readthedocs make spelling 34 | 35 | To automatically rebuild the documentation upon change run: 36 | 37 | .. code-block:: bash 38 | 39 | $ docker run --rm -t -v "$PWD":/doc webplates/readthedocs watch 40 | 41 | For more details see the `readthedocs image`_ documentation. 42 | 43 | 44 | Build Documentation 45 | ------------------- 46 | 47 | Before building the documentation make sure to install all requirements. 48 | 49 | .. code-block:: bash 50 | 51 | $ pip install -r requirements.txt 52 | 53 | To build the docs: 54 | 55 | .. code-block:: bash 56 | 57 | $ make html 58 | $ make spelling 59 | 60 | 61 | .. _Sphinx installation page: http://sphinx-doc.org/latest/install.html 62 | .. _install enchant: http://www.abisource.com/projects/enchant/ 63 | .. _readthedocs image: https://hub.docker.com/r/webplates/readthedocs/ 64 | -------------------------------------------------------------------------------- /clients/curl-client.rst: -------------------------------------------------------------------------------- 1 | cURL Client 2 | =========== 3 | 4 | This client uses `cURL PHP extension `_. 5 | 6 | Installation 7 | ------------ 8 | 9 | To install the cURL client, run: 10 | 11 | .. code-block:: bash 12 | 13 | $ composer require php-http/curl-client 14 | 15 | Usage 16 | ----- 17 | 18 | The cURL client needs a PSR-17 message factory and stream factory to work. 19 | You can either specify the factory explicitly:: 20 | 21 | use Http\Client\Curl\Client; 22 | use Http\Message\MessageFactory\DiactorosMessageFactory; 23 | use Http\Message\StreamFactory\DiactorosStreamFactory; 24 | 25 | $client = new Client(new DiactorosMessageFactory(), new DiactorosStreamFactory()); 26 | 27 | Or you can omit the parameters and let the client use :doc:`../discovery` to 28 | determine a suitable factory:: 29 | 30 | use Http\Client\Curl\Client; 31 | use Http\Discovery\MessageFactoryDiscovery; 32 | use Http\Discovery\StreamFactoryDiscovery; 33 | 34 | $client = new Client(); 35 | 36 | Configuring Client 37 | ------------------ 38 | 39 | You can use `cURL options `_ to configure Client:: 40 | 41 | use Http\Client\Curl\Client; 42 | use Http\Discovery\MessageFactoryDiscovery; 43 | use Http\Discovery\StreamFactoryDiscovery; 44 | 45 | $options = [ 46 | CURLOPT_CONNECTTIMEOUT => 10, // The number of seconds to wait while trying to connect. 47 | ]; 48 | $client = new Client(null, null, $options); 49 | 50 | The following options can not be changed in the set up. Most of them are to be provided with the 51 | request instead: 52 | 53 | * CURLOPT_CUSTOMREQUEST 54 | * CURLOPT_FOLLOWLOCATION 55 | * CURLOPT_HEADER 56 | * CURLOPT_HTTP_VERSION 57 | * CURLOPT_HTTPHEADER 58 | * CURLOPT_NOBODY 59 | * CURLOPT_POSTFIELDS 60 | * CURLOPT_RETURNTRANSFER 61 | * CURLOPT_URL 62 | * CURLOPT_USERPWD 63 | 64 | .. include:: includes/further-reading-async.inc 65 | -------------------------------------------------------------------------------- /clients/buzz-adapter.rst: -------------------------------------------------------------------------------- 1 | Buzz Adapter (deprecated) 2 | ========================= 3 | 4 | This adapter only implements the PHP-HTTP synchronous interface. This interface 5 | has been superseded by PSR-18, which the `Buzz HTTP client`_ implements 6 | directly. 7 | 8 | Installation 9 | ------------ 10 | 11 | To install the Buzz adapter, which will also install Buzz itself (if it was 12 | not yet included in your project), run: 13 | 14 | .. code-block:: bash 15 | 16 | $ composer require php-http/buzz-adapter 17 | 18 | Usage 19 | ----- 20 | 21 | Begin by creating a Buzz client, you may pass any listener or configuration parameters to it 22 | like:: 23 | 24 | use Buzz\Browser; 25 | use Buzz\Client\Curl; 26 | use Buzz\Listener\CookieListener; 27 | 28 | $browser = new Browser(); 29 | 30 | $client = new Curl(); 31 | $client->setMaxRedirects(0); 32 | $browser->setClient($client); 33 | 34 | // Create CookieListener 35 | $listener = new CookieListener(); 36 | $browser->addListener($listener); 37 | 38 | Then create the adapter:: 39 | 40 | use Http\Adapter\Buzz\Client as BuzzAdapter; 41 | use Http\Message\MessageFactory\GuzzleMessageFactory; 42 | 43 | $adapter = new BuzzAdapter($browser, new GuzzleMessageFactory()); 44 | 45 | Or relying on :doc:`discovery `:: 46 | 47 | use Http\Adapter\Buzz\Client as BuzzAdapter; 48 | 49 | $adapter = new BuzzAdapter($browser); 50 | 51 | Be Aware 52 | -------- 53 | 54 | This adapter violates the Liskov substitution principle in a rare edge case. When the adapter is configured to use 55 | Buzz' Curl client, it does not send request bodies for request methods such as GET, HEAD and TRACE. A ``RequestException`` 56 | will be thrown if this ever happens. 57 | 58 | If you need GET request with a body (e.g. for Elasticsearch) you need to use the Buzz FileGetContents client or choose a different HTTPlug client like Guzzle 6. 59 | 60 | .. include:: includes/further-reading-sync.inc 61 | 62 | .. _Buzz HTTP client: https://github.com/kriswallsmith/Buzz 63 | -------------------------------------------------------------------------------- /plugins/error.rst: -------------------------------------------------------------------------------- 1 | Error Plugin 2 | ============ 3 | 4 | The ``ErrorPlugin`` transforms responses with HTTP error status codes into exceptions: 5 | 6 | * 400-499 status code are transformed into ``Http\Client\Common\Exception\ClientErrorException``; 7 | * 500-599 status code are transformed into ``Http\Client\Common\Exception\ServerErrorException`` 8 | 9 | .. warning:: 10 | 11 | Throwing an exception on a valid response violates the PSR-18 specification. 12 | This plugin is provided as a convenience when writing a small application. 13 | When providing a client to a third party library, this plugin must not be 14 | included, or the third party library will have problems with error handling. 15 | 16 | Both exceptions extend the ``Http\Client\Exception\HttpException`` class, so you can fetch the request 17 | and the response coming from them:: 18 | 19 | use Http\Discovery\HttpClientDiscovery; 20 | use Http\Client\Common\PluginClient; 21 | use Http\Client\Common\Plugin\ErrorPlugin; 22 | use Http\Client\Common\Exception\ClientErrorException; 23 | 24 | $errorPlugin = new ErrorPlugin(); 25 | 26 | $pluginClient = new PluginClient( 27 | HttpClientDiscovery::find(), 28 | [$errorPlugin] 29 | ); 30 | 31 | ... 32 | 33 | try { 34 | $response = $pluginClient->sendRequest($request); 35 | } catch (ClientErrorException $e) { 36 | if ($e->getResponse()->getStatusCode() == 404) { 37 | // Something has not been found 38 | } 39 | } 40 | 41 | The error plugin is intended for when an application operates with the client directly. When 42 | writing a library around an API, the best practice is to have the client convert responses into 43 | domain objects, and transform HTTP errors into meaningful domain exceptions. In that scenario, 44 | the ErrorPlugin is not needed. It is more efficient to check the HTTP status codes yourself than 45 | throwing and catching exceptions. 46 | 47 | If your application handles responses with 4xx status codes, but needs exceptions for 5xx status codes only, 48 | you can set the option ``only_server_exception`` to ``true``:: 49 | 50 | $errorPlugin = new ErrorPlugin(['only_server_exception' => true]); 51 | -------------------------------------------------------------------------------- /clients/react-adapter.rst: -------------------------------------------------------------------------------- 1 | React Adapter 2 | ============= 3 | 4 | An HTTPlug adapter for the `React Http client`_. 5 | 6 | Installation 7 | ------------ 8 | 9 | Use Composer_ to install the React adapter. It will also install React as it's a 10 | dependency. 11 | 12 | .. code-block:: bash 13 | 14 | $ composer require php-http/react-adapter 15 | 16 | Usage 17 | ----- 18 | 19 | If you need control on the React instances, you can inject them during 20 | initialization:: 21 | 22 | use Http\Adapter\React\Client; 23 | 24 | $systemDnsConfig = React\Dns\Config\Config::loadSystemConfigBlocking(); 25 | if (!$config->nameservers) { 26 | $config->nameservers[] = '8.8.8.8'; 27 | } 28 | 29 | $dnsResolverFactory = new React\Dns\Resolver\Factory(); 30 | $dnsResolver = $factory->create($config); 31 | 32 | $connector = new React\Socket\Connector([ 33 | 'dns' => $dnsResolver, 34 | ]); 35 | $browser = new React\Http\Browser($connector); 36 | 37 | $adapter = new Client($browser); 38 | 39 | You can also use a ``ReactFactory`` in order to initialize React instances:: 40 | 41 | use Http\Adapter\React\ReactFactory; 42 | 43 | $reactHttp = ReactFactory::buildHttpClient(); 44 | 45 | Then you can use the adapter to send synchronous requests:: 46 | 47 | use GuzzleHttp\Psr7\Request; 48 | 49 | $request = new Request('GET', 'http://httpbin.org'); 50 | 51 | // Returns a Psr\Http\Message\ResponseInterface 52 | $response = $adapter->sendRequest($request); 53 | 54 | Or send asynchronous ones:: 55 | 56 | use GuzzleHttp\Psr7\Request; 57 | 58 | $request = new Request('GET', 'http://httpbin.org'); 59 | 60 | // Returns a Http\Promise\Promise 61 | $promise = $adapter->sendAsyncRequest(request); 62 | 63 | Note that since v4 calling `wait` on `Http\Promise\Promise` is expected to run inside a fiber:: 64 | 65 | use function React\Async\async; 66 | 67 | async(static function () { 68 | // Returns a Http\Promise\Promise 69 | $promise = $adapter->sendAsyncRequest(request); 70 | 71 | // Returns a Psr\Http\Message\ResponseInterface 72 | $response = $promise->wait(); 73 | })(); 74 | 75 | .. include:: includes/further-reading-async.inc 76 | 77 | .. _React HTTP client: https://github.com/reactphp/http 78 | -------------------------------------------------------------------------------- /clients/guzzle6-adapter.rst: -------------------------------------------------------------------------------- 1 | Guzzle 6 Adapter (deprecated) 2 | ============================= 3 | 4 | An HTTPlug adapter for the `Guzzle 6 HTTP client`_. 5 | 6 | Guzzle 5 is `not maintained anymore`_. We recommend to upgrade to Guzzle version 7. 7 | 8 | Installation 9 | ------------ 10 | 11 | To install the Guzzle adapter, which will also install Guzzle itself (if it was 12 | not yet included in your project), run: 13 | 14 | .. code-block:: bash 15 | 16 | $ composer require php-http/guzzle6-adapter 17 | 18 | Usage 19 | ----- 20 | 21 | To create a Guzzle6 adapter you should use the `createWithConfig()` function. It will let you to pass Guzzle configuration 22 | to the client:: 23 | 24 | use Http\Adapter\Guzzle6\Client as GuzzleAdapter; 25 | 26 | $config = [ 27 | 'timeout' => 2, 28 | 'handler' => //... 29 | // ... 30 | ]; 31 | $adapter = GuzzleAdapter::createWithConfig($config); 32 | 33 | .. note:: 34 | 35 | If you want even more control over your Guzzle object, you may give a Guzzle client as first argument to the adapter's 36 | constructor:: 37 | 38 | use GuzzleHttp\Client as GuzzleClient; 39 | use Http\Adapter\Guzzle6\Client as GuzzleAdapter; 40 | 41 | $config = ['timeout' => 5]; 42 | // ... 43 | $guzzle = new GuzzleClient($config); 44 | // ... 45 | $adapter = new GuzzleAdapter($guzzle); 46 | 47 | If you pass a Guzzle instance to the adapter, make sure to configure Guzzle to not throw exceptions on HTTP error status codes, or this adapter will violate PSR-18. 48 | 49 | And use it to send synchronous requests:: 50 | 51 | use GuzzleHttp\Psr7\Request; 52 | 53 | $request = new Request('GET', 'http://httpbin.org'); 54 | 55 | // Returns a Psr\Http\Message\ResponseInterface 56 | $response = $adapter->sendRequest($request); 57 | 58 | Or send asynchronous ones:: 59 | 60 | use GuzzleHttp\Psr7\Request; 61 | 62 | $request = new Request('GET', 'http://httpbin.org'); 63 | 64 | // Returns a Http\Promise\Promise 65 | $promise = $adapter->sendAsyncRequest(request); 66 | 67 | 68 | .. include:: includes/further-reading-async.inc 69 | 70 | .. _Guzzle 6 HTTP client: http://docs.guzzlephp.org/en/6.5/ 71 | .. _not maintained anymore: https://github.com/guzzle/guzzle#version-guidance 72 | -------------------------------------------------------------------------------- /clients/guzzle7-adapter.rst: -------------------------------------------------------------------------------- 1 | Guzzle 7 Adapter 2 | ================ 3 | 4 | An HTTPlug adapter for the `Guzzle 7 HTTP client`_. Guzzle 7 supports PSR-18 5 | out of the box. This adapter makes sense if you want to use HTTPlug async 6 | interface or to use Guzzle 7 with a library that did not upgrade to PSR-18 yet 7 | and depends on ``php-http/client-implementation``. 8 | 9 | Installation 10 | ------------ 11 | 12 | To install the Guzzle adapter, which will also install Guzzle itself (if it was 13 | not yet included in your project), run: 14 | 15 | .. code-block:: bash 16 | 17 | $ composer require php-http/guzzle7-adapter 18 | 19 | Usage 20 | ----- 21 | 22 | To create a Guzzle7 adapter you should use the `createWithConfig()` function. It will let you to pass Guzzle configuration 23 | to the client:: 24 | 25 | use Http\Adapter\Guzzle7\Client as GuzzleAdapter; 26 | 27 | $config = [ 28 | 'timeout' => 2, 29 | 'handler' => //... 30 | // ... 31 | ]; 32 | $adapter = GuzzleAdapter::createWithConfig($config); 33 | 34 | .. note:: 35 | 36 | If you want even more control over your Guzzle object, you may give a Guzzle client as first argument to the adapter's 37 | constructor:: 38 | 39 | use GuzzleHttp\Client as GuzzleClient; 40 | use Http\Adapter\Guzzle7\Client as GuzzleAdapter; 41 | 42 | $config = ['timeout' => 5]; 43 | // ... 44 | $guzzle = new GuzzleClient($config); 45 | // ... 46 | $adapter = new GuzzleAdapter($guzzle); 47 | 48 | If you pass a Guzzle instance to the adapter, make sure to configure Guzzle to not throw exceptions on HTTP error status codes, or this adapter will violate PSR-18. 49 | 50 | And use it to send synchronous requests:: 51 | 52 | use GuzzleHttp\Psr7\Request; 53 | 54 | $request = new Request('GET', 'http://httpbin.org'); 55 | 56 | // Returns a Psr\Http\Message\ResponseInterface 57 | $response = $adapter->sendRequest($request); 58 | 59 | Or send asynchronous ones:: 60 | 61 | use GuzzleHttp\Psr7\Request; 62 | 63 | $request = new Request('GET', 'http://httpbin.org'); 64 | 65 | // Returns a Http\Promise\Promise 66 | $promise = $adapter->sendAsyncRequest(request); 67 | 68 | 69 | .. include:: includes/further-reading-async.inc 70 | 71 | .. _Guzzle 7 HTTP client: http://docs.guzzlephp.org/en/7.0/ 72 | -------------------------------------------------------------------------------- /development/code-of-conduct.rst: -------------------------------------------------------------------------------- 1 | Contributor Code of Conduct 2 | =========================== 3 | 4 | As contributors and maintainers of this project, and in the interest of 5 | fostering an open and welcoming community, we pledge to respect all people who 6 | contribute through reporting issues, posting feature requests, updating 7 | documentation, submitting pull requests or patches, and other activities. 8 | 9 | We are committed to making participation in this project a harassment-free 10 | experience for everyone, regardless of level of experience, gender, gender 11 | identity and expression, sexual orientation, disability, personal appearance, 12 | body size, race, ethnicity, age, religion, or nationality. 13 | 14 | Examples of unacceptable behavior by participants include: 15 | 16 | * The use of sexualized language or imagery 17 | * Personal attacks 18 | * Trolling or insulting/derogatory comments 19 | * Public or private harassment 20 | * Publishing other's private information, such as physical or electronic 21 | addresses, without explicit permission 22 | * Other unethical or unprofessional conduct 23 | 24 | Project maintainers have the right and responsibility to remove, edit, or 25 | reject comments, commits, code, wiki edits, issues, and other contributions 26 | that are not aligned to this Code of Conduct, or to ban temporarily or 27 | permanently any contributor for other behaviors that they deem inappropriate, 28 | threatening, offensive, or harmful. 29 | 30 | By adopting this Code of Conduct, project maintainers commit themselves to 31 | fairly and consistently applying these principles to every aspect of managing 32 | this project. Project maintainers who do not follow or enforce the Code of 33 | Conduct may be permanently removed from the project team. 34 | 35 | This Code of Conduct applies both within project spaces and in public spaces 36 | when an individual is representing the project or its community. 37 | 38 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 39 | reported by contacting a project maintainer at team@php-http.org. All 40 | complaints will be reviewed and investigated and will result in a response that 41 | is deemed necessary and appropriate to the circumstances. Maintainers are 42 | obligated to maintain confidentiality with regard to the reporter of an 43 | incident. 44 | 45 | 46 | This Code of Conduct is adapted from the `Contributor Covenant`_, 47 | version 1.3.0, available at http://contributor-covenant.org/version/1/3/0/ 48 | 49 | .. _Contributor Covenant: http://contributor-covenant.org 50 | -------------------------------------------------------------------------------- /components/multipart-stream-builder.rst: -------------------------------------------------------------------------------- 1 | Multipart Stream Builder 2 | ======================== 3 | 4 | A multipart stream is a special kind of stream that is used to transfer files over HTTP. There is currently no PSR-7 support for multipart streams as they are considered to be normal streams with a special content. A multipart stream HTTP request may look like this: 5 | 6 | .. code-block:: none 7 | 8 | POST / HTTP/1.1 9 | Host: example.com 10 | Content-Type: multipart/form-data; boundary="578de3b0e3c46.2334ba3" 11 | 12 | --578de3b0e3c46.2334ba3 13 | Content-Disposition: form-data; name="foo" 14 | Content-Length: 15 15 | 16 | A normal stream 17 | --578de3b0e3c46.2334ba3 18 | Content-Disposition: form-data; name="bar"; filename="bar.png" 19 | Content-Length: 71 20 | Content-Type: image/png 21 | 22 | ?PNG 23 |  24 | ??? 25 | IHDR??? ??? ?????? ???? IDATxc???51?)?:??????IEND?B`? 26 | --578de3b0e3c46.2334ba3 27 | Content-Type: text/plain 28 | Content-Disposition: form-data; name="baz" 29 | Content-Length: 6 30 | 31 | string 32 | --578de3b0e3c46.2334ba3-- 33 | 34 | 35 | In the request above you see a set of HTTP headers and a body with two streams. The body starts and ends with a "boundary" and it is also this boundary that separates the streams. That boundary also needs to be specified in the ``Content-Type`` header. 36 | 37 | Building a Multipart Stream 38 | ``````````````````````````` 39 | 40 | To build a multipart stream you may use the ``MultipartStreamBuilder``. It is not coupled to any stream implementation so it needs a ``StreamFactory`` to create the streams. 41 | 42 | .. code-block:: php 43 | 44 | $streamFactory = StreamFactoryDiscovery::find(); 45 | $builder = new MultipartStreamBuilder($streamFactory); 46 | $builder 47 | ->addResource('foo', $stream) 48 | ->addResource('bar', fopen($filePath, 'r'), ['filename' => 'bar.png']) 49 | ->addData('baz', ['headers' => ['Content-Type' => 'text/plain']]); 50 | 51 | $multipartStream = $builder->build(); 52 | $boundary = $builder->getBoundary(); 53 | 54 | $request = MessageFactoryDiscovery::find()->createRequest( 55 | 'POST', 56 | 'http://example.com', 57 | ['Content-Type' => 'multipart/form-data; boundary="'.$boundary.'"'], 58 | $multipartStream 59 | ); 60 | 61 | $response = HttpClientDiscovery::find()->sendRequest($request); 62 | 63 | The second parameter of ``MultipartStreamBuilder::addResource()`` is the content of the stream. The supported input is the same as ``StreamFactory::createStream()``. 64 | -------------------------------------------------------------------------------- /plugins/retry.rst: -------------------------------------------------------------------------------- 1 | Retry Plugin 2 | ============ 3 | 4 | The ``RetryPlugin`` can automatically attempt to re-send a request that failed, 5 | to work around unreliable connections and servers. It re-sends the request when 6 | an exception is thrown, unless the exception is a HttpException for a status 7 | code in the 5xx server error range. Since version 2.0, responses with status 8 | codes in the 5xx range are also retried. Each retry attempt is delayed by an 9 | exponential backoff time. 10 | 11 | See below for how to configure that behavior. 12 | 13 | .. warning:: 14 | 15 | You should keep an eye on retried requests, as they add overhead. If a 16 | request fails due to a client side mistake, retrying is only a waste of 17 | time and resources. 18 | 19 | Contrary to the :doc:`redirect`, the retry plugin does not restart the chain 20 | but simply tries again from the current position. 21 | 22 | Async 23 | ----- 24 | 25 | This plugin is not fully compatible with asynchronous behavior, as the wait 26 | between retries is done with a blocking call to a sleep function. 27 | 28 | Options 29 | ------- 30 | 31 | ``retries``: int (default: 1) 32 | 33 | Number of retry attempts to make before giving up. 34 | 35 | ``error_response_decider``: callable (default behavior: retry if status code is in 5xx range) 36 | 37 | A callback function that receives the request and response to decide whether the 38 | request should be retried. 39 | 40 | ``exception_decider``: callable (default behavior: retry if the exception is not an HttpException or status code is in 5xx range) 41 | 42 | A callback function that receives a request and an exception to decide after a 43 | failure whether the request should be retried. 44 | 45 | ``error_response_delay``: callable (default behavior: exponential backoff) 46 | 47 | A callback that receives a request, a response, the current number of retries 48 | and returns how many microseconds we should wait before trying again. 49 | 50 | ``exception_delay``: callable (default behavior: exponential backoff) 51 | 52 | A callback that receives a request, an exception, the current number of retries 53 | and returns how many microseconds we should wait before trying again. 54 | 55 | Interaction with Exceptions 56 | --------------------------- 57 | 58 | If you use the :doc:`ErrorPlugin `, you should place it after the RetryPlugin in the 59 | plugin chain:: 60 | 61 | use Http\Discovery\HttpClientDiscovery; 62 | use Http\Client\Common\PluginClient; 63 | use Http\Client\Common\Plugin\ErrorPlugin; 64 | use Http\Client\Common\Plugin\RetryPlugin; 65 | 66 | $pluginClient = new PluginClient( 67 | HttpClientDiscovery::find(), 68 | [ 69 | new RetryPlugin(), 70 | new ErrorPlugin(), 71 | ] 72 | ); 73 | -------------------------------------------------------------------------------- /httplug/introduction.rst: -------------------------------------------------------------------------------- 1 | HTTPlug: HTTP client abstraction 2 | ================================ 3 | 4 | HTTPlug allows you to write reusable libraries and applications that need 5 | an HTTP client without binding to a specific implementation. 6 | When all packages used in an application only specify HTTPlug, 7 | the application developers can choose the client that best fits their project 8 | and use the same client with all packages. 9 | 10 | Client Interfaces 11 | ----------------- 12 | 13 | HTTPlug defines two HTTP client interfaces that we kept as simple as possible: 14 | 15 | * PSR-18 defines the ``ClientInterface`` with a ``sendRequest`` method that 16 | accepts a PSR-7 ``RequestInterface`` and either returns a PSR-7 17 | ``ResponseInterface`` or throws an exception that implements 18 | ``Psr\Http\Client\ClientExceptionInterface``. 19 | 20 | HTTPlug has the compatible interface ``HttpClient`` which now extends the 21 | PSR-18 interface to allow migrating to PSR-18. 22 | 23 | * ``HttpAsyncClient`` defines a ``sendAsyncRequest`` method that sends a PSR-7 24 | request asynchronously and always returns a ``Http\Client\Promise``. 25 | See :doc:`../components/promise` for more information. 26 | 27 | Implementations 28 | --------------- 29 | 30 | PHP-HTTP offers two types of clients that implement the above interfaces: 31 | 32 | 1. Standalone clients that directly implement the interfaces. 33 | 34 | Examples: :doc:`/clients/curl-client` and :doc:`/clients/socket-client`. 35 | 36 | 2. Adapters that wrap existing HTTP clients, such as Guzzle. These adapters act 37 | as a bridge between the HTTPlug interfaces and the clients that do not (yet) 38 | implement these interfaces. 39 | 40 | More and more clients implement PSR-18 directly. If that is all you need, we 41 | recommend not using HTTPlug as it would only add overhead. However, as there 42 | is no PSR for asynchronous requests yet, you can use the adapters to do such 43 | requests without binding yourself to a specific implementation. 44 | 45 | Examples: :doc:`/clients/guzzle7-adapter` and :doc:`/clients/react-adapter`. 46 | 47 | .. note:: 48 | 49 | Ideally, there will be a PSR for asynchronous requests and all HTTP client 50 | libraries out there will implement PSR-18 and the not yet existing PSR. At 51 | that point, our adapters will no longer be necessary. 52 | 53 | Usage 54 | ----- 55 | 56 | There are two main use cases for HTTPlug: 57 | 58 | * Usage in an application that executes HTTP requests (see :doc:`tutorial` and :doc:`../integrations/index`); 59 | * Usage in a reusable package that executes HTTP requests (see :doc:`library-developers`). 60 | 61 | History 62 | ------- 63 | 64 | This project has been started by `Eric Geloen`_ as `Ivory Http Adapter`_. It 65 | never made it to a stable release, but it relied on PSR-7 which was not stable 66 | either that time. Because of the constantly changing PSR-7, Eric had to rewrite 67 | the library over and over again (at least the message handling part, which in 68 | most cases affected every adapter as well). 69 | 70 | In 2015, a decision has been made to move the library to its own organization, 71 | so PHP-HTTP was born. 72 | 73 | See :doc:`migrating` for a guide how to migrate your code from the Ivory 74 | adapter. 75 | 76 | .. _`Eric Geloen`: https://github.com/egeloen 77 | .. _`Ivory Http Adapter`: https://github.com/egeloen/ivory-http-adapter 78 | -------------------------------------------------------------------------------- /plugins/redirect.rst: -------------------------------------------------------------------------------- 1 | Redirect Plugin 2 | =============== 3 | 4 | The ``RedirectPlugin`` automatically follows redirection answers from a server. If the plugin 5 | detects a redirection, it creates a request to the target URL and restarts the plugin chain. 6 | 7 | The plugin attempts to detect circular redirects and will abort when such a redirect is 8 | encountered. Note that a faulty server appending something on each request is not detected. This 9 | situation is caught by the plugin client itself and can be controlled through the 10 | :ref:`plugin-client.max-restarts` setting. 11 | 12 | Initiate the redirect plugin as follows:: 13 | 14 | use Http\Discovery\HttpClientDiscovery; 15 | use Http\Client\Common\PluginClient; 16 | use Http\Client\Common\Plugin\RedirectPlugin; 17 | 18 | $redirectPlugin = new RedirectPlugin(); 19 | 20 | $pluginClient = new PluginClient( 21 | HttpClientDiscovery::find(), 22 | [$redirectPlugin] 23 | ); 24 | 25 | .. warning:: 26 | 27 | Following redirects can increase the robustness of your application. But if you build some sort 28 | of API client, you want to at least keep an eye on the log files. Having your application 29 | follow redirects instead of going to the right end point directly makes your application slower 30 | and increases the load on both server and client. 31 | 32 | .. note:: 33 | 34 | Depending on the status code, redirecting should change POST/PUT requests to GET requests. This 35 | plugin implements this behavior - except if you set the ``strict`` option to true, as explained 36 | below. It removes the request body if the method changes, see ``stream_factory`` below. 37 | 38 | To understand the exact semantics of which HTTP status changes the method and which not, have a 39 | look at the configuration in the source code of the RedirectPlugin class. 40 | 41 | Options 42 | ------- 43 | 44 | ``preserve_header``: boolean|string[] (default: true) 45 | 46 | When set to ``true``, all headers are kept for the next request. ``false`` means all headers are 47 | removed. An array of strings is treated as a whitelist of header names to keep from the original 48 | request. 49 | 50 | ``use_default_for_multiple``: bool (default: true) 51 | 52 | Whether to follow the default direction on the multiple redirection status code 300. If set to 53 | false, a status of 300 will raise the ``Http\Client\Common\Exception\MultipleRedirectionException``. 54 | 55 | ``strict``: bool (default: false) 56 | 57 | When set to ``true``, 300, 301 and 302 status codes will not modify original request's method and 58 | body on consecutive requests. E. g. POST redirect requests are sent as POST requests instead of 59 | POST redirect requests are sent as GET requests. 60 | 61 | ``stream_factory``: StreamFactoryInterface (default: auto discovered) 62 | 63 | The PSR-17 stream factory is used to create an empty stream for removing the body of the request on 64 | redirection. To keep the body on all redirections, set ``stream_factory`` to null. 65 | The stream factory is discovered if either ``php-http/discovery`` is installed and provides a 66 | factory, or ``nyholm/psr7`` or a new enough version of ``guzzlehttp/psr7`` are installed. If you 67 | only have other implementations, you need to provide the factory in ``stream_factory``. 68 | 69 | If no factory is found, the redirect plugin does not remove the body on redirection. 70 | -------------------------------------------------------------------------------- /httplug/users.rst: -------------------------------------------------------------------------------- 1 | HTTPlug for library users 2 | ========================= 3 | 4 | This page explains how to set up a library that depends on HTTPlug. 5 | 6 | TL;DR 7 | ----- 8 | 9 | For the impatient: Require the following packages before requiring the library 10 | you plan to use: 11 | 12 | .. code-block:: bash 13 | 14 | composer require php-http/curl-client guzzlehttp/psr7 php-http/message 15 | 16 | If you use a framework, check the :doc:`integrations <../integrations/index>` 17 | overview to see if there is a plugin for your framework. 18 | 19 | Details 20 | ------- 21 | 22 | If a library depends on HTTPlug, it requires the virtual package 23 | `php-http/client-implementation`_. A virtual package is used to declare that 24 | the library needs *an* implementation of the HTTPlug interfaces, but does not 25 | care which implementation specifically. 26 | 27 | When using such a library, you need to choose a HTTPlug client and include that 28 | in your project explicitly. Lets say you want to use ``some/awesome-library`` 29 | that depends on ``php-http/client implementation``. In the example we are using cURL: 30 | 31 | .. code-block:: bash 32 | 33 | $ composer require php-http/curl-client some/awesome-library 34 | 35 | You can pick any of the clients or adapters :doc:`provided by PHP-HTTP `. 36 | Popular choices are ``php-http/curl-client`` and ``php-http/guzzle6-adapter``. 37 | 38 | Many libraries also need a PSR-7 implementation and the PHP-HTTP message 39 | factories to create messages. The PSR-7 implementations are Laminas Diactoros (also still supports the abandoned Zend Diactoros), Guzzle's PSR-7 and Slim Framework's PSR-7 messages. Do one of the following: 40 | 41 | .. code-block:: bash 42 | 43 | $ composer require php-http/message laminas/laminas-diactoros 44 | 45 | .. code-block:: bash 46 | 47 | $ composer require php-http/message guzzlehttp/psr7 48 | 49 | .. code-block:: bash 50 | 51 | $ composer require php-http/message slim/psr7 52 | 53 | Troubleshooting 54 | --------------- 55 | 56 | Composer fails 57 | `````````````` 58 | 59 | If you try to include the HTTPlug dependent library before you have included a 60 | HTTP client in your project, Composer will throw an error: 61 | 62 | .. code-block:: none 63 | 64 | Loading composer repositories with package information 65 | Updating dependencies (including require-dev) 66 | Your requirements could not be resolved to an installable set of packages. 67 | 68 | Problem 1 69 | - The requested package php-http/client-implementation could not be found in any version, 70 | there may be a typo in the package name. 71 | 72 | You can solve this by including a HTTP client or adapter, as described above. 73 | 74 | No Message Factories 75 | ````````````````````` 76 | 77 | You may get an exception telling you that "No message factories found", this 78 | means that either you have not installed a PSR-7 implementation or that there 79 | are no factories installed to create HTTP messages. 80 | 81 | .. code-block:: none 82 | 83 | No message factories found. To use Guzzle or Diactoros factories install 84 | php-http/message and the chosen message implementation. 85 | 86 | You can solve this by including ``php-http/message`` and Zend Diactoros or 87 | Guzzle PSR-7, as described above. 88 | 89 | Background 90 | ---------- 91 | 92 | Reusable libraries do not depend on a concrete implementation but only on the virtual package 93 | ``php-http/client-implementation``. This is to avoid hard coupling and allows the user of the 94 | library to choose the implementation. You can think of this as an "interface" or "contract" for packages. 95 | 96 | The reusable libraries have no hard coupling to the PSR-7 implementation either, which gives you the flexibility to 97 | choose an implementation yourself. 98 | 99 | .. _`php-http/client-implementation`: https://packagist.org/providers/php-http/client-implementation 100 | -------------------------------------------------------------------------------- /httplug/library-developers.rst: -------------------------------------------------------------------------------- 1 | HTTPlug for Library Developers 2 | ============================== 3 | 4 | If you’re developing a library or framework that performs HTTP requests, you 5 | should not be dependent on concrete HTTP client libraries (such as Guzzle). 6 | Instead, you should only make sure that *some* HTTP client is available. It is 7 | then up to your users to decide which HTTP client they want to include in their 8 | projects. This complies with the `dependency inversion principle`_. 9 | 10 | Manage Dependencies 11 | ------------------- 12 | 13 | To depend on *some* HTTP client, specify either 14 | ``psr/http-client-implementation`` for PSR-18 synchronous requests or 15 | ``php-http/async-client-implementation`` for asynchronous requests 16 | in your library’s ``composer.json``. These are virtual Composer packages that 17 | will throw an error if no concrete client was found: 18 | 19 | .. code-block:: json 20 | 21 | { 22 | "name": "you/and-your-awesome-library", 23 | "require": { 24 | "psr/http-client-implementation": "^1.0" 25 | } 26 | } 27 | 28 | Your users then include your project alongside with a HTTP client of their 29 | choosing in their project’s ``composer.json``. In this case, the user decided 30 | to include the Socket client: 31 | 32 | .. code-block:: json 33 | 34 | { 35 | "name": "some-user/nice-project", 36 | "require": { 37 | "you/and-your-awesome-library": "^1.2", 38 | "php-http/socket-client": "^1.0" 39 | } 40 | } 41 | 42 | Testing your library 43 | -------------------- 44 | 45 | When you install your library on a CI-server (like Travis) you need to include a client. So specify any concrete client 46 | in the ``require-dev`` section in your library’s ``composer.json``. You could use any client but the 47 | :doc:`/clients/mock-client` will make it easier to write good tests. 48 | 49 | .. code-block:: json 50 | 51 | { 52 | "name": "you/and-your-awesome-library", 53 | "require": { 54 | "psr/http-client-implementation": "^1.0" 55 | }, 56 | "require-dev": { 57 | "php-http/mock-client": "^1.0" 58 | } 59 | } 60 | 61 | 62 | 63 | Messages 64 | -------- 65 | 66 | When you construct HTTP message objects in your library, you should not depend on a concrete PSR-7 message 67 | implementation. Instead, use the :doc:`HTTP factories <../message/message-factory>`. 68 | 69 | Discovery 70 | --------- 71 | 72 | To make it as convenient as possible for your users you should use the :doc:`/discovery` component. It will help you 73 | find factories to create ``Request``, ``Streams`` etc. That component is light weight and has no hard dependencies. 74 | 75 | Plugins 76 | ------- 77 | 78 | If your library relies on specific plugins, the recommended way is to provide a factory method for 79 | your users, so they can create the correct client from a base HttpClient. See 80 | :ref:`plugin-client.libraries` for a concrete example. 81 | 82 | User Documentation 83 | ------------------ 84 | 85 | To explain to your users that they need to install a concrete HTTP client, 86 | you can point them to :doc:`users`. 87 | 88 | 89 | Your Final ``composer.json`` 90 | ---------------------------- 91 | 92 | Putting it all together your final ``composer.json`` is much likely to look similar to this: 93 | 94 | .. code-block:: json 95 | 96 | { 97 | "name": "you/and-your-awesome-library", 98 | "require": { 99 | "psr/http-message": "^1.0", 100 | "psr/http-client-implementation": "^1.0", 101 | "php-http/httplug": "^2.0", 102 | "php-http/message-factory": "^1.0", 103 | "php-http/discovery": "^1.0" 104 | }, 105 | "require-dev": { 106 | "php-http/mock-client": "^1.0", 107 | "php-http/message": "^1.0", 108 | "guzzlehttp/psr7": "^1.0" 109 | } 110 | } 111 | 112 | .. _`dependency inversion principle`: https://en.wikipedia.org/wiki/Dependency_inversion_principle 113 | -------------------------------------------------------------------------------- /clients/socket-client.rst: -------------------------------------------------------------------------------- 1 | Socket Client (deprecated) 2 | ========================== 3 | 4 | The socket client uses the stream extension from PHP, which is integrated into 5 | the core. 6 | 7 | This client only implements the PHP-HTTP synchronous interface, which has been 8 | superseded by PSR-18. Use one of the PSR-18 clients instead. 9 | 10 | Features 11 | -------- 12 | 13 | * TCP Socket Domain (``tcp://hostname:port``) 14 | * UNIX Socket Domain (``unix:///path/to/socket.sock``) 15 | * TLS / SSL encryption 16 | * Client Certificate (only for PHP > 5.6) 17 | 18 | Installation 19 | ------------ 20 | 21 | To install the Socket client, run: 22 | 23 | .. code-block:: bash 24 | 25 | $ composer require php-http/socket-client 26 | 27 | This client does not come with a PSR-7 implementation out of the box, so you have 28 | to install one as well (for example `Guzzle PSR-7`_): 29 | 30 | .. code-block:: bash 31 | 32 | $ composer require guzzlehttp/psr7 33 | 34 | In order to provide full interoperability, message implementations are accessed 35 | through :ref:`factories `. Message factories for 36 | `Laminas Diactoros`_ (and its abandoned predecessor `Zend Diactoros`_), 37 | `Guzzle PSR-7`_ and `Slim PSR-7`_ are available in the 38 | :doc:`message ` component: 39 | 40 | .. code-block:: bash 41 | 42 | $ composer require php-http/message 43 | 44 | 45 | Usage 46 | ----- 47 | 48 | The Socket client needs a :ref:`message factory ` in order to 49 | to work:: 50 | 51 | use Http\Client\Socket\Client; 52 | 53 | $options = []; 54 | $client = new Client($messageFactory, $options); 55 | 56 | The available options are: 57 | 58 | :remote_socket: Specify the remote socket where the library should send the request to 59 | 60 | * Can be a TCP remote: ``tcp://hostname:port`` 61 | * Can be a UNIX remote: ``unix:///path/to/remote.sock`` 62 | * Do not use a TLS/SSL scheme, this is handle by the SSL option. 63 | * If not set, the client will try to determine it from the request URI or host header. 64 | :timeout: Timeout in milliseconds for writing request and reading response on the remote 65 | :ssl: Activate or deactivate SSL/TLS encryption 66 | :stream_context_options: Custom options for the context of the stream. See `PHP stream context options `_. 67 | :stream_context_params: Custom parameters for the context of the stream. See `PHP stream context parameters `_. 68 | :write_buffer_size: When sending the request we need to buffer the body, this option specify the size of this buffer, default is 8192, 69 | if you are sending big file with your client it may be interesting to have a bigger value in order to increase performance. 70 | 71 | As an example someone may want to pass a client certificate when using the ssl, 72 | a valid configuration for this use case would be:: 73 | 74 | use Http\Client\Socket\Client; 75 | 76 | $options = [ 77 | 'stream_context_options' => [ 78 | 'ssl' => [ 79 | 'local_cert' => '/path/to/my/client-certificate.pem' 80 | ] 81 | ] 82 | ]; 83 | $client = new Client($messageFactory, $options); 84 | 85 | .. warning:: 86 | 87 | This client assumes that the request is compliant with HTTP 2.0, 1.1 or 1.0 88 | standard. So a request without a ``Host`` header, or with a body but 89 | without a ``Content-Length`` will certainly fail. To make sure all requests 90 | will be sent out correctly, we recommend to use the ``PluginClient`` with 91 | the following plugins: 92 | 93 | * ``ContentLengthPlugin`` sets the correct ``Content-Length`` header, or decorate the stream to use chunked encoding 94 | * ``DecoderPlugin`` decodes encoding coming from the response (chunked, gzip, deflate and compress) 95 | 96 | :doc:`Read more on plugins ` 97 | 98 | .. include:: includes/further-reading-sync.inc 99 | 100 | .. _Guzzle PSR-7: https://github.com/guzzle/psr7 101 | .. _Laminas Diactoros: https://github.com/laminas/laminas-diactoros 102 | .. _Slim PSR-7: https://github.com/slimphp/Slim-Psr7 103 | .. _Zend Diactoros: https://github.com/zendframework/zend-diactoros 104 | -------------------------------------------------------------------------------- /development/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you're here, you would like to contribute to this project and you're really welcome! 5 | 6 | Bug Reports 7 | ----------- 8 | 9 | If you find a bug or a documentation issue, please report it or even better: fix it :). If you report it, 10 | please be as precise as possible. Here is a little list of required information: 11 | 12 | - Precise description of the bug 13 | - Details of your environment (for example: OS, PHP version, installed extensions) 14 | - Backtrace which might help identifying the bug 15 | 16 | 17 | Security Issues 18 | --------------- 19 | 20 | If you discover any security related issues, 21 | please contact us at security@php-http.org instead of submitting an issue on GitHub. 22 | This allows us to fix the issue and release a security hotfix without publicly disclosing the vulnerability. 23 | 24 | 25 | Feature Requests 26 | ---------------- 27 | 28 | If you think a feature is missing, please report it or even better: implement it :). If you report it, describe the more 29 | precisely what you would like to see implemented and we will discuss what is the best approach for it. If you can do 30 | some research before submitting it and link the resources to your description, you're awesome! It will allow us to more 31 | easily understood/implement it. 32 | 33 | 34 | Sending a Pull Request 35 | ---------------------- 36 | 37 | If you're here, you are going to fix a bug or implement a feature and you're the best! 38 | To do it, first fork the repository, clone it and create a new branch with the following commands: 39 | 40 | .. code-block:: bash 41 | 42 | $ git clone git@github.com:your-name/repo-name.git 43 | $ git checkout -b feature-or-bug-fix-description 44 | 45 | Then install the dependencies through Composer_: 46 | 47 | .. code-block:: bash 48 | 49 | $ composer install 50 | 51 | Write code and tests. When you are ready, run the tests. 52 | (This is usually PHPUnit_ or PHPSpec_) 53 | 54 | .. code-block:: bash 55 | 56 | $ composer test 57 | 58 | When you are ready with the code, tested it and documented it, you can commit and push it with the following commands: 59 | 60 | .. code-block:: bash 61 | 62 | $ git commit -m "Feature or bug fix description" 63 | $ git push origin feature-or-bug-fix-description 64 | 65 | .. note:: 66 | 67 | Please write your commit messages in the imperative and follow the 68 | guidelines_ for clear and concise messages. 69 | 70 | Then `create a pull request`_ on GitHub. 71 | 72 | Please make sure that each individual commit in your pull request is meaningful. 73 | If you had to make multiple intermediate commits while developing, 74 | please squash them before submitting with the following commands 75 | (here, we assume you would like to squash 3 commits in a single one): 76 | 77 | .. code-block:: bash 78 | 79 | $ git rebase -i HEAD~3 80 | 81 | If your branch conflicts with the master branch, you will need to rebase and re-push it with the following commands: 82 | 83 | .. code-block:: bash 84 | 85 | $ git remote add upstream git@github.com:orga/repo-name.git 86 | $ git pull --rebase upstream master 87 | $ git push -f origin feature-or-bug-fix-description 88 | 89 | Coding Standard 90 | --------------- 91 | 92 | This repository follows the `PSR-2 standard`_ and so, if you want to contribute, 93 | you must follow these rules. 94 | 95 | 96 | Semver 97 | ------ 98 | 99 | We are trying to follow semver_. When you are making BC breaking changes, 100 | please let us know why you think it is important. 101 | In this case, your patch can only be included in the next major version. 102 | 103 | 104 | Contributor Code of Conduct 105 | --------------------------- 106 | 107 | This project is released with a :doc:`code-of-conduct`. 108 | By participating in this project you agree to abide by its terms. 109 | 110 | License 111 | ------- 112 | 113 | All of our packages are licensed under the :doc:`MIT license `. 114 | 115 | .. _PHPUnit: http://phpunit.de/ 116 | .. _PHPSpec: http://phpspec.net/ 117 | .. _guidelines: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 118 | .. _create a pull request: https://help.github.com/articles/creating-a-pull-request/ 119 | .. _semver: http://semver.org 120 | .. _PSR-2 standard: http://www.php-fig.org/psr/psr-2 121 | -------------------------------------------------------------------------------- /plugins/headers.rst: -------------------------------------------------------------------------------- 1 | Header Plugins 2 | ============== 3 | 4 | Header plugins are useful to manage request headers. Many operations are 5 | possible with the provided plugins. 6 | 7 | Default Headers Values 8 | ---------------------- 9 | 10 | The plugin ``HeaderDefaultsPlugin`` allows you to define default values for 11 | given headers. If a header is not set, it will be added. However, if the header 12 | is already present, the request is left unchanged:: 13 | 14 | use Http\Discovery\HttpClientDiscovery; 15 | use Http\Client\Common\PluginClient; 16 | use Http\Client\Common\Plugin\HeaderDefaultsPlugin; 17 | 18 | $defaultUserAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1'; 19 | 20 | $headerDefaultsPlugin = new HeaderDefaultsPlugin([ 21 | 'User-Agent' => $defaultUserAgent 22 | ]); 23 | 24 | $pluginClient = new PluginClient( 25 | HttpClientDiscovery::find(), 26 | [$headerDefaultsPlugin] 27 | ); 28 | 29 | Mandatory Headers Values 30 | ------------------------ 31 | 32 | The plugin ``HeaderSetPlugin`` allows you to fix values of given headers. That 33 | means that any request passing through this plugin will be set to the specified 34 | value. Existing values of the header will be overwritten. 35 | 36 | .. code:: php 37 | 38 | use Http\Discovery\HttpClientDiscovery; 39 | use Http\Client\Common\PluginClient; 40 | use Http\Client\Common\Plugin\HeaderSetPlugin; 41 | 42 | $userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1'; 43 | 44 | $headerSetPlugin = new HeaderSetPlugin([ 45 | 'User-Agent' => $userAgent, 46 | 'Accept' => 'application/json' 47 | ]); 48 | 49 | $pluginClient = new PluginClient( 50 | HttpClientDiscovery::find(), 51 | [$headerSetPlugin] 52 | ); 53 | 54 | Removing Headers 55 | ---------------- 56 | 57 | The plugin ``HeaderRemovePlugin`` allows you to remove headers from the request. 58 | 59 | .. code:: php 60 | 61 | use Http\Discovery\HttpClientDiscovery; 62 | use Http\Client\Common\PluginClient; 63 | use Http\Client\Common\Plugin\HeaderRemovePlugin; 64 | 65 | $headerRemovePlugin = new HeaderRemovePlugin([ 66 | 'User-Agent' 67 | ]); 68 | 69 | $pluginClient = new PluginClient( 70 | HttpClientDiscovery::find(), 71 | [$headerRemovePlugin] 72 | ); 73 | 74 | Appending Header Values 75 | ----------------------- 76 | 77 | The plugin ``HeaderAppendPlugin`` allows you to add headers. The header will be 78 | created if not existing yet. If the header already exists, the value will be 79 | appended to the list of values for this header. 80 | 81 | .. note:: 82 | 83 | The use cases for this plugin are limited. One real world example of 84 | headers that can have multiple values is "Forwarded". 85 | 86 | .. code:: php 87 | 88 | use Http\Discovery\HttpClientDiscovery; 89 | use Http\Client\Common\PluginClient; 90 | use Http\Client\Common\Plugin\HeaderAppendPlugin; 91 | 92 | $myIp = '100.100.100.100'; 93 | 94 | $headerAppendPlugin = new HeaderAppendPlugin([ 95 | 'Forwarded' => 'for=' . $myIp 96 | ]); 97 | 98 | $pluginClient = new PluginClient( 99 | HttpClientDiscovery::find(), 100 | [$headerAppendPlugin] 101 | ); 102 | 103 | Mixing operations 104 | ----------------- 105 | 106 | Different header plugins can be mixed together to achieve different behaviors 107 | and you can use the same plugin for identical operations. 108 | 109 | The following example will force the ``User-Agent`` and the ``Accept`` header values while removing the ``Cookie`` header: 110 | 111 | .. code:: php 112 | 113 | use Http\Discovery\HttpClientDiscovery; 114 | use Http\Client\Common\PluginClient; 115 | use Http\Client\Common\Plugin\HeaderSetPlugin; 116 | use Http\Client\Common\Plugin\HeaderRemovePlugin; 117 | 118 | $userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1'; 119 | 120 | $headerSetPlugin = new HeaderSetPlugin([ 121 | 'User-Agent' => $userAgent, 122 | 'Accept' => 'application/json' 123 | ]); 124 | 125 | $headerRemovePlugin = new HeaderRemovePlugin([ 126 | 'Cookie' 127 | ]); 128 | 129 | $pluginClient = new PluginClient( 130 | HttpClientDiscovery::find(), 131 | [ 132 | $headerSetPlugin, 133 | $headerRemovePlugin 134 | ] 135 | ); 136 | 137 | 138 | -------------------------------------------------------------------------------- /components/promise.rst: -------------------------------------------------------------------------------- 1 | Promise 2 | ======= 3 | 4 | A promise represents a single result of an asynchronous operation. 5 | It is not necessarily available at a specific time, but should become in the future. 6 | 7 | The PHP-HTTP promise follows the `Promises/A+`_ standard. 8 | 9 | .. note:: 10 | 11 | Work is underway for a `Promise PSR`_. When that PSR has been released, we 12 | will use it in HTTPlug and deprecate our ``Http\Promise\Promise`` interface. 13 | 14 | Asynchronous requests 15 | --------------------- 16 | 17 | Asynchronous requests enable non-blocking HTTP operations. 18 | When sending asynchronous HTTP requests, a promise is returned. The promise acts 19 | as a proxy for the response or error result, which is not yet known. 20 | 21 | To execute such a request with HTTPlug:: 22 | 23 | $request = $messageFactory->createRequest('GET', 'http://php-http.org'); 24 | 25 | // Where $client implements HttpAsyncClient 26 | $promise = $client->sendAsyncRequest($request); 27 | 28 | // This code will be executed right after the request is sent, but before 29 | // the response is returned. 30 | echo 'Wow, non-blocking!'; 31 | 32 | See :ref:`message-factory` on how to use message factories. 33 | 34 | Although the promise itself is not restricted to resolve a specific result type, 35 | in HTTP context it resolves a PSR-7 ``Psr\Http\Message\ResponseInterface`` or fails with an ``Http\Client\Exception``. 36 | 37 | .. note:: 38 | 39 | An asynchronous request will never throw an exception directly but always 40 | return a promise. All exceptions SHOULD implement ``Http\Client\Exception``. 41 | See :doc:`../httplug/exceptions` for more information on the exceptions 42 | you might encounter. 43 | 44 | Wait 45 | ---- 46 | 47 | The ``$promise`` that is returned implements ``Http\Promise\Promise``. At this 48 | point in time, the response is not known yet. You can be polite and wait for 49 | that response to arrive:: 50 | 51 | try { 52 | $response = $promise->wait(); 53 | } catch (\Exception $exception) { 54 | echo $exception->getMessage(); 55 | } 56 | 57 | Then 58 | ---- 59 | 60 | Instead of waiting, however, you can handle things asynchronously. Call the 61 | ``then`` method with two arguments: one callback that will be executed if the 62 | request turns out to be successful and/or a second callback that will be 63 | executed if the request results in an error:: 64 | 65 | $promise->then( 66 | // The success callback 67 | function (ResponseInterface $response) { 68 | echo 'Yay, we have a shiny new response!'; 69 | 70 | // Write status code to some log file 71 | file_put_contents('responses.log', $response->getStatusCode() . "\n", FILE_APPEND); 72 | 73 | return $response; 74 | }, 75 | 76 | // The failure callback 77 | function (\Exception $exception) { 78 | echo 'Oh darn, we have a problem'; 79 | 80 | throw $exception; 81 | } 82 | ); 83 | 84 | The failure callback can also return a ``Promise``. This can be useful to implement a retry 85 | mechanism, as follows:: 86 | 87 | use Http\Discovery\HttpAsyncClientDiscovery; 88 | use Http\Discovery\Psr17FactoryDiscovery; 89 | 90 | $client = HttpAsyncClientDiscovery::find(); 91 | $requestFactory = Psr17FactoryDiscovery::findRequestFactory(); 92 | $retries = 2; // number of HTTP retries 93 | $request = $requestFactory->createRequest("GET", "http://localhost:8080/test"); 94 | 95 | // success callback 96 | $success = function (ResponseInterface $response) { 97 | return $response; 98 | }; 99 | // failure callback 100 | $failure = function (Exception $e) use ($client, $request) { 101 | // $request can be changed, e.g. using a Round-Robin algorithm 102 | 103 | // try another execution 104 | return $client->sendAsyncRequest($request); 105 | }; 106 | 107 | $promise = $client->sendAsyncRequest($request); 108 | for ($i=0; $i < $retries; $i++) { 109 | $promise = $promise->then($success, $failure); 110 | } 111 | // Add the last callable to manage the exceeded maximum number of retries 112 | $promise->then($success, function(\Exception $e) { 113 | throw new \Exception(sprintf( 114 | "Exceeded maximum number of retries (%d): %s", 115 | $retries, 116 | $e->getMessage() 117 | )); 118 | }); 119 | 120 | .. _`Promise PSR`: https://groups.google.com/forum/?fromgroups#!topic/php-fig/wzQWpLvNSjs 121 | .. _Promises/A+: https://promisesaplus.com 122 | -------------------------------------------------------------------------------- /message/message-factory.rst: -------------------------------------------------------------------------------- 1 | .. _message-factory: 2 | .. _stream-factory: 3 | 4 | HTTP Factories (deprecated) 5 | =========================== 6 | 7 | **Factory interfaces for PSR-7 HTTP objects.** 8 | 9 | This package has been superseded by `PSR-17`_. Our HTTP-PHP factories have been 10 | retired and the repository archived. The PHP-HTTP libraries switched to use the 11 | PSR-17 factories. Please migrate your code to the PSR-17 factories too. 12 | 13 | Rationale 14 | --------- 15 | 16 | At the time of building this, PSR-17 did not yet exist. Read the documentation 17 | of `PSR-17`_ to learn why a standard for factories is useful. 18 | 19 | Factories 20 | --------- 21 | 22 | The `php-http/message-factory` package defines interfaces for PSR-7 factories 23 | including: 24 | 25 | - ``RequestFactory`` 26 | - ``ResponseFactory`` 27 | - ``MessageFactory`` (combination of request and response factories) 28 | - ``StreamFactory`` 29 | - ``UriFactory`` 30 | 31 | Implementations of the interfaces above for `Laminas Diactoros`_ (and its 32 | abandoned predecessor `Zend Diactoros`_), `Guzzle PSR-7`_ and the 33 | `Slim PSR-7`_ can be found in ``php-http/message``. 34 | 35 | Usage 36 | ----- 37 | 38 | Instantiate the factories in your bootstrap code or use discovery for them. 39 | Inject the factories into the rest of your code to limit the implementation 40 | choice to the bootstrapping code:: 41 | 42 | // ApiClient.php 43 | 44 | use Http\Message\RequestFactory; 45 | use Http\Message\StreamFactory; 46 | use Http\Message\UriFactory; 47 | 48 | class ApiClient 49 | { 50 | /** 51 | * @var RequestFactory 52 | */ 53 | private $requestFactory; 54 | 55 | /** 56 | * @var StreamFactory 57 | */ 58 | private $streamFactory; 59 | 60 | /** 61 | * @var UriFactory 62 | */ 63 | private $uriFactory; 64 | 65 | public function __construct( 66 | RequestFactory $requestFactory, 67 | StreamFactory $streamFactory, 68 | UriFactory $uriFactory 69 | ) { 70 | $this->requestFactory = $requestFactory; 71 | $this->streamFactory = $streamFactory; 72 | $this->uriFactory = $uriFactory; 73 | } 74 | 75 | public function doStuff() 76 | { 77 | $request = $this->requestFactory->createRequest('GET', 'http://httplug.io'); 78 | $stream = $this->streamFactory->createStream('stream content'); 79 | $uri = $this->uriFactory->createUri('http://httplug.io'); 80 | ... 81 | } 82 | } 83 | 84 | The bootstrapping code could look like this:: 85 | 86 | // bootstrap.php 87 | use Http\Message\MessageFactory\DiactorosMessageFactory; 88 | use Http\Message\StreamFactory\DiactorosStreamFactory; 89 | use Http\Message\UriFactory\DiactorosUriFactory; 90 | 91 | $apiClient = new ApiClient( 92 | new DiactorosMessageFactory(), 93 | new DiactorosStreamFactory(), 94 | new DiactorosUriFactory() 95 | ); 96 | 97 | You could also use :doc:`/discovery` to make the factory arguments optional and 98 | automatically find an available factory in the client:: 99 | 100 | // ApiClient.php 101 | 102 | use Http\Discovery\MessageFactoryDiscovery; 103 | use Http\Discovery\StreamFactoryDiscovery; 104 | use Http\Discovery\UriFactoryDiscovery; 105 | use Http\Message\RequestFactory; 106 | use Http\Message\StreamFactory; 107 | use Http\Message\UriFactory; 108 | 109 | class ApiClient 110 | { 111 | public function __construct( 112 | RequestFactory $requestFactory = null, 113 | StreamFactory $streamFactory = null, 114 | UriFactory $uriFactory = null 115 | ) { 116 | $this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find(), 117 | $this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find(); 118 | $this->uriFactory = $uriFactory ?: UriFactoryDiscovery::find();; 119 | } 120 | 121 | ... 122 | } 123 | 124 | .. hint:: 125 | 126 | If you create requests only and no responses, use ``RequestFactory`` in the 127 | type hint, instead of the ``MessageFactory``. And vice versa if you create 128 | responses only. 129 | 130 | .. _PSR-17: https://www.php-fig.org/psr/psr-17/ 131 | .. _Guzzle PSR-7: https://github.com/guzzle/psr7 132 | .. _Laminas Diactoros: https://github.com/laminas/laminas-diactoros 133 | .. _Slim PSR-7: https://github.com/slimphp/Slim-Psr7 134 | .. _Zend Diactoros: https://github.com/zendframework/zend-diactoros 135 | .. _Slim Framework: https://github.com/slimphp/Slim 136 | -------------------------------------------------------------------------------- /plugins/build-your-own.rst: -------------------------------------------------------------------------------- 1 | Building Custom Plugins 2 | ======================= 3 | 4 | When writing your own Plugin, you need to be aware that the Plugin Client is async first. 5 | This means that every plugin must be written with Promises. More about this later. 6 | 7 | Each plugin must implement the ``Http\Client\Common\Plugin`` interface. 8 | 9 | .. versionadded:: 1.1 10 | The plugins were moved to the `client-common` package in version 1.1. 11 | If you work with version 1.0, the interface is called ``Http\Client\Plugin\Plugin`` and 12 | you need to require the separate `php-http/plugins` package. 13 | The old interface will keep extending ``Http\Client\Common\Plugin``, but relying on it is deprecated. 14 | 15 | This interface defines the ``handleRequest`` method that allows to modify behavior of the call:: 16 | 17 | /** 18 | * Handles the request and returns the response coming from the next callable. 19 | * 20 | * @param RequestInterface $request Request to use. 21 | * @param callable $next Callback to call to have the request, it muse have the request as it first argument. 22 | * @param callable $first First element in the plugin chain, used to to restart a request from the beginning. 23 | * 24 | * @return Promise 25 | */ 26 | public function handleRequest(RequestInterface $request, callable $next, callable $first); 27 | 28 | The ``$request`` comes from an upstream plugin or ``PluginClient`` itself. 29 | You can replace it and pass a new version downstream if you need. 30 | 31 | The ``$next`` callable is the next plugin in the execution chain. When you need to call it, you must pass the ``$request`` 32 | as the first argument of this callable. 33 | 34 | For example a simple plugin setting a header would look like this:: 35 | 36 | public function handleRequest(RequestInterface $request, callable $next, callable $first) 37 | { 38 | $newRequest = $request->withHeader('MyHeader', 'MyValue'); 39 | 40 | return $next($newRequest); 41 | } 42 | 43 | The ``$first`` callable is the first plugin in the chain. It allows you to completely reboot the execution chain, or send 44 | another request if needed, while still going through all the defined plugins. 45 | Like in case of the ``$next`` callable, you must pass the ``$request`` as the first argument:: 46 | 47 | public function handleRequest(RequestInterface $request, callable $next, callable $first) 48 | { 49 | if ($someCondition) { 50 | $newRequest = new Request(); 51 | $promise = $first($newRequest); 52 | 53 | // Use the promise to do some jobs ... 54 | } 55 | 56 | return $next($request); 57 | } 58 | 59 | .. warning:: 60 | 61 | In this example the condition is not superfluous: 62 | you need to have some way to not call the ``$first`` callable each time 63 | or you will end up in an infinite execution loop. 64 | 65 | The ``$next`` and ``$first`` callables will return a :doc:`/components/promise`. 66 | You can manipulate the ``Psr\Http\Message\ResponseInterface`` or the ``Http\Client\Exception`` by using the 67 | ``then`` method of the promise:: 68 | 69 | public function handleRequest(RequestInterface $request, callable $next, callable $first) 70 | { 71 | $newRequest = $request->withHeader('MyHeader', 'MyValue'); 72 | 73 | return $next($request)->then(function (ResponseInterface $response) { 74 | return $response->withHeader('MyResponseHeader', 'value'); 75 | }, function (\Http\Client\Exception $exception) { 76 | echo $exception->getMessage(); 77 | 78 | throw $exception; 79 | }); 80 | } 81 | 82 | .. warning:: 83 | 84 | Contract for the ``Http\Promise\Promise`` is temporary until a 85 | PSR_ is released. Once it is out, we will use this PSR in HTTPlug and 86 | deprecate the old contract. 87 | 88 | .. warning:: 89 | 90 | If a plugin throws an exception that does not implement ``Http\Client\Exception`` 91 | it will break the plugin chain. 92 | 93 | To better understand the whole process check existing implementations in the 94 | `client-common package`_. 95 | 96 | Contributing Your Plugins to PHP-HTTP 97 | ------------------------------------- 98 | 99 | We are open to contributions. If the plugin is of general interest, not too 100 | complex and does not have dependencies, the best is to do a Pull Request to 101 | ``php-http/client-common``. Please see the :doc:`contribution guide <../development/contributing>`. 102 | We don't promise that every plugin gets merged into the core. We need to keep 103 | the core as small as possible with only the most widely used plugins to keep 104 | it maintainable. 105 | 106 | The alternative is providing your plugins in your own repository. Please let us 107 | know when you do, we would like to add a list of existing third party plugins 108 | to the list of plugins. 109 | 110 | .. _PSR: https://groups.google.com/forum/?fromgroups#!topic/php-fig/wzQWpLvNSjs 111 | .. _client-common package: https://github.com/php-http/client-common 112 | -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | PHP-HTTP: standardized HTTP for PHP 2 | =================================== 3 | 4 | .. image:: assets/img/plugefant.png 5 | :align: right 6 | :width: 120px 7 | :alt: HTTPlug Logo 8 | 9 | PHP-HTTP is the next step in standardizing HTTP interaction for PHP packages. 10 | 11 | It builds on top of PSR-7_, which defines interfaces for HTTP requests and 12 | responses. The HTTPlug HTTP client interface has been standardized in PSR-18_ 13 | to define synchronous HTTP requests. When using a client that implements PSR-18, 14 | we recommend directly using PSR-18 and not HTTPlug nor our adapters. 15 | 16 | However, PSR-18 does not define asynchronous requests. HTTPlug provides interfaces 17 | for those, but to do that also needs to define how :doc:`promises ` 18 | are implemented. 19 | 20 | PHP-HTTP has three goals: 21 | 22 | 1. Encourage package developers to depend on the simple HTTPlug interface 23 | instead of concrete HTTP clients. 24 | 25 | 2. Provide good quality HTTP-related packages to the PHP community. 26 | 27 | 3. Now that PSR-18 exists, we miss a PSR for asynchronous requests. This is blocked 28 | by not having a PSR for promises. 29 | 30 | HTTPlug 31 | ------- 32 | 33 | HTTPlug abstracts from HTTP clients written in PHP, offering a simple interface. 34 | It also provides an implementation-independent plugin system to build pipelines 35 | regardless of the HTTP client implementation used. 36 | 37 | Read more about :doc:`HTTPlug `. 38 | 39 | They use us 40 | ``````````` 41 | 42 | .. image:: https://avatars3.githubusercontent.com/u/5303359?v=3&s=100 43 | :align: left 44 | :width: 10% 45 | :target: https://github.com/geocoder-php/Geocoder 46 | :alt: Geocoder 47 | 48 | .. image:: https://avatars0.githubusercontent.com/u/447686?v=3&s=100 49 | :align: left 50 | :width: 10% 51 | :target: https://github.com/mailgun/mailgun-php 52 | :alt: Mailgun 53 | 54 | .. image:: https://avatars2.githubusercontent.com/u/529709?v=3&s=100 55 | :align: left 56 | :width: 10% 57 | :target: https://github.com/FriendsOfSymfony/FOSHttpCache 58 | :alt: FOSHttpCache 59 | 60 | .. image:: https://avatars3.githubusercontent.com/u/202732?v=3&s=100 61 | :align: left 62 | :width: 10% 63 | :target: https://github.com/KnpLabs/php-github-api 64 | :alt: KnpLabs 65 | 66 | .. image:: https://avatars1.githubusercontent.com/u/1396951?v=3&s=100 67 | :align: left 68 | :width: 10% 69 | :target: https://github.com/getsentry/sentry-php 70 | :alt: Sentry 71 | 72 | .. image:: https://avatars2.githubusercontent.com/u/1150427?v=3&s=100 73 | :align: left 74 | :width: 10% 75 | :target: https://github.com/hwi/HWIOAuthBundle 76 | :alt: HWIOAuthBundle 77 | 78 | |clearfloat| 79 | 80 | Packages 81 | -------- 82 | 83 | PHP-HTTP offers several packages: 84 | 85 | =============== =========================================================== ==================================== 86 | Type Description Namespace 87 | =============== =========================================================== ==================================== 88 | Clients HTTP clients: Socket, cURL and others ``Http\Client\[Name]`` 89 | Client adapters Adapters for other clients: Guzzle, React and others ``Http\Adapter\[Name]`` 90 | Plugins Implementation-independent authentication, cookies and more ``Http\Client\Common\Plugin\[Name]`` 91 | =============== =========================================================== ==================================== 92 | 93 | Read more about :doc:`clients and adapters ` and :doc:`plugins `. 94 | 95 | The future 96 | ---------- 97 | 98 | HTTPlug, as a working example of an HTTP client interface, can serve as a basis 99 | for discussion around a future HTTP client PSR. 100 | 101 | Community 102 | --------- 103 | 104 | If you want to get in touch, feel free to ask questions on `our Slack channel`_ or ping `@httplug`_ on Twitter. 105 | 106 | 107 | .. toctree:: 108 | :hidden: 109 | 110 | PHP-HTTP 111 | 112 | .. toctree:: 113 | :hidden: 114 | :caption: HTTPlug 115 | :maxdepth: 4 116 | 117 | Introduction 118 | Usage 119 | Exceptions 120 | Tutorial 121 | Migrating 122 | 123 | clients 124 | plugins/index 125 | 126 | integrations/index 127 | 128 | Backwards compatibility 129 | 130 | .. toctree:: 131 | :hidden: 132 | :caption: Components 133 | 134 | message 135 | components/client-common 136 | components/adapter-integration-tests 137 | components/promise 138 | discovery 139 | components/multipart-stream-builder 140 | 141 | .. toctree:: 142 | :hidden: 143 | :caption: --------- 144 | 145 | development/index.rst 146 | 147 | .. |clearfloat| raw:: html 148 | 149 |
150 | 151 | .. _`our Slack channel`: http://slack.httplug.io/ 152 | .. _`@httplug`: https://twitter.com/httplug 153 | -------------------------------------------------------------------------------- /plugins/vcr.rst: -------------------------------------------------------------------------------- 1 | VCR Plugin - Record and Replay Responses 2 | ======================================== 3 | 4 | The VCR plugins allow you to record & replay HTTP responses. It's very useful for test purpose (using production-like predictable fixtures and avoid making actual HTTP request). 5 | You can also use it during your development cycle, when the endpoint you're contacting is not ready yet. 6 | 7 | Unlike the :doc:`php-http/mock-client
`, where you have to manually define responses, the responses are **automatically** generated from the previously recorded ones. 8 | 9 | Install 10 | ------- 11 | 12 | .. code-block:: bash 13 | 14 | $ composer require --dev php-http/vcr-plugin 15 | 16 | Usage 17 | ----- 18 | 19 | To record or replay a response, you will need two components, a **naming strategy** and a **recorder**. 20 | 21 | The naming strategy 22 | ******************* 23 | 24 | The naming strategy turn a request into a deterministic and unique identifier. 25 | The identifier must be safe to use with a file system. 26 | The plugin provide a default naming strategy, the ``PathNamingStrategy``. You can define two options: 27 | 28 | * **hash_headers**: the list of header(s) that make the request unique (Ex: 'Authorization'). The name & content of the header will be hashed to generate a unique signature. By default no header is used. 29 | * **hash_body_methods**: indicate for which request methods the body makes requests distinct. (Default: PUT, POST, PATCH) 30 | 31 | This naming strategy will turn a GET request to https://example.org/my-path to the ``example.org_GET_my-path`` name, and optionally add hashes if the request 32 | contain a header defined in the options, or if the method is not idempotent. 33 | 34 | To create your own strategy, you need to create a class implementing ``Http\Client\Plugin\Vcr\NamingStrategy\NamingStrategyInterface``. 35 | 36 | The recorder 37 | ************ 38 | 39 | The recorder records and replays responses. The plugin provides two recorders: 40 | 41 | * ``FilesystemRecorder``: Saves the response on your file system using Symfony's `filesystem component`_ and `Guzzle PSR7`_ library. 42 | * ``InMemoryRecorder``: Saves the response in memory. **Response will be lost at the end of the running process** 43 | 44 | To create your own recorder, you need to create a class implementing the following interfaces: 45 | 46 | * ``Http\Client\Plugin\Vcr\Recorder\RecorderInterface`` used by the RecordPlugin. 47 | * ``Http\Client\Plugin\Vcr\Recorder\PlayerInterface`` used by the ReplayPlugin. 48 | 49 | The plugins 50 | *********** 51 | 52 | There are two plugins, one to record responses, the other to replay them. 53 | 54 | * ``Http\Client\Plugin\Vcr\ReplayPlugin``, use a ``PlayerInterface`` to replay previously recorded responses. 55 | * ``Http\Client\Plugin\Vcr\RecordPlugin``, use a ``RecorderInterface`` instance to record the responses, 56 | 57 | Both plugins add a response header to indicate either under which name the response has been stored (RecordPlugin, ``X-VCR-RECORD`` header), or which response name has been used to replay the request (ReplayPlugin, ``X-VCR-REPLAYED`` header). 58 | 59 | If you plan on using both plugins at the same time (Replay or Record), the ``ReplayPlugin`` **must always** come first. 60 | Please also note that by default, the ``ReplayPlugin`` throws an exception when it cannot replay a request. If you want the plugin to continue the request (possibly to the actual server), set the third constructor argument to ``false`` (See example below). 61 | 62 | Example 63 | ******* 64 | 65 | .. code-block:: php 66 | 67 | ['X-Custom-Header'], // None by default 78 | 'hash_body_methods' => ['POST'], // Default: PUT, POST, PATCH 79 | ]); 80 | $recorder = new FilesystemRecorder('some/dir/in/vcs'); // You can use InMemoryRecorder as well 81 | 82 | // To record responses: 83 | $record = new RecordPlugin($namingStrategy, $recorder); 84 | 85 | // To replay responses: 86 | // Third argument prevent the plugin from throwing an exception when a request cannot be replayed 87 | $replay = new ReplayPlugin($namingStrategy, $recorder, false); 88 | 89 | $pluginClient = new PluginClient( 90 | HttpClientDiscovery::find(), 91 | [$replay, $record] // Replay should always go first 92 | ); 93 | 94 | /** @var \Psr\Http\Message\RequestInterface $request */ 95 | $request = new MyRequest('GET', 'https://httplug.io'); 96 | 97 | // Will be recorded in "some/dir/in/vcs" 98 | $client->sendRequest($request); 99 | 100 | // Will be replayed from "some/dir/in/vcs" 101 | $client->sendRequest($request); 102 | 103 | .. _filesystem component: https://symfony.com/doc/current/components/filesystem.html 104 | .. _Guzzle PSR7: https://github.com/guzzle/psr7 105 | -------------------------------------------------------------------------------- /plugins/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Install 5 | ------- 6 | 7 | The plugin client and the core plugins are available in the `php-http/client-common`_ package: 8 | 9 | .. code-block:: bash 10 | 11 | $ composer require php-http/client-common 12 | 13 | .. _php-http/client-common: https://github.com/php-http/client-common 14 | 15 | .. versionadded:: 1.1 16 | The plugins were moved to the clients-common package in version 1.1. 17 | If you work with version 1.0, you need to require the separate `php-http/plugins` package 18 | and the namespace is ``Http\Client\Plugin`` instead of ``Http\Client\Common`` 19 | 20 | 21 | How it works 22 | ------------ 23 | 24 | In the plugin package, you can find the following content: 25 | 26 | - the ``PluginClient`` itself which acts as a wrapper around any kind of HTTP client (sync/async); 27 | - the ``Plugin`` interface; 28 | - a set of core plugins (see the full list in the left side navigation). 29 | 30 | The ``PluginClient`` accepts an HTTP client implementation and an array of plugins. 31 | Let’s see an example:: 32 | 33 | use Http\Discovery\HttpClientDiscovery; 34 | use Http\Client\Common\PluginClient; 35 | use Http\Client\Common\Plugin\RetryPlugin; 36 | use Http\Client\Common\Plugin\RedirectPlugin; 37 | 38 | $retryPlugin = new RetryPlugin(); 39 | $redirectPlugin = new RedirectPlugin(); 40 | 41 | $pluginClient = new PluginClient( 42 | HttpClientDiscovery::find(), 43 | [ 44 | $retryPlugin, 45 | $redirectPlugin, 46 | ] 47 | ); 48 | 49 | The ``PluginClient`` accepts and implements both ``Http\Client\HttpClient`` and 50 | ``Http\Client\HttpAsyncClient``, so you can use both ways to send a request. In 51 | case the passed client implements only one of these interfaces, the ``PluginClient`` 52 | "emulates" the other behavior as a fallback. 53 | 54 | It is important to note that the order of plugins matters. During the request, 55 | plugins are executed in the order they have been specified in the constructor, 56 | from first to last. Once a response has been received, the plugins are called 57 | again in reversed order, from last to first. 58 | 59 | For our previous example, the execution chain will look like this: 60 | 61 | .. code:: 62 | 63 | Request ---> PluginClient ---> RetryPlugin ---> RedirectPlugin ---> HttpClient ---- 64 | | (processing call) 65 | Response <--- PluginClient <--- RetryPlugin <--- RedirectPlugin <--- HttpClient <--- 66 | 67 | In order to achieve the intended behavior in the global process, you need to 68 | pay attention to what each plugin does and define the correct order accordingly. 69 | 70 | For example, the ``RetryPlugin`` should probably be at the end of the chain to 71 | keep the retry process as short as possible. However, if one of the other 72 | plugins is doing a fragile operation that might need a retry, place the retry 73 | plugin before that. 74 | 75 | The recommended way to order plugins is the following: 76 | 77 | 1. Plugins that modify the request should be at the beginning (like Authentication or Cookie Plugin); 78 | 2. Plugins which intervene in the workflow should be in the "middle" (like Retry or Redirect Plugin); 79 | 3. Plugins which log information should be last (like Logger or History Plugin). 80 | 81 | .. note:: 82 | 83 | There can be exceptions to these rules. For example, for security reasons you might not want 84 | to log the authentication information (like ``Authorization`` header) and choose to put the 85 | :doc:`Authentication Plugin ` after the :doc:`Logger Plugin `. 86 | 87 | Configuration Options 88 | --------------------- 89 | 90 | The ``PluginClient`` accepts an array of configuration options to tweak its behavior. 91 | 92 | .. _plugin-client.max-restarts: 93 | 94 | ``max_restarts``: int (default 10) 95 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 96 | 97 | To prevent issues with faulty plugins or endless redirects, the ``PluginClient`` injects a security 98 | check to the start of the plugin chain. If the same request is restarted more than specified by 99 | that value, execution is aborted and an error is raised. 100 | 101 | .. _plugin-client.debug-plugins: 102 | 103 | ``debug_plugins``: array of Plugin 104 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 105 | 106 | The debug plugins are injected between each normal plugin. This can be used to 107 | log the changes each plugin does on the request and response objects. 108 | 109 | .. _plugin-client.libraries: 110 | 111 | Libraries that Require Plugins 112 | ------------------------------ 113 | 114 | When :doc:`writing a library based on HTTPlug <../httplug/library-developers>`, you might require 115 | specific plugins to be active. The recommended way for doing this is to provide a factory method 116 | for the ``PluginClient`` that library users should use. This allows them to inject their own 117 | plugins or configure a different client. For example:: 118 | 119 | $myApiClient = new My\Api\Client('https://api.example.org', My\Api\HttpClientFactory::create('john', 's3cr3t')); 120 | 121 | use Http\Client\HttpClient; 122 | use Http\Client\Common\Plugin; 123 | use Http\Client\Common\Plugin\AuthenticationPlugin; 124 | use Http\Client\Common\Plugin\ErrorPlugin; 125 | use Http\Discovery\HttpClientDiscovery; 126 | 127 | class HttpClientFactory 128 | { 129 | /** 130 | * Build the HTTP client to talk with the API. 131 | * 132 | * @param string $user Username for the application on the API 133 | * @param string $pass Password for the application on the API 134 | * @param Plugin[] $plugins List of additional plugins to use 135 | * @param HttpClient $client Base HTTP client 136 | * 137 | * @return HttpClient 138 | */ 139 | public static function create($user, $pass, array $plugins = [], HttpClient $client = null) 140 | { 141 | if (!$client) { 142 | $client = HttpClientDiscovery::find(); 143 | } 144 | $plugins[] = new ErrorPlugin(); 145 | $plugins[] = new AuthenticationPlugin( 146 | // This API has it own authentication algorithm 147 | new ApiAuthentication(Client::AUTH_OAUTH_TOKEN, $user, $pass) 148 | ); 149 | return new PluginClient($client, $plugins); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /httplug/tutorial.rst: -------------------------------------------------------------------------------- 1 | HTTPlug Tutorial 2 | ================ 3 | 4 | This tutorial should give you an idea how to use HTTPlug in your project. HTTPlug has two main use cases: 5 | 6 | 1. Usage in your project; 7 | 2. Usage in a reusable package. 8 | 9 | This tutorial will start with the first use case and then explain the special considerations to 10 | take into account when building a reusable package. 11 | 12 | We use Composer_ for dependency management. Install it if you don't have it yet. 13 | 14 | .. note:: 15 | 16 | If you are using a framework, check the :doc:`../integrations/index` to see if 17 | there is an integration available. Framework integrations will simplify the way 18 | you set up clients, letting you focus on handling the requests. 19 | 20 | Setting up the Project 21 | ---------------------- 22 | 23 | .. code-block:: bash 24 | 25 | mkdir httplug-tutorial 26 | cd httplug-tutorial 27 | composer init 28 | # specify your information as you want. say no to defining the dependencies interactively 29 | composer require php-http/guzzle6-adapter 30 | 31 | The last command will install Guzzle as well as the Guzzle HTTPlug adapter and the required interface repositories. 32 | We are now ready to start coding. 33 | 34 | 35 | Writing Some Simple Code 36 | ------------------------ 37 | 38 | Create a file ``demo.php`` in the root folder and write the following code:: 39 | 40 | sendRequest( 49 | $messageFactory->createRequest('GET', 'http://httplug.io') 50 | ); 51 | 52 | var_dump($homeResponse->getStatusCode()); // 200, hopefully 53 | 54 | $missingPageResponse = $client->sendRequest( 55 | $messageFactory->createRequest('GET', 'http://httplug.io/missingPage') 56 | ); 57 | 58 | var_dump($missingPageResponse->getStatusCode()); // 404 59 | 60 | Using an Asynchronous Client 61 | ---------------------------- 62 | 63 | Asynchronous client accepts a PSR-7 ``RequestInterface`` and returns a ``Http\Promise\Promise``:: 64 | 65 | use Http\Discovery\HttpAsyncClientDiscovery; 66 | 67 | $httpAsyncClient = HttpAsyncClientDiscovery::find(); 68 | $promise = $httpAsyncClient->sendAsyncRequest($request); 69 | 70 | Using the Callback System 71 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 72 | 73 | The promise allows you to add callbacks for when the response is available or an errors happens by using the then method:: 74 | 75 | $promise->then(function (ResponseInterface $response) { 76 | // onFulfilled callback 77 | echo 'The response is available'; 78 | 79 | return $response; 80 | }, function (Exception $e) { 81 | // onRejected callback 82 | echo 'An error happens'; 83 | 84 | throw $e; 85 | }); 86 | 87 | This method will return another promise so you can manipulate the response and/or exception and 88 | still provide a way to interact with this object for your users:: 89 | 90 | $promise->then(function (ResponseInterface $response) { 91 | // onFulfilled callback 92 | echo 'The response is available'; 93 | 94 | return $response; 95 | }, function (Exception $e) { 96 | // onRejected callback 97 | echo 'An error happens'; 98 | 99 | throw $e; 100 | })->then(function (ResponseInterface $response) { 101 | echo 'Response still available'; 102 | 103 | return $response; 104 | }, function (Exception $e) { 105 | throw $e 106 | }); 107 | 108 | In order to achieve the chain callback, if you read previous examples carefully, 109 | callbacks provided to the ``then`` method *must* return a PSR-7_ ``ResponseInterface`` or throw a ``Http\Client\Exception``. 110 | For both of the callbacks, if it returns a PSR-7 ``ResponseInterface`` it will call the ``onFulfilled`` callback for 111 | the next element in the chain, if it throws a ``Http\Client\Exception`` it will call the ``onRejected`` callback. 112 | 113 | i.e. you can inverse the behavior of a call:: 114 | 115 | $promise->then(function (ResponseInterface $response) use($request) { 116 | // onFulfilled callback 117 | echo 'The response is available, but it\'s not ok...'; 118 | 119 | throw new HttpException('My error message', $request, $response); 120 | }, function (Exception $e) { 121 | // onRejected callback 122 | echo 'An error happens, but it\'s ok...'; 123 | 124 | return $exception->getResponse(); 125 | }); 126 | 127 | Calling the ``wait`` method on the promise will wait for the response or exception to be available and 128 | invoke callback provided in the ``then`` method. 129 | 130 | Using the promise directly 131 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 132 | 133 | If you don't want to use the callback system, you can also get the state of the promise with ``$promise->getState()`` 134 | will return of one ``Promise::PENDING``, ``Promise::FULFILLED`` or ``Promise::REJECTED``. 135 | 136 | Then you can get the response of the promise if it's in ``FULFILLED`` state or trigger the exception of the promise 137 | if it's in ``REJECTED`` state with ``$promise->wait(true)`` call. 138 | 139 | .. note:: 140 | 141 | Read :doc:`/components/promise` for more information about promises. 142 | 143 | Example 144 | ^^^^^^^ 145 | 146 | Here is a full example of a classic usage when using the ``sendAsyncRequest`` method:: 147 | 148 | use Http\Client\Exception; 149 | use Http\Discovery\HttpAsyncClientDiscovery; 150 | 151 | $httpAsyncClient = HttpAsyncClientDiscovery::find(); 152 | 153 | $promise = $httpAsyncClient->sendAsyncRequest($request); 154 | $promise->then(function (ResponseInterface $response) { 155 | echo 'The response is available'; 156 | 157 | return $response; 158 | }, function (Exception $e) { 159 | echo 'An error happens'; 160 | 161 | throw $e; 162 | }); 163 | 164 | // Do some stuff not depending on the response, calling another request, etc .. 165 | ... 166 | 167 | try { 168 | // We need now the response for our final treatment... 169 | $response = $promise->wait(true); 170 | } catch (Exception $e) { 171 | // ...or catch the thrown exception 172 | } 173 | 174 | // Do your stuff with the response 175 | ... 176 | 177 | Handling Errors 178 | --------------- 179 | 180 | TODO: explain how to handle exceptions, distinction between network exception and HttpException. 181 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | 3 | # You can set these variables from the command line. 4 | SPHINXOPTS = 5 | SPHINXBUILD = sphinx-build 6 | PAPER = 7 | BUILDDIR = _build 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | # the i18n builder cannot share the environment and doctrees with the others 14 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 15 | 16 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 17 | 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 33 | @echo " text to make text files" 34 | @echo " man to make manual pages" 35 | @echo " texinfo to make Texinfo files" 36 | @echo " info to make Texinfo files and run them through makeinfo" 37 | @echo " gettext to make PO message catalogs" 38 | @echo " changes to make an overview of all changed/added/deprecated items" 39 | @echo " xml to make Docutils-native XML files" 40 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 41 | @echo " linkcheck to check all external links for integrity" 42 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 43 | @echo " coverage to run coverage check of the documentation (if enabled)" 44 | @echo " spelling generate a spelling report" 45 | 46 | clean: 47 | rm -rf $(BUILDDIR)/* 48 | 49 | html: 50 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 53 | 54 | dirhtml: 55 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 58 | 59 | singlehtml: 60 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 61 | @echo 62 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 63 | 64 | pickle: 65 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 66 | @echo 67 | @echo "Build finished; now you can process the pickle files." 68 | 69 | json: 70 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 71 | @echo 72 | @echo "Build finished; now you can process the JSON files." 73 | 74 | htmlhelp: 75 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 76 | @echo 77 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 78 | ".hhp project file in $(BUILDDIR)/htmlhelp." 79 | 80 | qthelp: 81 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 82 | @echo 83 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 84 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 85 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PHP-HTTP.qhcp" 86 | @echo "To view the help file:" 87 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PHP-HTTP.qhc" 88 | 89 | applehelp: 90 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 91 | @echo 92 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 93 | @echo "N.B. You won't be able to view it unless you put it in" \ 94 | "~/Library/Documentation/Help or install it in your application" \ 95 | "bundle." 96 | 97 | devhelp: 98 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 99 | @echo 100 | @echo "Build finished." 101 | @echo "To view the help file:" 102 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PHP-HTTP" 103 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PHP-HTTP" 104 | @echo "# devhelp" 105 | 106 | epub: 107 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 108 | @echo 109 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 110 | 111 | latex: 112 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 113 | @echo 114 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 115 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 116 | "(use \`make latexpdf' here to do that automatically)." 117 | 118 | latexpdf: 119 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 120 | @echo "Running LaTeX files through pdflatex..." 121 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 122 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 123 | 124 | latexpdfja: 125 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 126 | @echo "Running LaTeX files through platex and dvipdfmx..." 127 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 128 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 129 | 130 | text: 131 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 132 | @echo 133 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 134 | 135 | man: 136 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 137 | @echo 138 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 139 | 140 | texinfo: 141 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 142 | @echo 143 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 144 | @echo "Run \`make' in that directory to run these through makeinfo" \ 145 | "(use \`make info' here to do that automatically)." 146 | 147 | info: 148 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 149 | @echo "Running Texinfo files through makeinfo..." 150 | make -C $(BUILDDIR)/texinfo info 151 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 152 | 153 | gettext: 154 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 155 | @echo 156 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 157 | 158 | changes: 159 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 160 | @echo 161 | @echo "The overview file is in $(BUILDDIR)/changes." 162 | 163 | linkcheck: 164 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 165 | @echo 166 | @echo "Link check complete; look for any errors in the above output " \ 167 | "or in $(BUILDDIR)/linkcheck/output.txt." 168 | 169 | doctest: 170 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 171 | @echo "Testing of doctests in the sources finished, look at the " \ 172 | "results in $(BUILDDIR)/doctest/output.txt." 173 | 174 | coverage: 175 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 176 | @echo "Testing of coverage in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/coverage/python.txt." 178 | 179 | xml: 180 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 181 | @echo 182 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 183 | 184 | pseudoxml: 185 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 186 | @echo 187 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 188 | 189 | spelling: 190 | $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling 191 | @echo "Spelling report generated in $(BUILDDIR)/spelling/output.txt" 192 | -------------------------------------------------------------------------------- /message/authentication.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | ============== 3 | 4 | The Authentication component allows you to to implement authentication methods which can simply update the request 5 | with authentication detail (for example by adding an ``Authorization`` header). 6 | This is useful when you have to send multiple requests to the same endpoint. Using an authentication implementation, 7 | these details can be separated from the actual requests. 8 | 9 | 10 | Installation 11 | ^^^^^^^^^^^^ 12 | 13 | .. code-block:: bash 14 | 15 | $ composer require php-http/message 16 | 17 | Authentication Methods 18 | ^^^^^^^^^^^^^^^^^^^^^^ 19 | 20 | +----------------+---------------------------------------------------+-----------------------------------------------------+ 21 | |Method | Parameters | Behavior | 22 | +================+===================================================+=====================================================+ 23 | | `Basic Auth`_ | Username and password | ``Authorization`` header of the HTTP specification | 24 | +----------------+---------------------------------------------------+-----------------------------------------------------+ 25 | |Bearer | Token | ``Authorization`` header of the HTTP specification | 26 | +----------------+---------------------------------------------------+-----------------------------------------------------+ 27 | |WSSE_ | Username and password | ``Authorization`` header of the HTTP specification | 28 | +----------------+---------------------------------------------------+-----------------------------------------------------+ 29 | |Query Params | Array of param-value pairs | URI parameters | 30 | +----------------+---------------------------------------------------+-----------------------------------------------------+ 31 | |Chain | Array of authentication instances | Behaviors of the underlying authentication methods | 32 | +----------------+---------------------------------------------------+-----------------------------------------------------+ 33 | |Matching | An authentication instance and a matcher callback | Behavior of the underlying authentication method if | 34 | | | | the matcher callback passes | 35 | +----------------+---------------------------------------------------+-----------------------------------------------------+ 36 | |Header | Header name and value | Add an arbitrary authentication header | 37 | +----------------+---------------------------------------------------+-----------------------------------------------------+ 38 | 39 | .. _`Basic Auth`: https://en.wikipedia.org/wiki/Basic_access_authentication 40 | .. _WSSE: http://www.xml.com/pub/a/2003/12/17/dive.html 41 | 42 | Integration with HTTPlug 43 | ^^^^^^^^^^^^^^^^^^^^^^^^ 44 | 45 | Normally requests must be authenticated "by hand" which is not really convenient. 46 | 47 | If you use HTTPlug, you can integrate this component into the client using the 48 | :doc:`authentication plugin `. 49 | 50 | 51 | Examples 52 | ^^^^^^^^ 53 | 54 | General usage looks like the following:: 55 | 56 | $authentication = new AuthenticationMethod(); 57 | 58 | /** @var Psr\Http\Message\RequestInterface */ 59 | $authentication->authenticate($request); 60 | 61 | Basic Auth 62 | ********** 63 | 64 | .. code-block:: php 65 | 66 | use Http\Message\Authentication\BasicAuth; 67 | 68 | $authentication = new BasicAuth('username', 'password'); 69 | 70 | Bearer 71 | ****** 72 | 73 | .. code-block:: php 74 | 75 | use Http\Message\Authentication\Bearer; 76 | 77 | $authentication = new Bearer('token'); 78 | 79 | WSSE 80 | **** 81 | 82 | .. code-block:: php 83 | 84 | use Http\Message\Authentication\Wsse; 85 | 86 | $authentication = new Wsse('username', 'password'); 87 | 88 | For better security, also pass the 3rd optional parameter to use a better hashing algorithm than ``sha1``, e.g. 89 | 90 | .. code-block:: php 91 | 92 | use Http\Message\Authentication\Wsse; 93 | 94 | $authentication = new Wsse('username', 'password', 'sha512'); 95 | 96 | .. _Authentication-QueryParams: 97 | 98 | Query Params 99 | ************ 100 | 101 | ``http://api.example.com/endpoint?access_token=9zh987g86fg87gh978hg9g79``:: 102 | 103 | 104 | use Http\Message\Authentication\QueryParam; 105 | 106 | $authentication = new QueryParam([ 107 | 'access_token' => '9zh987g86fg87gh978hg9g79', 108 | ]); 109 | 110 | .. warning:: 111 | 112 | Using query parameters for authentication is not safe. 113 | Only use it when this is the only authentication method offered by a third party application. 114 | 115 | Chain 116 | ***** 117 | 118 | The idea behind this authentication method is that in some cases you might need to 119 | authenticate the request with multiple methods. 120 | 121 | For example it's a common practice to protect development APIs with Basic Auth and the regular token authentication as well 122 | to protect the API from unnecessary processing:: 123 | 124 | use Http\Message\Authentication\Chain; 125 | 126 | $authenticationChain = [ 127 | new AuthenticationMethod1(), 128 | new AuthenticationMethod2(), 129 | ]; 130 | 131 | $authentication = new Chain($authenticationChain); 132 | 133 | Matching 134 | ******** 135 | 136 | With this authentication method you can conditionally add authentication details to your request by passing a callable 137 | to it. When a request is passed, the callable is called and used as a boolean value in order to decide whether 138 | the request should be authenticated or not. 139 | It also accepts an authentication method instance which does the actual authentication when the condition is 140 | fulfilled. 141 | 142 | For example a common use case is to authenticate requests sent to certain paths:: 143 | 144 | use Http\Message\Authentication\Matching; 145 | use Psr\Http\Message\RequestInterface; 146 | 147 | $authentication = new Matching( 148 | new AuthenticationMethod1(), 149 | function (RequestInterface $request) { 150 | $path = $request->getUri()->getPath(); 151 | 152 | return 0 === strpos($path, '/api'); 153 | } 154 | ); 155 | 156 | In order to ease creating matchers for URLs/paths, there is a static factory method for this purpose: ``createUrlMatcher`` 157 | The first argument is an authentication method, the second is a regular expression to match against the URL:: 158 | 159 | use Http\Message\Authentication\Matching; 160 | 161 | $authentication = Matching::createUrlMatcher(new AuthenticationMethod(), '\/api'); 162 | 163 | 164 | Header 165 | ****** 166 | 167 | With this authentication method you can add arbitrary headers. 168 | 169 | In the following example, we are setting a ``X-AUTH-TOKEN`` header with it's value:: 170 | 171 | use Http\Message\Authentication\Header; 172 | 173 | $authentication = new Header('X-AUTH-TOKEN', '9zh987g86fg87gh978hg9g79'); 174 | 175 | Implement Your Own 176 | ^^^^^^^^^^^^^^^^^^ 177 | 178 | Implementing an authentication method is easy: only one method needs to be implemented:: 179 | 180 | use Http\Message\Authentication; 181 | use Psr\Http\Message\RequestInterface; 182 | 183 | class MyAuth implements Authentication 184 | { 185 | public function authenticate(RequestInterface $request) 186 | { 187 | // do something with the request 188 | 189 | // keep in mind that the request is immutable - return the updated 190 | // version of the request with the authentication information added 191 | // to it. 192 | return $request; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /clients/mock-client.rst: -------------------------------------------------------------------------------- 1 | Mock Client 2 | =========== 3 | 4 | The mock client is a special type of client. It is a test double that does not 5 | send the requests that you pass to it, but collects them instead. You can then 6 | retrieve those request objects and make assertions about them. Additionally, you 7 | can fake HTTP server responses and exceptions to validate how your code handles 8 | them. This behavior is most useful in tests. 9 | 10 | To install the Mock client, run: 11 | 12 | .. code-block:: bash 13 | 14 | $ composer require php-http/mock-client 15 | 16 | Collect Requests 17 | ---------------- 18 | 19 | To make assertions:: 20 | 21 | use Http\Mock\Client; 22 | 23 | class YourTest extends \PHPUnit_Framework_TestCase 24 | { 25 | public function testRequests() 26 | { 27 | // $firstRequest and $secondRequest are Psr\Http\Message\RequestInterface 28 | // objects 29 | 30 | $client = new Client(); 31 | $client->sendRequest($firstRequest); 32 | $client->sendRequest($secondRequest); 33 | 34 | $bothRequests = $client->getRequests(); 35 | 36 | // Do your assertions 37 | $this->assertEquals('GET', $bothRequests[0]->getMethod()); 38 | // ... 39 | } 40 | } 41 | 42 | Fake Responses and Exceptions 43 | ----------------------------- 44 | 45 | By default, the mock client returns an empty response with status 200. 46 | You can set responses and exceptions the mock client should return / throw. 47 | You can set several exceptions and responses, to have the client first throw 48 | each exception once and then each response once on subsequent calls to send(). 49 | Additionally you can set a default response or a default exception to be used 50 | instead of the empty response. 51 | 52 | Test how your code behaves when the HTTP client throws exceptions or returns 53 | certain responses:: 54 | 55 | use Http\Mock\Client; 56 | 57 | class YourTest extends \PHPUnit_Framework_TestCase 58 | { 59 | public function testClientReturnsResponse() 60 | { 61 | $client = new Client(); 62 | 63 | $response = $this->createMock('Psr\Http\Message\ResponseInterface'); 64 | $client->addResponse($response); 65 | 66 | // $request is an instance of Psr\Http\Message\RequestInterface 67 | $returnedResponse = $client->sendRequest($request); 68 | $this->assertSame($response, $returnedResponse); 69 | $this->assertSame($request, $client->getLastRequest()); 70 | } 71 | } 72 | 73 | Or set a default response:: 74 | 75 | use Http\Mock\Client; 76 | 77 | class YourTest extends \PHPUnit_Framework_TestCase 78 | { 79 | public function testClientReturnsResponse() 80 | { 81 | $client = new Client(); 82 | 83 | $response = $this->createMock('Psr\Http\Message\ResponseInterface'); 84 | $client->setDefaultResponse($response); 85 | 86 | // $firstRequest and $secondRequest are instances of Psr\Http\Message\RequestInterface 87 | $firstReturnedResponse = $client->sendRequest($firstRequest); 88 | $secondReturnedResponse = $client->sendRequest($secondRequest); 89 | $this->assertSame($response, $firstReturnedResponse); 90 | $this->assertSame($response, $secondReturnedResponse); 91 | } 92 | } 93 | 94 | To fake an exception being thrown:: 95 | 96 | use Http\Mock\Client; 97 | 98 | class YourTest extends \PHPUnit_Framework_TestCase 99 | { 100 | /** 101 | * @expectedException \Exception 102 | */ 103 | public function testClientThrowsException() 104 | { 105 | $client = new Client(); 106 | 107 | $exception = new \Exception('Whoops!'); 108 | $client->addException($exception); 109 | 110 | // $request is an instance of Psr\Http\Message\RequestInterface 111 | $returnedResponse = $client->sendRequest($request); 112 | } 113 | } 114 | 115 | Or set a default exception:: 116 | 117 | use Http\Mock\Client; 118 | 119 | class YourTest extends \PHPUnit_Framework_TestCase 120 | { 121 | /** 122 | * @expectedException \Exception 123 | */ 124 | public function testClientThrowsException() 125 | { 126 | $client = new Client(); 127 | 128 | $exception = new \Exception('Whoops!'); 129 | $client->setDefaultException($exception); 130 | 131 | $response = $this->createMock('Psr\Http\Message\ResponseInterface'); 132 | $client->addResponse($response); 133 | 134 | // $firstRequest and $secondRequest are instances of Psr\Http\Message\RequestInterface 135 | // The first request will returns the added response. 136 | $firstReturnedResponse = $client->sendRequest($firstRequest); 137 | // There is no more added response, the default exception will be thrown. 138 | $secondReturnedResponse = $client->sendRequest($secondRequest); 139 | } 140 | } 141 | 142 | Mocking request-dependent responses 143 | ----------------------------------- 144 | 145 | You can mock responses and exceptions which are only used in certain requests 146 | or act differently depending on the request. 147 | 148 | To conditionally return a response when a request is matched:: 149 | 150 | use Http\Mock\Client; 151 | use Psr\Http\Message\ResponseInterface; 152 | 153 | class YourTest extends \PHPUnit_Framework_TestCase 154 | { 155 | /** 156 | * @expectedException \Exception 157 | */ 158 | public function testClientThrowsException() 159 | { 160 | $client = new Client(); 161 | 162 | // $requestMatcher is an instance of Http\Message\RequestMatcher 163 | 164 | $response = $this->createMock(ResponseInterface::class); 165 | $client->on($requestMatcher, $response); 166 | 167 | // $request is an instance of Psr\Http\Message\RequestInterface 168 | $returnedResponse = $client->sendRequest($request); 169 | } 170 | } 171 | 172 | Or for an exception:: 173 | 174 | use Http\Mock\Client; 175 | use Exception; 176 | 177 | class YourTest extends \PHPUnit_Framework_TestCase 178 | { 179 | public function testClientThrowsException() 180 | { 181 | $client = new Client(); 182 | 183 | // $requestMatcher is an instance of Http\Message\RequestMatcher 184 | 185 | $exception = new \Exception('Whoops!'); 186 | $client->on($requestMatcher, $exception); 187 | 188 | // $request is an instance of Psr\Http\Message\RequestInterface 189 | 190 | $this->expectException(\Exception::class); 191 | $returnedResponse = $client->sendRequest($request); 192 | } 193 | } 194 | 195 | Or pass a callable, and return a response or exception based on the request:: 196 | 197 | use Http\Mock\Client; 198 | use Psr\Http\Message\RequestInterface; 199 | 200 | class YourTest extends \PHPUnit_Framework_TestCase 201 | { 202 | /** 203 | * @expectedException \Exception 204 | */ 205 | public function testClientThrowsException() 206 | { 207 | $client = new Client(); 208 | 209 | // $requestMatcher is an instance of Http\Message\RequestMatcher 210 | 211 | $client->on($requestMatcher, function (RequestInterface $request) { 212 | 213 | // return a response 214 | return $this->createMock('Psr\Http\Message\ResponseInterface'); 215 | 216 | // or an exception 217 | return new \Exception('Whoops!'); 218 | }); 219 | 220 | // $request is an instance of Psr\Http\Message\RequestInterface 221 | $returnedResponse = $client->sendRequest($request); 222 | } 223 | } 224 | 225 | 226 | .. hint:: 227 | 228 | If you're using the :doc:`/integrations/symfony-bundle`, the mock client is 229 | available as a service with ``httplug.client.mock`` id. 230 | 231 | See :ref:`symfony-functional-tests` for more on how to use the mock client in Symfony. 232 | 233 | .. include:: includes/further-reading-async.inc 234 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PHP-HTTP documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Jan 2 15:26:57 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | from sphinx.highlighting import lexers 16 | from pygments.lexers.web import PhpLexer 17 | 18 | lexers['php'] = PhpLexer(startinline=True, linenos=1) 19 | 20 | primary_domain = 'php' 21 | highlight_language = 'php' 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | #sys.path.insert(0, os.path.abspath('.')) 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # Warnings 31 | suppress_warnings = ["image.nonlocal_uri"] 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | #needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinxcontrib.spelling', 41 | 'sphinx_rtd_theme', 42 | ] 43 | 44 | # Spelling configuration 45 | spelling_lang='en_US' 46 | spelling_word_list_filename='spelling_word_list.txt' 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The encoding of source files. 57 | #source_encoding = 'utf-8-sig' 58 | 59 | # The master toctree document. 60 | master_doc = 'index' 61 | 62 | # General information about the project. 63 | project = u'PHP-HTTP' 64 | copyright = u'2015, The PHP-HTTP Team' 65 | author = u'The PHP-HTTP Team' 66 | 67 | # The version info for the project you're documenting, acts as replacement for 68 | # |version| and |release|, also used in various other places throughout the 69 | # built documents. 70 | # 71 | # The short X.Y version. 72 | version = u'1.0.0' 73 | # The full version, including alpha/beta/rc tags. 74 | release = u'1.0.0' 75 | 76 | # The language for content autogenerated by Sphinx. Refer to documentation 77 | # for a list of supported languages. 78 | # 79 | # This is also used if you do content translation via gettext catalogs. 80 | # Usually you set "language" from the command line for these cases. 81 | language = 'en' 82 | 83 | # There are two options for replacing |today|: either, you set today to some 84 | # non-false value, then it is used: 85 | #today = '' 86 | # Else, today_fmt is used as the format for a strftime call. 87 | #today_fmt = '%B %d, %Y' 88 | 89 | # List of patterns, relative to source directory, that match files and 90 | # directories to ignore when looking for source files. 91 | exclude_patterns = ['_build'] 92 | 93 | # The reST default role (used for this markup: `text`) to use for all 94 | # documents. 95 | #default_role = None 96 | 97 | # If true, '()' will be appended to :func: etc. cross-reference text. 98 | #add_function_parentheses = True 99 | 100 | # If true, the current module name will be prepended to all description 101 | # unit titles (such as .. function::). 102 | #add_module_names = True 103 | 104 | # If true, sectionauthor and moduleauthor directives will be shown in the 105 | # output. They are ignored by default. 106 | #show_authors = False 107 | 108 | # The name of the Pygments (syntax highlighting) style to use. 109 | pygments_style = 'sphinx' 110 | 111 | # A list of ignored prefixes for module index sorting. 112 | #modindex_common_prefix = [] 113 | 114 | # If true, keep warnings as "system message" paragraphs in the built documents. 115 | #keep_warnings = False 116 | 117 | # If true, `todo` and `todoList` produce output, else they produce nothing. 118 | todo_include_todos = False 119 | 120 | 121 | # -- Options for HTML output ---------------------------------------------- 122 | 123 | html_theme = 'sphinx_rtd_theme' 124 | 125 | # Theme options are theme-specific and customize the look and feel of a theme 126 | # further. For a list of options available for each theme, see the 127 | # documentation. 128 | #html_theme_options = {} 129 | 130 | # Add any paths that contain custom themes here, relative to this directory. 131 | #html_theme_path = [] 132 | 133 | # The name for this set of Sphinx documents. If None, it defaults to 134 | # " v documentation". 135 | #html_title = None 136 | 137 | # A shorter title for the navigation bar. Default is the same as html_title. 138 | #html_short_title = None 139 | 140 | # The name of an image file (relative to this directory) to place at the top 141 | # of the sidebar. 142 | #html_logo = None 143 | 144 | # The name of an image file (within the static path) to use as favicon of the 145 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 146 | # pixels large. 147 | html_favicon = 'favicon.ico' 148 | 149 | # Add any paths that contain custom static files (such as style sheets) here, 150 | # relative to this directory. They are copied after the builtin static files, 151 | # so a file named "default.css" will overwrite the builtin "default.css". 152 | html_static_path = ['_static'] 153 | 154 | html_css_files = [ 155 | 'custom.css', 156 | ] 157 | 158 | html_logo = "_static/logo.png" 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | #html_extra_path = [] 164 | 165 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 166 | # using the given strftime format. 167 | #html_last_updated_fmt = '%b %d, %Y' 168 | 169 | # If true, SmartyPants will be used to convert quotes and dashes to 170 | # typographically correct entities. 171 | #html_use_smartypants = True 172 | 173 | # Custom sidebar templates, maps document names to template names. 174 | #html_sidebars = {} 175 | 176 | # Additional templates that should be rendered to pages, maps page names to 177 | # template names. 178 | #html_additional_pages = {} 179 | 180 | # If false, no module index is generated. 181 | #html_domain_indices = True 182 | 183 | # If false, no index is generated. 184 | #html_use_index = True 185 | 186 | # If true, the index is split into individual pages for each letter. 187 | #html_split_index = False 188 | 189 | # If true, links to the reST sources are added to the pages. 190 | #html_show_sourcelink = True 191 | 192 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 193 | #html_show_sphinx = True 194 | 195 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 196 | #html_show_copyright = True 197 | 198 | # If true, an OpenSearch description file will be output, and all pages will 199 | # contain a tag referring to it. The value of this option must be the 200 | # base URL from which the finished HTML is served. 201 | #html_use_opensearch = '' 202 | 203 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 204 | #html_file_suffix = None 205 | 206 | # Language to be used for generating the HTML full-text search index. 207 | # Sphinx supports the following languages: 208 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 209 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 210 | #html_search_language = 'en' 211 | 212 | # A dictionary with options for the search language support, empty by default. 213 | # Now only 'ja' uses this config value 214 | #html_search_options = {'type': 'default'} 215 | 216 | # The name of a javascript file (relative to the configuration directory) that 217 | # implements a search results scorer. If empty, the default will be used. 218 | #html_search_scorer = 'scorer.js' 219 | 220 | # Output file base name for HTML help builder. 221 | htmlhelp_basename = 'PHP-HTTPdoc' 222 | 223 | # -- Options for LaTeX output --------------------------------------------- 224 | 225 | latex_elements = { 226 | # The paper size ('letterpaper' or 'a4paper'). 227 | #'papersize': 'letterpaper', 228 | 229 | # The font size ('10pt', '11pt' or '12pt'). 230 | #'pointsize': '10pt', 231 | 232 | # Additional stuff for the LaTeX preamble. 233 | #'preamble': '', 234 | 235 | # Latex figure (float) alignment 236 | #'figure_align': 'htbp', 237 | } 238 | 239 | # Grouping the document tree into LaTeX files. List of tuples 240 | # (source start file, target name, title, 241 | # author, documentclass [howto, manual, or own class]). 242 | latex_documents = [ 243 | (master_doc, 'PHP-HTTP.tex', u'PHP-HTTP Documentation', 244 | u'The PHP-HTTP Team', 'manual'), 245 | ] 246 | 247 | # The name of an image file (relative to this directory) to place at the top of 248 | # the title page. 249 | #latex_logo = None 250 | 251 | # For "manual" documents, if this is true, then toplevel headings are parts, 252 | # not chapters. 253 | #latex_use_parts = False 254 | 255 | # If true, show page references after internal links. 256 | #latex_show_pagerefs = False 257 | 258 | # If true, show URL addresses after external links. 259 | #latex_show_urls = False 260 | 261 | # Documents to append as an appendix to all manuals. 262 | #latex_appendices = [] 263 | 264 | # If false, no module index is generated. 265 | #latex_domain_indices = True 266 | 267 | rst_epilog = """ 268 | .. _PSR-7: https://www.php-fig.org/psr/psr-7 269 | .. _PSR-18: https://www.php-fig.org/psr/psr-18 270 | .. _Composer: https://getcomposer.org 271 | .. _HttplugBundle: https://github.com/php-http/HttplugBundle 272 | """ 273 | -------------------------------------------------------------------------------- /integrations/symfony-full-configuration.rst: -------------------------------------------------------------------------------- 1 | Full configuration 2 | ================== 3 | 4 | This page shows an example of all configuration values provided by the bundle. 5 | 6 | .. hint:: 7 | 8 | See :doc:`the plugin documentation <../plugins/index>` for more information 9 | on the plugins. 10 | 11 | If a plugin is not listed in the configuration reference below, you can 12 | configure it as a service and reference the plugin by service id as you 13 | would do for a :ref:`custom plugin `. 14 | 15 | .. code-block:: yaml 16 | 17 | // config.yml 18 | httplug: 19 | # allows to disable autowiring of the clients 20 | default_client_autowiring: true 21 | # define which service to use as httplug. 22 | # this does NOT change autowiring, which will always go to the "default" client 23 | main_alias: 24 | client: httplug.client.default 25 | psr17_request_factory: httplug.psr17_request_factory.default 26 | psr17_response_factory: httplug.psr17_response_factory.default 27 | psr17_uri_factory: httplug.psr17_uri_factory.default 28 | psr17_stream_factory: httplug.psr17_stream_factory.default 29 | classes: 30 | # uses discovery if not specified 31 | client: ~ 32 | psr17_request_factory: ~ 33 | psr17_response_factory: ~ 34 | psr17_uri_factory: ~ 35 | psr17_stream_factory: ~ 36 | 37 | plugins: # Global plugin configuration. When configured here, plugins need to be explicitly added to clients by service name. 38 | authentication: 39 | # The names can be freely chosen, the authentication type is specified in the "type" option 40 | my_basic: 41 | type: 'basic' 42 | username: 'my_username' 43 | password: 'p4ssw0rd' 44 | my_wsse: 45 | type: 'wsse' 46 | username: 'my_username' 47 | password: 'p4ssw0rd' 48 | my_bearer: 49 | type: 'bearer' 50 | token: 'authentication_token_hash' 51 | my_query_param: 52 | type: 'query_param' 53 | params: 54 | access_token: '9zh987g86fg87gh978hg9g79' 55 | my_header: 56 | type: 'header' 57 | header_name: 'ApiKey' 58 | header_value: '9zh987g86fg87gh978hg9g79' 59 | my_service: 60 | type: 'service' 61 | service: 'my_authentication_service' 62 | cache: # requires the php-http/cache-plugin package to be installed in your package 63 | cache_pool: 'my_cache_pool' 64 | stream_factory: 'httplug.stream_factory' 65 | config: 66 | default_ttl: 3600 67 | respect_response_cache_directives: ['no-cache', 'private', 'max-age', 'no-store'] 68 | cache_key_generator: null # This must be a service id to a service implementing 'Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator'. If 'null' 'Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator' will be used. 69 | cookie: 70 | cookie_jar: my_cookie_jar 71 | decoder: 72 | use_content_encoding: true 73 | history: 74 | journal: my_journal 75 | logger: 76 | logger: 'logger' 77 | formatter: null 78 | redirect: 79 | preserve_header: true 80 | use_default_for_multiple: true 81 | retry: 82 | retry: 1 83 | stopwatch: 84 | stopwatch: 'debug.stopwatch' 85 | error: 86 | enabled: false 87 | only_server_exception: false 88 | throttle: 89 | name: default 90 | key: null 91 | tokens: 1 92 | max_time: 1 93 | 94 | profiling: 95 | enabled: true # Defaults to kernel.debug 96 | formatter: null # Defaults to \Http\Message\Formatter\FullHttpMessageFormatter 97 | captured_body_length: 0 98 | 99 | discovery: 100 | client: 'auto' 101 | async_client: false 102 | 103 | clients: 104 | acme: 105 | factory: 'httplug.factory.guzzle6' 106 | service: 'my_service' # Can not be used with "factory" or "config" 107 | flexible_client: false # Can only be true if http_methods_client is false 108 | http_methods_client: false # Can only be true if flexible_client is false 109 | public: null # Set to true if you really cannot use dependency injection and need to make the client service public 110 | config: 111 | # Options to the Guzzle 6 constructor 112 | timeout: 2 113 | plugins: 114 | # Can reference a globally configured plugin service 115 | - 'httplug.plugin.authentication.my_wsse' 116 | # Configure a plugin using a custom PluginConfigurator 117 | - configurator: 118 | id: App\Httplug\Plugin\MyPluginConfigurator 119 | config: 120 | foo: 'bar' 121 | baz: 'qux' 122 | # Can configure a plugin customized for this client 123 | - cache: 124 | cache_pool: 'my_other_pool' 125 | config: 126 | default_ttl: 120 127 | # Can configure plugins that can not be configured globally 128 | - add_host: 129 | # Host name including protocol and optionally the port number, e.g. https://api.local:8000 130 | host: http://localhost:80 # Required 131 | # Whether to replace the host if request already specifies it 132 | replace: false 133 | - add_path: 134 | # Path to be added, e.g. /api/v1 135 | path: /api/v1 # Required 136 | - base_uri: 137 | # Base Uri including protocol, optionally the port number and prepend path, e.g. https://api.local:8000/api 138 | uri: http://localhost:80 # Required 139 | # Whether to replace the host if request already specifies one 140 | replace: false 141 | # Set content-type header based on request body, if the header is not already set 142 | - content_type: 143 | # skip content-type detection if body is larger than size_limit 144 | skip_detection: true 145 | # size_limit in bytes for when skip_detection is enabled 146 | size_limit: 200000 147 | # Append headers to the request. If the header already exists the value will be appended to the current value. 148 | - header_append: 149 | # Keys are the header names, values the header values 150 | headers: 151 | 'X-FOO': bar # contrary to default symfony behavior, hyphens "-" are NOT translated to underscores "_" for the headers. 152 | # Set header to default value if it does not exist. 153 | - header_defaults: 154 | # Keys are the header names, values the header values 155 | headers: 156 | 'X-FOO': bar 157 | # Set headers to requests. If the header does not exist it wil be set, if the header already exists it will be replaced. 158 | - header_set: 159 | # Keys are the header names, values the header values 160 | headers: 161 | 'X-FOO': bar 162 | # Remove headers from requests. 163 | - header_remove: 164 | # List of header names to remove 165 | headers: ["X-FOO"] 166 | # Sets query parameters to default value if they are not present in the request. 167 | - query_defaults: 168 | parameters: 169 | locale: en 170 | # Plugins to ensure the request resp response body is seekable 171 | - request_seekable_body: 172 | use_file_buffer: true 173 | memory_buffer_size: 2097152 174 | - response_seekable_body: 175 | use_file_buffer: true 176 | memory_buffer_size: 2097152 177 | # Enable VCR plugin integration (Must be installed first). 178 | - vcr: 179 | mode: replay # record | replay | replay_or_record 180 | fixtures_directory: '%kernel.project_dir%/fixtures/http' # mandatory for "filesystem" recorder 181 | # recorder: filesystem ## Can be filesystem, in_memory or the id of your custom recorder 182 | # naming_strategy: service_id.of.naming_strategy # or "default" 183 | # naming_strategy_options: # options for the default naming strategy, see VCR plugin documentation 184 | # hash_headers: [] 185 | # hash_body_methods: [] 186 | -------------------------------------------------------------------------------- /clients.rst: -------------------------------------------------------------------------------- 1 | Clients & Adapters 2 | ================== 3 | 4 | There are two types of libraries you can use to send HTTP messages; clients and adapters. A client implements the 5 | ``HttpClient`` and/or the ``HttpAsyncClient`` interfaces directly. A client adapter is a class implementing the 6 | interface and forwarding the calls to an HTTP client not implementing the interface. (See `Adapter pattern`_ on Wikipedia). 7 | 8 | .. hint:: 9 | 10 | Modern PHP clients implement the ``PSR-18 HTTP Client`` standard. If you want to do synchronous requests, you don't 11 | need a PHP-HTTP adapter anymore. We keep providing the the curl client and a mock client for testing. 12 | 13 | The adapters are still useful if you need the PHP-HTTP ``HttpAsyncClient``. 14 | 15 | .. note:: 16 | 17 | All clients and adapters comply with `Liskov substitution principle`_ which means that you can easily change one 18 | for another without any side effects. 19 | 20 | .. toctree:: 21 | :hidden: 22 | 23 | clients/curl-client 24 | clients/mock-client 25 | clients/symfony-client 26 | clients/artax-adapter 27 | clients/guzzle7-adapter 28 | clients/react-adapter 29 | clients/buzz-adapter 30 | clients/cakephp-adapter 31 | clients/guzzle5-adapter 32 | clients/guzzle6-adapter 33 | clients/socket-client 34 | clients/zend-adapter 35 | 36 | Current Clients and Adapters 37 | ---------------------------- 38 | 39 | .. csv-table:: 40 | :header: "Name", "Type", "Links", "Stats" 41 | :widths: 32, 15, 15, 38 42 | 43 | "``php-http/curl-client``", "Client", ":doc:`Docs `, `Repo `__", "|curl_version| |curl_downloads| " 44 | "``php-http/mock-client``", "Client", ":doc:`Docs `, `Repo `__", "|mock_version| |mock_downloads| " 45 | "``symfony/http-client``", "Client", ":doc:`Docs `, `Repo `__", "|symfony_version| |symfony_downloads| " 46 | "``php-http/artax-adapter``", "Adapter", ":doc:`Docs `, `Repo `__", "|artax_version| |artax_downloads| " 47 | "``php-http/guzzle7-adapter``", "Adapter", ":doc:`Docs `, `Repo `__", "|guzzle7_version| |guzzle7_downloads| " 48 | "``php-http/react-adapter``", "Adapter", ":doc:`Docs `, `Repo `__", "|react_version| |react_downloads| " 49 | 50 | Legacy Clients and Adapters 51 | --------------------------- 52 | 53 | These are not maintained anymore, but we keep documentation around for now. Please upgrade your applications to use a maintained client or adapter. 54 | 55 | .. csv-table:: 56 | :header: "Name", "Type", "Links", "Stats" 57 | :widths: 32, 15, 15, 38 58 | 59 | "``php-http/socket-client``", "Client", ":doc:`Docs `, `Repo `__", "|socket_version| |socket_downloads| " 60 | "``php-http/buzz-adapter``", "Adapter", ":doc:`Docs `, `Repo `__", "|buzz_version| |buzz_downloads| " 61 | "``php-http/cakephp-adapter``", "Adapter", ":doc:`Docs `, `Repo `__", "|cakephp_version| |cakephp_downloads| " 62 | "``php-http/guzzle5-adapter``", "Adapter", ":doc:`Docs `, `Repo `__", "|guzzle5_version| |guzzle5_downloads| " 63 | "``php-http/guzzle6-adapter``", "Adapter", ":doc:`Docs `, `Repo `__", "|guzzle6_version| |guzzle6_downloads| " 64 | "``php-http/zend-adapter``", "Adapter", ":doc:`Docs `, `Repo `__", "|zend_version| |zend_downloads| " 65 | 66 | Composer Virtual Packages 67 | ------------------------- 68 | 69 | Virtual packages are a way to specify the dependency on an implementation of an interface-only repository 70 | without forcing a specific implementation. For HTTPlug, the virtual packages are called `php-http/client-implementation`_ 71 | (though you should be using `psr/http-client-implementation`_ to use PSR-18) and `php-http/async-client-implementation`_. 72 | 73 | There is no library registered with those names. However, all client implementations (including client adapters) for 74 | HTTPlug use the ``provide`` section to tell composer that they do provide the client-implementation. 75 | 76 | .. _`php-http/client-implementation`: https://packagist.org/providers/php-http/client-implementation 77 | .. _`psr/http-client-implementation`: https://packagist.org/providers/psr/http-client-implementation 78 | .. _`php-http/async-client-implementation`: https://packagist.org/providers/php-http/async-client-implementation 79 | .. _`Adapter pattern`: https://en.wikipedia.org/wiki/Adapter_pattern 80 | .. _`Liskov substitution principle`: https://en.wikipedia.org/wiki/Liskov_substitution_principle 81 | 82 | 83 | .. |curl_downloads| image:: https://img.shields.io/packagist/dt/php-http/curl-client.svg?style=flat-square 84 | :target: https://packagist.org/packages/php-http/curl-client 85 | :alt: Total Downloads 86 | .. |curl_version| image:: https://img.shields.io/github/release/php-http/curl-client.svg?style=flat-square 87 | :target: https://github.com/php-http/curl-client/releases 88 | :alt: Latest Version 89 | 90 | .. |socket_downloads| image:: https://img.shields.io/packagist/dt/php-http/socket-client.svg?style=flat-square 91 | :target: https://packagist.org/packages/php-http/socket-client 92 | :alt: Total Downloads 93 | .. |socket_version| image:: https://img.shields.io/github/release/php-http/socket-client.svg?style=flat-square 94 | :target: https://github.com/php-http/socket-client/releases 95 | :alt: Latest Version 96 | 97 | .. |mock_downloads| image:: https://img.shields.io/packagist/dt/php-http/mock-client.svg?style=flat-square 98 | :target: https://packagist.org/packages/php-http/mock-client 99 | :alt: Total Downloads 100 | .. |mock_version| image:: https://img.shields.io/github/release/php-http/mock-client.svg?style=flat-square 101 | :target: https://github.com/php-http/mock-client/releases 102 | :alt: Latest Version 103 | 104 | .. |symfony_downloads| image:: https://img.shields.io/packagist/dt/symfony/http-client.svg?style=flat-square 105 | :target: https://packagist.org/packages/symfony/http-client 106 | :alt: Total Downloads 107 | .. |symfony_version| image:: https://img.shields.io/github/v/tag/symfony/http-client.svg?style=flat-square 108 | :target: https://github.com/symfony/http-client/releases 109 | :alt: Latest Version 110 | 111 | .. |artax_downloads| image:: https://img.shields.io/packagist/dt/php-http/artax-adapter.svg?style=flat-square 112 | :target: https://packagist.org/packages/php-http/artax-adapter 113 | :alt: Total Downloads 114 | .. |artax_version| image:: https://img.shields.io/github/release/php-http/artax-adapter.svg?style=flat-square 115 | :target: https://github.com/php-http/artax-adapter/releases 116 | :alt: Latest Version 117 | 118 | .. |buzz_downloads| image:: https://img.shields.io/packagist/dt/php-http/buzz-adapter.svg?style=flat-square 119 | :target: https://packagist.org/packages/php-http/buzz-adapter 120 | :alt: Total Downloads 121 | .. |buzz_version| image:: https://img.shields.io/github/release/php-http/buzz-adapter.svg?style=flat-square 122 | :target: https://github.com/php-http/buzz-adapter/releases 123 | :alt: Latest Version 124 | 125 | .. |cakephp_downloads| image:: https://img.shields.io/packagist/dt/php-http/cakephp-adapter.svg?style=flat-square 126 | :target: https://packagist.org/packages/php-http/cakephp-adapter 127 | :alt: Total Downloads 128 | .. |cakephp_version| image:: https://img.shields.io/github/release/php-http/cakephp-adapter.svg?style=flat-square 129 | :target: https://github.com/php-http/cakephp-adapter/releases 130 | :alt: Latest Version 131 | 132 | .. |guzzle5_downloads| image:: https://img.shields.io/packagist/dt/php-http/guzzle5-adapter.svg?style=flat-square 133 | :target: https://packagist.org/packages/php-http/guzzle5-adapter 134 | :alt: Total Downloads 135 | .. |guzzle5_version| image:: https://img.shields.io/github/release/php-http/guzzle5-adapter.svg?style=flat-square 136 | :target: https://github.com/php-http/guzzle5-adapter/releases 137 | :alt: Latest Version 138 | 139 | .. |guzzle6_downloads| image:: https://img.shields.io/packagist/dt/php-http/guzzle6-adapter.svg?style=flat-square 140 | :target: https://packagist.org/packages/php-http/guzzle6-adapter 141 | :alt: Total Downloads 142 | .. |guzzle6_version| image:: https://img.shields.io/github/release/php-http/guzzle6-adapter.svg?style=flat-square 143 | :target: https://github.com/php-http/guzzle6-adapter/releases 144 | :alt: Latest Version 145 | 146 | .. |guzzle7_downloads| image:: https://img.shields.io/packagist/dt/php-http/guzzle7-adapter.svg?style=flat-square 147 | :target: https://packagist.org/packages/php-http/guzzle7-adapter 148 | :alt: Total Downloads 149 | .. |guzzle7_version| image:: https://img.shields.io/github/release/php-http/guzzle7-adapter.svg?style=flat-square 150 | :target: https://github.com/php-http/guzzle7-adapter/releases 151 | :alt: Latest Version 152 | 153 | .. |react_downloads| image:: https://img.shields.io/packagist/dt/php-http/react-adapter.svg?style=flat-square 154 | :target: https://packagist.org/packages/php-http/react-adapter 155 | :alt: Total Downloads 156 | .. |react_version| image:: https://img.shields.io/github/release/php-http/react-adapter.svg?style=flat-square 157 | :target: https://github.com/php-http/react-adapter/releases 158 | :alt: Latest Version 159 | 160 | .. |zend_downloads| image:: https://img.shields.io/packagist/dt/php-http/zend-adapter.svg?style=flat-square 161 | :target: https://packagist.org/packages/php-http/zend-adapter 162 | :alt: Total Downloads 163 | .. |zend_version| image:: https://img.shields.io/github/release/php-http/zend-adapter.svg?style=flat-square 164 | :target: https://github.com/php-http/zend-adapter/releases 165 | :alt: Latest Version 166 | -------------------------------------------------------------------------------- /components/client-common.rst: -------------------------------------------------------------------------------- 1 | Client Common 2 | ============= 3 | 4 | The client-common package provides some useful tools for working with HTTPlug. 5 | Include them in your project with composer: 6 | 7 | .. code-block:: bash 8 | 9 | composer require php-http/client-common 10 | 11 | HttpMethodsClient 12 | ----------------- 13 | 14 | This client wraps the HttpClient and provides convenience methods for common HTTP requests like ``GET`` and ``POST``. 15 | To be able to do that, it also wraps a message factory:: 16 | 17 | use Http\Discovery\HttpClientDiscovery; 18 | use Http\Discovery\MessageFactoryDiscovery; 19 | 20 | $client = new HttpMethodsClient( 21 | HttpClientDiscovery::find(), 22 | MessageFactoryDiscovery::find() 23 | ); 24 | 25 | $foo = $client->get('http://example.com/foo'); 26 | $bar = $client->get('http://example.com/bar', ['accept-encoding' => 'application/json']); 27 | $post = $client->post('http://example.com/update', [], 'My post body'); 28 | 29 | ..versionadded:: 2.0 30 | ``HttpMethodsClient`` is final since version 2.0. You can typehint the 31 | ``HttpMethodsClientInterface`` to allow mocking the client in unit tests. 32 | 33 | BatchClient 34 | ----------- 35 | 36 | This client wraps a HttpClient and extends it with the possibility to send an array of requests and to retrieve 37 | their responses as a ``BatchResult``:: 38 | 39 | use Http\Discovery\HttpClientDiscovery; 40 | use Http\Discovery\MessageFactoryDiscovery; 41 | 42 | $messageFactory = MessageFactoryDiscovery::find(); 43 | 44 | $requests = [ 45 | $messageFactory->createRequest('GET', 'http://example.com/foo'), 46 | $messageFactory->createRequest('POST', 'http://example.com/update', [], 'My post body'), 47 | ]; 48 | 49 | $client = new BatchClient( 50 | HttpClientDiscovery::find() 51 | ); 52 | 53 | $batchResult = $client->sendRequests($requests); 54 | 55 | ..versionadded:: 2.0 56 | ``BatchClient`` is final since version 2.0. You can typehint the 57 | ``BatchClientInterface`` to allow mocking the client in unit tests. 58 | 59 | The ``BatchResult`` itself is an object that contains responses for all requests sent. 60 | It provides methods that give appropriate information based on a given request:: 61 | 62 | $requests = [ 63 | $messageFactory->createRequest('GET', 'http://example.com/foo'), 64 | $messageFactory->createRequest('POST', 'http://example.com/update', [], 'My post body'), 65 | ]; 66 | 67 | $batchResult = $client->sendRequests($requests); 68 | 69 | if ($batchResult->hasResponses()) { 70 | $fooSuccessful = $batchResult->isSuccessful($requests[0]); 71 | $updateResponse = $batchResult->getResponseFor($request[1]); 72 | } 73 | 74 | If one or more of the requests throw exceptions, they are added to the 75 | ``BatchResult`` and the ``BatchClient`` will ultimately throw a 76 | ``BatchException`` containing the ``BatchResult`` and therefore its exceptions:: 77 | 78 | $requests = [ 79 | $messageFactory->createRequest('GET', 'http://example.com/update'), 80 | ]; 81 | 82 | try { 83 | $batchResult = $client->sendRequests($requests); 84 | } catch (BatchException $e) { 85 | var_dump($e->getResult()->getExceptions()); 86 | } 87 | 88 | PluginClient 89 | ------------ 90 | 91 | See :doc:`the documentation about plugins ` 92 | 93 | HttpClientPool 94 | -------------- 95 | 96 | The ``HttpClientPool`` allows to balance requests between a pool of ``HttpClient`` and/or ``HttpAsyncClient``. 97 | 98 | The use cases are: 99 | 100 | - Using a cluster (like an Elasticsearch service with multiple master nodes) 101 | - Using fallback servers with the combination of the ``RetryPlugin`` (see :doc:`/plugins/retry`) 102 | 103 | You can attach HTTP clients to this kind of client by using the ``addHttpClient`` method:: 104 | 105 | use Http\Client\Common\HttpClientPool\LeastUsedClientPool; 106 | use Http\Discovery\HttpAsyncClientDiscovery; 107 | use Http\Discovery\HttpClientDiscovery; 108 | use Http\Discovery\MessageFactoryDiscovery; 109 | 110 | $messageFactory = MessageFactoryDiscovery::find(); 111 | 112 | $httpClient = HttpClientDiscovery::find(); 113 | $httpAsyncClient = HttpAsyncClientDiscovery::find(); 114 | 115 | $httpClientPool = new LeastUsedClientPool(); 116 | $httpClientPool->addHttpClient($httpClient); 117 | $httpClientPool->addHttpClient($httpAsyncClient); 118 | 119 | $httpClientPool->sendRequest($messageFactory->createRequest('GET', 'http://example.com/update')); 120 | 121 | Clients added to the pool are decorated with the ``HttpClientPoolItem`` class unless they already are an instance of this class. 122 | The pool item class lets the pool be aware of the number of requests currently being processed by that client. 123 | It is also used to deactivate clients when they receive errors. 124 | Deactivated clients can be reactivated after a certain amount of time, however, by default, they stay deactivated forever. 125 | To enable the behavior, wrap the clients with the ``HttpClientPoolItem`` class yourself and specify the re-enable timeout:: 126 | 127 | // Reactivate after 30 seconds 128 | $httpClientPool->addHttpClient(new HttpClientPoolItem($httpClient, 30)); 129 | // Reactivate after each call 130 | $httpClientPool->addHttpClient(new HttpClientPoolItem($httpClient, 0)); 131 | // Never reactivate the client (default) 132 | $httpClientPool->addHttpClient(new HttpClientPoolItem($httpClient, null)); 133 | 134 | ``HttpClientPool`` is an interface. There are three concrete implementations with specific strategies on how to choose clients: 135 | 136 | LeastUsedClientPool 137 | ******************* 138 | 139 | ``LeastUsedClientPool`` choose the client with the fewest requests in progress. As it sounds the best strategy for 140 | sending a request on a pool of clients, this strategy has some limitations: : 141 | 142 | - The counter is not shared between PHP process, so this strategy is not so useful in a web context, however it will make 143 | better sense in a PHP command line context. 144 | - This pool only makes sense with asynchronous clients. If you use ``sendRequest``, the call is blocking, and the pool 145 | will only ever use the first client as its request count will be 0 once ``sendRequest`` finished. 146 | 147 | A deactivated client will be removed for the pool until it is reactivated, if none are available it will throw a 148 | ``NotFoundHttpClientException`` 149 | 150 | RoundRobinClientPool 151 | ******************** 152 | 153 | ``RoundRobinClientPool`` keeps an internal pointer on the pool. At each call the pointer is moved to the next client, if 154 | the current client is disabled it will move to the next client, and if none are available it will throw a 155 | ``NotFoundHttpClientException`` 156 | 157 | The pointer is not shared across PHP processes, so for each new one it will always start on the first client. 158 | 159 | RandomClientPool 160 | **************** 161 | 162 | ``RandomClientPool`` randomly choose an available client, throw a ``NotFoundHttpClientException`` if none are available. 163 | 164 | 165 | HTTP Client Router 166 | ------------------ 167 | 168 | This client accepts pairs of clients and request matchers. 169 | Every request is "routed" through the ``HttpClientRouter``, checked against the request matchers 170 | and sent using the first matched client. If there is no matching client, an exception is thrown. 171 | 172 | This allows a single client to be used for different requests. 173 | 174 | In the following example we use the client router to access an API protected by basic auth 175 | and also to download an image from a static host:: 176 | 177 | use Http\Client\Common\HttpClientRouter; 178 | use Http\Client\Common\PluginClient; 179 | use Http\Client\Common\Plugin\AuthenticationPlugin; 180 | use Http\Client\Common\Plugin\CachePlugin; 181 | use Http\Discovery\HttpClientDiscovery; 182 | use Http\Discovery\MessageFactoryDiscovery; 183 | use Http\Message\Authentication\BasicAuth; 184 | use Http\Message\RequestMatcher\RequestMatcher; 185 | 186 | $client = new HttpClientRouter(); 187 | 188 | $requestMatcher = new RequestMatcher(null, 'api.example.com'); 189 | $pluginClient = new PluginClient( 190 | HttpClientDiscovery::find(), 191 | [new AuthenticationPlugin(new BasicAuth('user', 'password'))] 192 | ); 193 | 194 | $client->addClient($pluginClient, $requestMatcher); 195 | 196 | 197 | $requestMatcher = new RequestMatcher(null, 'images.example.com'); 198 | 199 | /** @var \Psr\Cache\CacheItemPoolInterface $pool */ 200 | $pool = ... 201 | /** @var \Http\Message\StreamFactory $streamFactory */ 202 | $streamFactory = ... 203 | 204 | $pluginClient = new PluginClient( 205 | HttpClientDiscovery::find(), 206 | [new CachePlugin($pool, $streamFactory)] 207 | ); 208 | 209 | $client->addClient($pluginClient, $requestMatcher); 210 | 211 | 212 | $messageFactory = MessageFactoryDiscovery::find(); 213 | 214 | // Get the user data 215 | $request = $messageFactory->createRequest('GET', 'https://api.example.com/user/1'); 216 | 217 | $response = $client->send($request); 218 | $imagePath = json_decode((string) $response->getBody(), true)['image_path']; 219 | 220 | // Download the image and store it in cache 221 | $request = $messageFactory->createRequest('GET', 'https://images.example.com/user/'.$imagePath); 222 | 223 | $response = $client->send($request); 224 | 225 | file_put_contents('path/to/images/'.$imagePath, (string) $response->getBody()); 226 | 227 | $request = $messageFactory->createRequest('GET', 'https://api2.example.com/user/1'); 228 | 229 | // Throws an Http\Client\Exception\RequestException 230 | $client->send($request); 231 | 232 | 233 | .. note:: 234 | 235 | When you have small difference between the underlying clients (for example different credentials based on host) 236 | it's easier to use the ``RequestConditionalPlugin`` and the ``PluginClient``, 237 | but in that case the routing logic is integrated into the linear request flow 238 | which might make debugging harder. 239 | 240 | ..versionadded:: 2.0 241 | ``HttpClientRouter`` is final since version 2.0. You can typehint the 242 | ``HttpClientRouterInterface`` to allow mocking the client in unit tests. 243 | -------------------------------------------------------------------------------- /plugins/cache.rst: -------------------------------------------------------------------------------- 1 | Cache Plugin 2 | ============ 3 | 4 | Install 5 | ------- 6 | 7 | .. code-block:: bash 8 | 9 | $ composer require php-http/cache-plugin 10 | 11 | Usage 12 | ----- 13 | 14 | The ``CachePlugin`` allows you to cache responses from the server. It can use 15 | any PSR-6 compatible caching engine. By default, the plugin respects the cache 16 | control headers from the server as specified in :rfc:`7234`. It needs a 17 | `PSR-17`_ StreamFactoryInterface and a `PSR-6`_ implementation:: 18 | 19 | use Http\Discovery\HttpClientDiscovery; 20 | use Http\Client\Common\PluginClient; 21 | use Http\Client\Common\Plugin\CachePlugin; 22 | 23 | /** @var \Psr\Cache\CacheItemPoolInterface $pool */ 24 | $pool = ... 25 | /** @var \Psr\Http\Message\StreamFactoryInterface $streamFactory */ 26 | $streamFactory = ... 27 | 28 | $options = []; 29 | $cachePlugin = new CachePlugin($pool, $streamFactory, $options); 30 | 31 | $pluginClient = new PluginClient( 32 | HttpClientDiscovery::find(), 33 | [$cachePlugin] 34 | ); 35 | 36 | 37 | The ``CachePlugin`` has also 2 factory methods to easily set up the plugin by caching type. See the example below. 38 | 39 | .. code-block:: php 40 | 41 | // This will allow caching responses with the 'private' and/or 'no-store' cache directives 42 | $cachePlugin = CachePlugin::clientCache($pool, $streamFactory, $options); 43 | 44 | // Returns a cache plugin with the current default behavior 45 | $cachePlugin = CachePlugin::serverCache($pool, $streamFactory, $options); 46 | 47 | .. note:: 48 | 49 | The two factory methods have been added in version 1.3.0. 50 | 51 | Options 52 | ------- 53 | 54 | The third parameter to the ``CachePlugin`` constructor takes an array of options. The available options are: 55 | 56 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 57 | | Name | Default value | Description | 58 | +=======================================+====================================================+=======================================================================+ 59 | | ``default_ttl`` | ``0`` | The default max age of a Response | 60 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 61 | | ``respect_cache_headers`` | ``true`` | Whether we should care about cache headers or not | 62 | | | | * This option is deprecated. Use `respect_response_cache_directives` | 63 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 64 | | ``hash_algo`` | ``sha1`` | The hashing algorithm to use when generating cache keys | 65 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 66 | | ``cache_lifetime`` | 30 days | The minimum time we should store a cache item | 67 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 68 | | ``methods`` | ``['GET', 'HEAD']`` | Which request methods to cache | 69 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 70 | | ``respect_response_cache_directives`` | ``['no-cache', 'private', 'max-age', 'no-store']`` | A list of cache directives to respect when caching responses | 71 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 72 | | ``cache_key_generator`` | ``new SimpleGenerator()`` | A class implementing ``CacheKeyGenerator`` to generate a PSR-6 cache | 73 | | | | key. | 74 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 75 | | ``cache_listeners`` | ``[]`` | A array of classes implementing ``CacheListener`` to act on a | 76 | | | | response with information on its cache status. | 77 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 78 | | ``blacklisted_paths`` | ``[]`` | A array of regular expressions to defined paths, that shall not be | 79 | | | | cached. | 80 | +---------------------------------------+----------------------------------------------------+-----------------------------------------------------------------------+ 81 | 82 | 83 | .. note:: 84 | 85 | A HTTP response may have expired but it is still in cache. If so, headers like ``If-Modified-Since`` and 86 | ``If-None-Match`` are added to the HTTP request to allow the server answer with 304 status code. When 87 | a 304 response is received we update the CacheItem and save it again for at least ``cache_lifetime``. 88 | 89 | Using these options together you can control how your responses should be cached. By default, responses with no 90 | cache control headers are not cached. If you want a default cache lifetime if the server specifies no ``max-age``, use:: 91 | 92 | $options = [ 93 | 'default_ttl' => 42, // cache lifetime time in seconds 94 | ]; 95 | 96 | You can tell the plugin to completely ignore the cache control headers from the server and force caching the response 97 | for the default time to live. The options below will cache all responses for one hour:: 98 | 99 | $options = [ 100 | 'default_ttl' => 3600, // cache for one hour 101 | 'respect_response_cache_directives' => [], 102 | ]; 103 | 104 | 105 | Generating a cache key 106 | `````````````````````` 107 | 108 | You may define a method how the PSR-6 cache key should be generated. The default generator is ``SimpleGenerator`` which 109 | is using the request method, URI and body of the request. The cache plugin does also include a ``HeaderCacheKeyGenerator`` 110 | which allow you to specify what HTTP header you want include in the cache key. 111 | 112 | Controlling cache listeners 113 | ``````````````````````````` 114 | 115 | One or more classes implementing ``CacheListener`` can be added through ``cache_listeners``. These classes receive a 116 | notification on whether a request was a cache hit or miss, and can optionally mutate the response based on those signals. 117 | As an example, adding the provided ``AddHeaderCacheListener`` will mutate the response, adding an ``X-Cache`` header with 118 | a value ``HIT`` or ``MISS``, which can be useful in debugging. 119 | 120 | 121 | Semantics of null values 122 | ```````````````````````` 123 | 124 | Setting null to the options ``cache_lifetime`` or ``default_ttl`` means "Store this as long as you can (forever)". 125 | This could be a great thing when you requesting a pay-per-request API (e.g. GoogleTranslate). 126 | 127 | Store a response as long the cache implementation allows:: 128 | 129 | $options = [ 130 | 'default_ttl' => null, 131 | 'respect_response_cache_directives' => [], 132 | 'cache_lifetime' => null, 133 | ]; 134 | 135 | 136 | Ask the server if the response is valid at most every hour. Store the cache item forever:: 137 | 138 | $options = [ 139 | 'default_ttl' => 3600, 140 | 'respect_response_cache_directives' => [], 141 | 'cache_lifetime' => null, 142 | ]; 143 | 144 | 145 | Ask the server if the response is valid at most every hour. If the response has not been used within one year it will be 146 | removed from the cache:: 147 | 148 | $options = [ 149 | 'default_ttl' => 3600, 150 | 'respect_response_cache_directives' => [], 151 | 'cache_lifetime' => 86400*365, // one year 152 | ]; 153 | 154 | Caching of different request methods 155 | ```````````````````````````````````` 156 | 157 | Most of the time you should not change the ``methods`` option. However if you are working for example with HTTPlug 158 | based SOAP client you might want to additionally enable caching of ``POST`` requests:: 159 | 160 | $options = [ 161 | 'methods' => ['GET', 'HEAD', 'POST'], 162 | ]; 163 | 164 | The ``methods`` setting overrides the defaults. If you want to keep caching ``GET`` and ``HEAD`` requests, you need 165 | to include them. You can specify any uppercase request method which conforms to :rfc:`7230`. 166 | 167 | .. note:: 168 | 169 | If your system has both normal and SOAP clients you need to use two different ``PluginClient`` instances. SOAP 170 | client should use ``PluginClient`` with POST caching enabled and normal client with POST caching disabled. 171 | 172 | Cache Control Handling 173 | ---------------------- 174 | 175 | By default this plugin does not cache responses with ``no-store``, ``no-cache`` or ``private`` instructions. Use 176 | ``CachePlugin::clientCache($pool, $streamFactory, $options);`` to cache ``no-store`` or ``private`` responses or change 177 | the ``respect_response_cache_directives`` option to your needs. 178 | 179 | It does store responses with cookies or a ``Set-Cookie`` header. Be careful with 180 | the order of your plugins. 181 | 182 | .. _PSR-6: http://www.php-fig.org/psr/psr-6/ 183 | .. _PSR-17: http://www.php-fig.org/psr/psr-17/ 184 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "acd34c96d2a0db595317b37102691e580828d52c4241fd0fbeac370ed1964db3" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alabaster": { 20 | "hashes": [ 21 | "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", 22 | "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" 23 | ], 24 | "version": "==0.7.12" 25 | }, 26 | "babel": { 27 | "hashes": [ 28 | "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", 29 | "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" 30 | ], 31 | "index": "pypi", 32 | "version": "==2.9.1" 33 | }, 34 | "docutils": { 35 | "hashes": [ 36 | "sha256:c7db717810ab6965f66c8cf0398a98c9d8df982da39b4cd7f162911eb89596fa", 37 | "sha256:dcebd4928112631626f4c4d0df59787c748404e66dda952110030ea883d3b8cd" 38 | ], 39 | "index": "pypi", 40 | "version": "==0.12" 41 | }, 42 | "imagesize": { 43 | "hashes": [ 44 | "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", 45 | "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" 46 | ], 47 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 48 | "version": "==1.2.0" 49 | }, 50 | "jinja2": { 51 | "hashes": [ 52 | "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45", 53 | "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c" 54 | ], 55 | "markers": "python_version >= '3.6'", 56 | "version": "==3.0.2" 57 | }, 58 | "markupsafe": { 59 | "hashes": [ 60 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 61 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 62 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 63 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 64 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 65 | "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", 66 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 67 | "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", 68 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 69 | "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", 70 | "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", 71 | "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", 72 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 73 | "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", 74 | "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", 75 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 76 | "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", 77 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 78 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 79 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 80 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 81 | "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", 82 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 83 | "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", 84 | "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", 85 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 86 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 87 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 88 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 89 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 90 | "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", 91 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 92 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 93 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 94 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 95 | "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", 96 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 97 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 98 | "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", 99 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 100 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 101 | "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", 102 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 103 | "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", 104 | "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", 105 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 106 | "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", 107 | "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", 108 | "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", 109 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 110 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 111 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 112 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 113 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 114 | ], 115 | "markers": "python_version >= '3.6'", 116 | "version": "==2.0.1" 117 | }, 118 | "pyenchant": { 119 | "hashes": [ 120 | "sha256:9a66aa441535e27d228baca320f7feed1b08d0c5e6167d5e5cf455b545b7c2cd", 121 | "sha256:b9526fc2c5f1ba0637e50200b645a7c20fb6644dbc6f6322027e7d2fbf1084a5", 122 | "sha256:e8000144e61551fcab9cd1b6fdccdded20e577e8d6d0985533f0b2b9c38fd952", 123 | "sha256:fc31cda72ace001da8fe5d42f11c26e514a91fa8c70468739216ddd8de64e2a0" 124 | ], 125 | "index": "pypi", 126 | "version": "==2.0.0" 127 | }, 128 | "pygments": { 129 | "hashes": [ 130 | "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", 131 | "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" 132 | ], 133 | "markers": "python_version >= '3.5'", 134 | "version": "==2.10.0" 135 | }, 136 | "pytz": { 137 | "hashes": [ 138 | "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", 139 | "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" 140 | ], 141 | "version": "==2021.3" 142 | }, 143 | "six": { 144 | "hashes": [ 145 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 146 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 147 | ], 148 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 149 | "version": "==1.16.0" 150 | }, 151 | "snowballstemmer": { 152 | "hashes": [ 153 | "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", 154 | "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" 155 | ], 156 | "version": "==2.1.0" 157 | }, 158 | "sphinx": { 159 | "hashes": [ 160 | "sha256:82cd2728c906be96e307b81352d3fd9fb731869234c6b835cc25e9a3dfb4b7e4", 161 | "sha256:b83f430200f546bfd5088c653f0c5516af708da36066dfde08d08bedb1b33a4b" 162 | ], 163 | "index": "pypi", 164 | "version": "==1.4.9" 165 | }, 166 | "sphinx-php": { 167 | "git": "https://github.com/fabpot/sphinx-php.git", 168 | "ref": "578573fd3215a97e56a4fc113fdb58dbdb4cf6a7" 169 | }, 170 | "sphinx-rtd-theme": { 171 | "hashes": [ 172 | "sha256:0f29f544f6d037989fa0c7729a9eab7e4d8ea50d6f0ef37363f472756c1edca6", 173 | "sha256:3d804aee67721f38e840efa3a3ae52898328e64709259341f2a8901d5bf01cd4", 174 | "sha256:97a2a0c04c25fb8b5431e95fd82c304a2a08fbc596820e6081264dcc11768bd9" 175 | ], 176 | "index": "pypi", 177 | "version": "==0.1.6" 178 | }, 179 | "sphinxcontrib-spelling": { 180 | "hashes": [ 181 | "sha256:44a9445b237ade895ae1fccbe6f41422489b1ffb2a026c1b78b0c1c1c229f9bf", 182 | "sha256:e25182225d8380c886000e544024f8513a2e7dad130f8297b8c23db80e31b6ed" 183 | ], 184 | "index": "pypi", 185 | "version": "==4.2.0" 186 | } 187 | }, 188 | "develop": {} 189 | } 190 | --------------------------------------------------------------------------------